diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index a4085f09..00000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,129 +0,0 @@ -# GitHub Copilot Instructions for Writing Tools - -This repository contains a multi-component writing tools application with specific package managers and coding conventions. - -## Project Structure - -- **Frontend** (`/frontend`): TypeScript/React Microsoft Office Add-in -- **Backend** (`/backend`): Python FastAPI application - -## Package Managers - -**IMPORTANT**: Always use the correct package manager for each component: - -### Frontend (`/frontend`) -- **Use `yarn`** - NOT npm -- Commands: - - Install dependencies: `yarn` or `yarn install` - - Run dev server: `yarn run dev-server` - - Build: `yarn build` - - Lint: `yarn lint` or `yarn lint --fix` - - Test: `yarn test` - -### Backend (`/backend`) -- **Use `uv`** - NOT pip or npm -- Commands: - - Install dependencies: `uv sync` - - Run commands: `uv run ` - - Run server: `uv run python server.py` or `uv run uvicorn server:app --host localhost --port 8000 --reload` - - Run tests: `uv run pytest` - -## Coding Conventions - -### Frontend (TypeScript/React) -- Use function declarations for named components, arrow functions for unnamed components -- Use camelCase for variables and function names -- Prefer `const` over `let` and `var` -- Use `@/` prefix for internal imports (webpack alias) -- Follow ESLint rules defined in `.eslintrc.json` -- Use TypeScript strict mode -- React components should not import React explicitly (configured in ESLint) - -### Backend (Python) -- Use `uv` for dependency management -- Follow PEP 8 style guidelines -- Use type hints for function parameters and return types -- Use `ruff` for linting (configured in `ruff.toml`) -- Use `mypy` for type checking -- Use `pytest` for testing -- Use snake_case for variables and function names -- Use CamelCase for class names - -### General -- Use relative paths when referencing files in the repository -- Lint and format code before committing -- Use descriptive commit messages -- Add appropriate error handling -- Include proper logging where applicable - -## Development Commands - -### Setup -```bash -# Root level - setup Python environment -uv sync - -# Frontend setup -cd frontend -yarn install -``` - -### Running Services -```bash -# Frontend development server -cd frontend -yarn run dev-server - -# Backend server (simple) -cd backend -uv run python server.py - -# Backend server (with auto-reload) -cd backend -uv run uvicorn server:app --host localhost --port 8000 --reload -``` - -### Testing and Linting -```bash -# Frontend linting -cd frontend -yarn lint --fix - -# Backend testing -cd backend -uv run pytest - -# Backend linting -cd backend -uv run ruff check -``` - -## Architecture Notes - -- The frontend is a Microsoft Office Add-in that communicates with the backend API -- The backend provides API endpoints for text processing and AI-powered writing assistance -- Authentication is handled through Auth0 in the frontend - -## File Naming and Organization - -- Use kebab-case for file names where possible -- Group related files in appropriate directories -- Use descriptive file names that indicate purpose -- Keep components modular and reusable - -## Common Patterns - -### Frontend -- Use Jotai for state management -- Use custom hooks for reusable logic -- Implement proper error boundaries -- Use TypeScript interfaces for type safety -- Follow React best practices for component composition - -### Backend -- Use FastAPI for API endpoints -- Implement proper error handling and validation -- Use dependency injection for testability -- Use environment variables for configuration - -Remember: Always use the correct package manager (`yarn` for frontend, `uv` for backend) and follow the established coding conventions for each component. \ No newline at end of file diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 6f91203e..985bbdd8 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,19 +1,11 @@ -name: Playwright Tests +name: Playwright Visual Regression Tests on: - # push: - # branches: [ main ] - # pull_request: - # branches: [ main ] - workflow_dispatch: # Enables manual triggering only - inputs: - update-snapshots: - description: 'Update visual snapshots' - required: false - type: boolean - default: false + workflow_dispatch: # Manual trigger only + defaults: run: working-directory: frontend + jobs: test: timeout-minutes: 60 @@ -31,19 +23,10 @@ jobs: run: npm run build - name: Run Playwright tests run: npx playwright test - # Playwright will automatically start the frontend via webServer config - # API calls are mocked in the test file with page.route() - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: playwright-report path: frontend/playwright-report/ retention-days: 30 - - name: Upload updated snapshots - if: github.event.inputs.update-snapshots == 'true' - uses: actions/upload-artifact@v4 - with: - name: updated-snapshots - path: frontend/tests/**/*-snapshots/** - retention-days: 30 diff --git a/.gitignore b/.gitignore index 9e5c81d3..0a419200 100644 --- a/.gitignore +++ b/.gitignore @@ -6,20 +6,6 @@ __pycache__/ *.so # Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ *.egg-info/ .installed.cfg *.egg @@ -47,7 +33,3 @@ node_modules/ # Data *.db - -# Azurite -__azurite_* - diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 00000000..f59bd2b3 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,28 @@ +#!/usr/bin/env sh + +# Get the remote ref being pushed to +remote="$1" +url="$2" + +# Read stdin to get local and remote refs +while read local_ref local_sha remote_ref remote_sha; do + # Check if pushing to main branch + if [ "$remote_ref" = "refs/heads/main" ]; then + echo "πŸ” Pushing to main - running typecheck for experiment..." + + # Run typecheck in experiment directory + cd experiment && npm run typecheck + + # Capture exit code + TYPECHECK_EXIT=$? + + if [ $TYPECHECK_EXIT -ne 0 ]; then + echo "❌ Type check failed! Fix errors or use --no-verify to skip." + exit 1 + fi + + echo "βœ… Type check passed!" + fi +done + +exit 0 diff --git a/CLAUDE.md b/CLAUDE.md index 334c8e7b..fd0aa18e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,49 +2,546 @@ Guidance for Claude Code when working with this repository. -## Project Structure +## Project Components -Writing tools application: -- `/frontend`: TypeScript/React Microsoft Office Add-in for Word + standalone editor -- `/backend`: Python FastAPI server for LLM calls and logging +This monorepo has two separate applications. If it's ambiguous which one to use, **ask the user for clarification**. -**Central concept**: LLM helps thinking and reflection instead of replacing writing. +- **Production add-in** (see [frontend/CLAUDE.md](frontend/CLAUDE.md) and [backend/CLAUDE.md](backend/CLAUDE.md)) + - `frontend`: TypeScript/React Microsoft Office Add-in + - `backend`: Python FastAPI server -## Package Managers +- **Experiment app** (see [experiment/CLAUDE.md](experiment/CLAUDE.md)) + - `experiment`: Separate Next.js application (does not use frontend/backend) -- **Frontend**: `npm` (standard commands in `package.json`) -- **Backend**: `uv` - NOT pip - - Install: `uv sync` (run from root) - - All commands: `uv run ` + +# Instructions for the usage of Backlog.md CLI Tool -## Key Architecture +## Backlog.md: Comprehensive Project Management Tool via CLI -### Frontend (Office Add-in) -- **Office.js APIs** - Microsoft Word integration -- **State Management**: Jotai atoms (see `frontend/src/contexts/`) -- **Path Alias**: `@/*` maps to `./src/*` (webpack config) -- **Entry Points**: - - `src/taskpane.html` - Word task pane - - `src/editor/editor.html` - Standalone demo editor and user study -- **Manifest**: `frontend/manifest.xml` for Office Add-in configuration +### Assistant Objective -### Backend -- OpenAI API (`nlp.py`) + FastAPI with SSE (`server.py`) -- **Logging**: Structured logs to `/backend/logs/` -- **Auth**: Auth0 JWT tokens (work in progress) +Efficiently manage all project tasks, status, and documentation using the Backlog.md CLI, ensuring all project metadata +remains fully synchronized and up-to-date. -## Non-Obvious Configuration +### Core Capabilities -- **TypeScript**: Path aliases enabled (`@/*` β†’ `./src/*`) +- βœ… **Task Management**: Create, edit, assign, prioritize, and track tasks with full metadata +- βœ… **Search**: Fuzzy search across tasks, documents, and decisions with `backlog search` +- βœ… **Acceptance Criteria**: Granular control with add/remove/check/uncheck by index +- βœ… **Board Visualization**: Terminal-based Kanban board (`backlog board`) and web UI (`backlog browser`) +- βœ… **Git Integration**: Automatic tracking of task states across branches +- βœ… **Dependencies**: Task relationships and subtask hierarchies +- βœ… **Documentation & Decisions**: Structured docs and architectural decision records +- βœ… **Export & Reporting**: Generate markdown reports and board snapshots +- βœ… **AI-Optimized**: `--plain` flag provides clean text output for AI processing -## User Study Mode +### Why This Matters to You (AI Agent) -The application includes a built-in user study system. See [STUDY.md](STUDY.md) for complete details on: -- Study flow and URL parameters -- Condition codes and configuration -- State management and logging -- Study-specific components +1. **Comprehensive system** - Full project management capabilities through CLI +2. **The CLI is the interface** - All operations go through `backlog` commands +3. **Unified interaction model** - You can use CLI for both reading (`backlog task 1 --plain`) and writing ( + `backlog task edit 1`) +4. **Metadata stays synchronized** - The CLI handles all the complex relationships -## Testing +### Key Understanding -Testing is not yet well configured. When editing code, suggest high-value tests to add but wait for approval. +- **Tasks** live in `backlog/tasks/` as `task- - .md` files +- **You interact via CLI only**: `backlog task create`, `backlog task edit`, etc. +- **Use `--plain` flag** for AI-friendly output when viewing/listing +- **Never bypass the CLI** - It handles Git, metadata, file naming, and relationships + +--- + +# ⚠️ CRITICAL: NEVER EDIT TASK FILES DIRECTLY. Edit Only via CLI + +**ALL task operations MUST use the Backlog.md CLI commands** + +- βœ… **DO**: Use `backlog task edit` and other CLI commands +- βœ… **DO**: Use `backlog task create` to create new tasks +- βœ… **DO**: Use `backlog task edit <id> --check-ac <index>` to mark acceptance criteria +- ❌ **DON'T**: Edit markdown files directly +- ❌ **DON'T**: Manually change checkboxes in files +- ❌ **DON'T**: Add or modify text in task files without using CLI + +**Why?** Direct file editing breaks metadata synchronization, Git tracking, and task relationships. + +--- + +## 1. Source of Truth & File Structure + +### πŸ“– **UNDERSTANDING** (What you'll see when reading) + +- Markdown task files live under **`backlog/tasks/`** (drafts under **`backlog/drafts/`**) +- Files are named: `task-<id> - <title>.md` (e.g., `task-42 - Add GraphQL resolver.md`) +- Project documentation is in **`backlog/docs/`** +- Project decisions are in **`backlog/decisions/`** + +### πŸ”§ **ACTING** (How to change things) + +- **All task operations MUST use the Backlog.md CLI tool** +- This ensures metadata is correctly updated and the project stays in sync +- **Always use `--plain` flag** when listing or viewing tasks for AI-friendly text output + +--- + +## 2. Common Mistakes to Avoid + +### ❌ **WRONG: Direct File Editing** + +```markdown +# DON'T DO THIS: + +1. Open backlog/tasks/task-7 - Feature.md in editor +2. Change "- [ ]" to "- [x]" manually +3. Add notes directly to the file +4. Save the file +``` + +### βœ… **CORRECT: Using CLI Commands** + +```bash +# DO THIS INSTEAD: +backlog task edit 7 --check-ac 1 # Mark AC #1 as complete +backlog task edit 7 --notes "Implementation complete" # Add notes +backlog task edit 7 -s "In Progress" -a @agent-k # Multiple commands: change status and assign the task when you start working on the task +``` + +--- + +## 3. Understanding Task Format (Read-Only Reference) + +⚠️ **FORMAT REFERENCE ONLY** - The following sections show what you'll SEE in task files. +**Never edit these directly! Use CLI commands to make changes.** + +### Task Structure You'll See + +```markdown +--- +id: task-42 +title: Add GraphQL resolver +status: To Do +assignee: [@sara] +labels: [backend, api] +--- + +## Description + +Brief explanation of the task purpose. + +## Acceptance Criteria + +<!-- AC:BEGIN --> + +- [ ] #1 First criterion +- [x] #2 Second criterion (completed) +- [ ] #3 Third criterion + +<!-- AC:END --> + +## Implementation Plan + +1. Research approach +2. Implement solution + +## Implementation Notes + +Summary of what was done. +``` + +### How to Modify Each Section + +| What You Want to Change | CLI Command to Use | +|-------------------------|----------------------------------------------------------| +| Title | `backlog task edit 42 -t "New Title"` | +| Status | `backlog task edit 42 -s "In Progress"` | +| Assignee | `backlog task edit 42 -a @sara` | +| Labels | `backlog task edit 42 -l backend,api` | +| Description | `backlog task edit 42 -d "New description"` | +| Add AC | `backlog task edit 42 --ac "New criterion"` | +| Check AC #1 | `backlog task edit 42 --check-ac 1` | +| Uncheck AC #2 | `backlog task edit 42 --uncheck-ac 2` | +| Remove AC #3 | `backlog task edit 42 --remove-ac 3` | +| Add Plan | `backlog task edit 42 --plan "1. Step one\n2. Step two"` | +| Add Notes (replace) | `backlog task edit 42 --notes "What I did"` | +| Append Notes | `backlog task edit 42 --append-notes "Another note"` | + +--- + +## 4. Defining Tasks + +### Creating New Tasks + +**Always use CLI to create tasks:** + +```bash +# Example +backlog task create "Task title" -d "Description" --ac "First criterion" --ac "Second criterion" +``` + +### Title (one liner) + +Use a clear brief title that summarizes the task. + +### Description (The "why") + +Provide a concise summary of the task purpose and its goal. Explains the context without implementation details. + +### Acceptance Criteria (The "what") + +**Understanding the Format:** + +- Acceptance criteria appear as numbered checkboxes in the markdown files +- Format: `- [ ] #1 Criterion text` (unchecked) or `- [x] #1 Criterion text` (checked) + +**Managing Acceptance Criteria via CLI:** + +⚠️ **IMPORTANT: How AC Commands Work** + +- **Adding criteria (`--ac`)** accepts multiple flags: `--ac "First" --ac "Second"` βœ… +- **Checking/unchecking/removing** accept multiple flags too: `--check-ac 1 --check-ac 2` βœ… +- **Mixed operations** work in a single command: `--check-ac 1 --uncheck-ac 2 --remove-ac 3` βœ… + +```bash +# Examples + +# Add new criteria (MULTIPLE values allowed) +backlog task edit 42 --ac "User can login" --ac "Session persists" + +# Check specific criteria by index (MULTIPLE values supported) +backlog task edit 42 --check-ac 1 --check-ac 2 --check-ac 3 # Check multiple ACs +# Or check them individually if you prefer: +backlog task edit 42 --check-ac 1 # Mark #1 as complete +backlog task edit 42 --check-ac 2 # Mark #2 as complete + +# Mixed operations in single command +backlog task edit 42 --check-ac 1 --uncheck-ac 2 --remove-ac 3 + +# ❌ STILL WRONG - These formats don't work: +# backlog task edit 42 --check-ac 1,2,3 # No comma-separated values +# backlog task edit 42 --check-ac 1-3 # No ranges +# backlog task edit 42 --check 1 # Wrong flag name + +# Multiple operations of same type +backlog task edit 42 --uncheck-ac 1 --uncheck-ac 2 # Uncheck multiple ACs +backlog task edit 42 --remove-ac 2 --remove-ac 4 # Remove multiple ACs (processed high-to-low) +``` + +**Key Principles for Good ACs:** + +- **Outcome-Oriented:** Focus on the result, not the method. +- **Testable/Verifiable:** Each criterion should be objectively testable +- **Clear and Concise:** Unambiguous language +- **Complete:** Collectively cover the task scope +- **User-Focused:** Frame from end-user or system behavior perspective + +Good Examples: + +- "User can successfully log in with valid credentials" +- "System processes 1000 requests per second without errors" +- "CLI preserves literal newlines in description/plan/notes; `\\n` sequences are not auto‑converted" + +Bad Example (Implementation Step): + +- "Add a new function handleLogin() in auth.ts" +- "Define expected behavior and document supported input patterns" + +### Task Breakdown Strategy + +1. Identify foundational components first +2. Create tasks in dependency order (foundations before features) +3. Ensure each task delivers value independently +4. Avoid creating tasks that block each other + +### Task Requirements + +- Tasks must be **atomic** and **testable** or **verifiable** +- Each task should represent a single unit of work for one PR +- **Never** reference future tasks (only tasks with id < current task id) +- Ensure tasks are **independent** and don't depend on future work + +--- + +## 5. Implementing Tasks + +### 5.1. First step when implementing a task + +The very first things you must do when you take over a task are: + +* set the task in progress +* assign it to yourself + +```bash +# Example +backlog task edit 42 -s "In Progress" -a @{myself} +``` + +### 5.2. Create an Implementation Plan (The "how") + +Previously created tasks contain the why and the what. Once you are familiar with that part you should think about a +plan on **HOW** to tackle the task and all its acceptance criteria. This is your **Implementation Plan**. +First do a quick check to see if all the tools that you are planning to use are available in the environment you are +working in. +When you are ready, write it down in the task so that you can refer to it later. + +```bash +# Example +backlog task edit 42 --plan "1. Research codebase for references\n2Research on internet for similar cases\n3. Implement\n4. Test" +``` + +## 5.3. Implementation + +Once you have a plan, you can start implementing the task. This is where you write code, run tests, and make sure +everything works as expected. Follow the acceptance criteria one by one and MARK THEM AS COMPLETE as soon as you +finish them. + +### 5.4 Implementation Notes (PR description) + +When you are done implementing a tasks you need to prepare a PR description for it. +Because you cannot create PRs directly, write the PR as a clean description in the task notes. +Append notes progressively during implementation using `--append-notes`: + +``` +backlog task edit 42 --append-notes "Implemented X" --append-notes "Added tests" +``` + +```bash +# Example +backlog task edit 42 --notes "Implemented using pattern X because Reason Y, modified files Z and W" +``` + +**IMPORTANT**: Do NOT include an Implementation Plan when creating a task. The plan is added only after you start the +implementation. + +- Creation phase: provide Title, Description, Acceptance Criteria, and optionally labels/priority/assignee. +- When you begin work, switch to edit, set the task in progress and assign to yourself + `backlog task edit <id> -s "In Progress" -a "..."`. +- Think about how you would solve the task and add the plan: `backlog task edit <id> --plan "..."`. +- After updating the plan, share it with the user and ask for confirmation. Do not begin coding until the user approves the plan or explicitly tells you to skip the review. +- Add Implementation Notes only after completing the work: `backlog task edit <id> --notes "..."` (replace) or append progressively using `--append-notes`. + +## Phase discipline: What goes where + +- Creation: Title, Description, Acceptance Criteria, labels/priority/assignee. +- Implementation: Implementation Plan (after moving to In Progress and assigning to yourself). +- Wrap-up: Implementation Notes (Like a PR description), AC and Definition of Done checks. + +**IMPORTANT**: Only implement what's in the Acceptance Criteria. If you need to do more, either: + +1. Update the AC first: `backlog task edit 42 --ac "New requirement"` +2. Or create a new follow up task: `backlog task create "Additional feature"` + +--- + +## 6. Typical Workflow + +```bash +# 1. Identify work +backlog task list -s "To Do" --plain + +# 2. Read task details +backlog task 42 --plain + +# 3. Start work: assign yourself & change status +backlog task edit 42 -s "In Progress" -a @myself + +# 4. Add implementation plan +backlog task edit 42 --plan "1. Analyze\n2. Refactor\n3. Test" + +# 5. Share the plan with the user and wait for approval (do not write code yet) + +# 6. Work on the task (write code, test, etc.) + +# 7. Mark acceptance criteria as complete (supports multiple in one command) +backlog task edit 42 --check-ac 1 --check-ac 2 --check-ac 3 # Check all at once +# Or check them individually if preferred: +# backlog task edit 42 --check-ac 1 +# backlog task edit 42 --check-ac 2 +# backlog task edit 42 --check-ac 3 + +# 8. Add implementation notes (PR Description) +backlog task edit 42 --notes "Refactored using strategy pattern, updated tests" + +# 9. Mark task as done +backlog task edit 42 -s Done +``` + +--- + +## 7. Definition of Done (DoD) + +A task is **Done** only when **ALL** of the following are complete: + +### βœ… Via CLI Commands: + +1. **All acceptance criteria checked**: Use `backlog task edit <id> --check-ac <index>` for each +2. **Implementation notes added**: Use `backlog task edit <id> --notes "..."` +3. **Status set to Done**: Use `backlog task edit <id> -s Done` + +### βœ… Via Code/Testing: + +4. **Tests pass**: Run test suite and linting +5. **Documentation updated**: Update relevant docs if needed +6. **Code reviewed**: Self-review your changes +7. **No regressions**: Performance, security checks pass + +⚠️ **NEVER mark a task as Done without completing ALL items above** + +--- + +## 8. Finding Tasks and Content with Search + +When users ask you to find tasks related to a topic, use the `backlog search` command with `--plain` flag: + +```bash +# Search for tasks about authentication +backlog search "auth" --plain + +# Search only in tasks (not docs/decisions) +backlog search "login" --type task --plain + +# Search with filters +backlog search "api" --status "In Progress" --plain +backlog search "bug" --priority high --plain +``` + +**Key points:** +- Uses fuzzy matching - finds "authentication" when searching "auth" +- Searches task titles, descriptions, and content +- Also searches documents and decisions unless filtered with `--type task` +- Always use `--plain` flag for AI-readable output + +--- + +## 9. Quick Reference: DO vs DON'T + +### Viewing and Finding Tasks + +| Task | βœ… DO | ❌ DON'T | +|--------------|-----------------------------|---------------------------------| +| View task | `backlog task 42 --plain` | Open and read .md file directly | +| List tasks | `backlog task list --plain` | Browse backlog/tasks folder | +| Check status | `backlog task 42 --plain` | Look at file content | +| Find by topic| `backlog search "auth" --plain` | Manually grep through files | + +### Modifying Tasks + +| Task | βœ… DO | ❌ DON'T | +|---------------|--------------------------------------|-----------------------------------| +| Check AC | `backlog task edit 42 --check-ac 1` | Change `- [ ]` to `- [x]` in file | +| Add notes | `backlog task edit 42 --notes "..."` | Type notes into .md file | +| Change status | `backlog task edit 42 -s Done` | Edit status in frontmatter | +| Add AC | `backlog task edit 42 --ac "New"` | Add `- [ ] New` to file | + +--- + +## 10. Complete CLI Command Reference + +### Task Creation + +| Action | Command | +|------------------|-------------------------------------------------------------------------------------| +| Create task | `backlog task create "Title"` | +| With description | `backlog task create "Title" -d "Description"` | +| With AC | `backlog task create "Title" --ac "Criterion 1" --ac "Criterion 2"` | +| With all options | `backlog task create "Title" -d "Desc" -a @sara -s "To Do" -l auth --priority high` | +| Create draft | `backlog task create "Title" --draft` | +| Create subtask | `backlog task create "Title" -p 42` | + +### Task Modification + +| Action | Command | +|------------------|---------------------------------------------| +| Edit title | `backlog task edit 42 -t "New Title"` | +| Edit description | `backlog task edit 42 -d "New description"` | +| Change status | `backlog task edit 42 -s "In Progress"` | +| Assign | `backlog task edit 42 -a @sara` | +| Add labels | `backlog task edit 42 -l backend,api` | +| Set priority | `backlog task edit 42 --priority high` | + +### Acceptance Criteria Management + +| Action | Command | +|---------------------|-----------------------------------------------------------------------------| +| Add AC | `backlog task edit 42 --ac "New criterion" --ac "Another"` | +| Remove AC #2 | `backlog task edit 42 --remove-ac 2` | +| Remove multiple ACs | `backlog task edit 42 --remove-ac 2 --remove-ac 4` | +| Check AC #1 | `backlog task edit 42 --check-ac 1` | +| Check multiple ACs | `backlog task edit 42 --check-ac 1 --check-ac 3` | +| Uncheck AC #3 | `backlog task edit 42 --uncheck-ac 3` | +| Mixed operations | `backlog task edit 42 --check-ac 1 --uncheck-ac 2 --remove-ac 3 --ac "New"` | + +### Task Content + +| Action | Command | +|------------------|----------------------------------------------------------| +| Add plan | `backlog task edit 42 --plan "1. Step one\n2. Step two"` | +| Add notes | `backlog task edit 42 --notes "Implementation details"` | +| Add dependencies | `backlog task edit 42 --dep task-1 --dep task-2` | + +### Multi‑line Input (Description/Plan/Notes) + +The CLI preserves input literally. Shells do not convert `\n` inside normal quotes. Use one of the following to insert real newlines: + +- Bash/Zsh (ANSI‑C quoting): + - Description: `backlog task edit 42 --desc $'Line1\nLine2\n\nFinal'` + - Plan: `backlog task edit 42 --plan $'1. A\n2. B'` + - Notes: `backlog task edit 42 --notes $'Done A\nDoing B'` + - Append notes: `backlog task edit 42 --append-notes $'Progress update line 1\nLine 2'` +- POSIX portable (printf): + - `backlog task edit 42 --notes "$(printf 'Line1\nLine2')"` +- PowerShell (backtick n): + - `backlog task edit 42 --notes "Line1`nLine2"` + +Do not expect `"...\n..."` to become a newline. That passes the literal backslash + n to the CLI by design. + +Descriptions support literal newlines; shell examples may show escaped `\\n`, but enter a single `\n` to create a newline. + +### Implementation Notes Formatting + +- Keep implementation notes human-friendly and PR-ready: use short paragraphs or + bullet lists instead of a single long line. +- Lead with the outcome, then add supporting details (e.g., testing, follow-up + actions) on separate lines or bullets. +- Prefer Markdown bullets (`-` for unordered, `1.` for ordered) so Maintainers + can paste notes straight into GitHub without additional formatting. +- When using CLI flags like `--append-notes`, remember to include explicit + newlines. Example: + + ```bash + backlog task edit 42 --append-notes $'- Added new API endpoint\n- Updated tests\n- TODO: monitor staging deploy' + ``` + +### Task Operations + +| Action | Command | +|--------------------|----------------------------------------------| +| View task | `backlog task 42 --plain` | +| List tasks | `backlog task list --plain` | +| Search tasks | `backlog search "topic" --plain` | +| Search with filter | `backlog search "api" --status "To Do" --plain` | +| Filter by status | `backlog task list -s "In Progress" --plain` | +| Filter by assignee | `backlog task list -a @sara --plain` | +| Archive task | `backlog task archive 42` | +| Demote to draft | `backlog task demote 42` | + +--- + +## Common Issues + +| Problem | Solution | +|----------------------|--------------------------------------------------------------------| +| Task not found | Check task ID with `backlog task list --plain` | +| AC won't check | Use correct index: `backlog task 42 --plain` to see AC numbers | +| Changes not saving | Ensure you're using CLI, not editing files | +| Metadata out of sync | Re-edit via CLI to fix: `backlog task edit 42 -s <current-status>` | + +--- + +## Remember: The Golden Rule + +**🎯 If you want to change ANYTHING in a task, use the `backlog task edit` command.** +**πŸ“– Use CLI to read tasks, exceptionally READ task files directly, never WRITE to them.** + +Full help available: `backlog --help` + +<!-- BACKLOG.MD GUIDELINES END --> diff --git a/Jenkinsfile b/Jenkinsfile index e2c32e0e..8b359fb4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,7 +4,10 @@ pipeline { stage('Build') { steps { echo 'Building the application...' - sh 'docker compose -f docker-compose.yml -f docker-compose-prod.yml build' + sh ''' + EXP_LOGS_GID=$(getent group writing-study-irb-approved | cut -d: -f3) + docker compose -f docker-compose.yml -f docker-compose-prod.yml build --build-arg EXP_LOGS_GID=${EXP_LOGS_GID} + ''' } } // stage('Test') { diff --git a/README.md b/README.md index 7a83e18e..46951a8a 100644 --- a/README.md +++ b/README.md @@ -43,4 +43,5 @@ We follow specific GitHub conventions to keep our project organized and maintain For detailed conventions on branch naming, commit messages, PR process, and issue management, see [CONTRIBUTING.md](CONTRIBUTING.md). # Running visual regression tests -Run manually via Actions tab. To update baselines after UI changes, re-run with "Update visual snapshots" checked and commit the downloaded artifact as new baseline images. + +The visual regression tests capture screenshots and ensure UI consistency of the application. See [VISUAL_REGRESSION.md](VISUAL_REGRESSION.md) for detailed instructions on running tests and updating baseline images. diff --git a/VISUAL_REGRESSION.md b/VISUAL_REGRESSION.md new file mode 100644 index 00000000..bb0087a0 --- /dev/null +++ b/VISUAL_REGRESSION.md @@ -0,0 +1,50 @@ +# Visual Regression Testing Guide + +This project uses Playwright to run visual regression tests that capture screenshots and compare them against baseline images. Currently, we have tests set up only for the demo page of the application. + +## Running the tests + +1. Navigate to the **Actions** tab in GitHub +2. Select **Playwright Visual Regression Tests** workflow +3. Click **Run workflow** button and choose the branch you wish to run tests on +4. The workflow will run tests against all browsers (Chromium, Firefox, WebKit) + +## Understanding test results + +- **βœ… Pass**: No visual changes detected - your PR is ready for merge +- **❌ Fail**: Visual differences detected - review the changes + +## Reviewing visual differences + +When tests fail: + +1. Go to the failed workflow run +2. Download the **playwright-report** artifact +3. Extract the artifact and open `index.html` in a browser +4. Review the visual comparison showing: + - Expected (baseline) image + - Actual (current) image + - Diff highlighting the changes + +## Updating baseline images + +If the UI changes are **intentional** and you want to update the baselines: + +1. From the Playwright report, download the actual images for each browser +2. Replace the existing baseline images in `frontend/tests/demo-page-visual.spec.ts-snapshots/` +3. Rename downloaded images to match existing baseline names: + - `demo-page-chromium-linux.png` + - `demo-page-firefox-linux.png` + - `demo-page-webkit-linux.png` +4. Commit and push the updated baseline images +5. Re-run the visual regression test to verify it passes + +## Baseline image locations + +Current baseline images are stored in: +``` +frontend/tests/demo-page-visual.spec.ts-snapshots/ +β”œβ”€β”€ demo-page-chromium-linux.png +β”œβ”€β”€ demo-page-firefox-linux.png +└── demo-page-webkit-linux.png +``` diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md new file mode 100644 index 00000000..23a0de31 --- /dev/null +++ b/backend/CLAUDE.md @@ -0,0 +1,12 @@ +Python FastAPI server for LLM calls and logging for a writing tools application. + +**Central concept**: LLM helps thinking and reflection instead of replacing writing. + +Use `uv` - NOT pip. `uv run <command>` + +Aspects: + +- OpenAI API (`nlp.py`) + FastAPI with SSE (`server.py`) +- **Logging**: Structured logs to `/backend/logs/` +- **Auth**: Auth0 JWT tokens (work in progress) + diff --git a/backlog/archive/tasks/task-7 - Improve-ChatPanel-API-response-format.md b/backlog/archive/tasks/task-7 - Improve-ChatPanel-API-response-format.md new file mode 100644 index 00000000..f0cb6ec9 --- /dev/null +++ b/backlog/archive/tasks/task-7 - Improve-ChatPanel-API-response-format.md @@ -0,0 +1,40 @@ +--- +id: task-7 +title: Improve ChatPanel API response format +status: In Progress +assignee: + - '@myself' +created_date: '2025-12-02 17:17' +updated_date: '2025-12-03 21:47' +labels: + - study + - chatpanel + - api + - refactor +dependencies: [] +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Instead of current markdown parsing approach (ChatPanel.tsx:94), have backend return structured JSON object directly. Eliminates need for client-side parsing and reduces coupling. +<!-- SECTION:DESCRIPTION:END --> + +## Acceptance Criteria +<!-- AC:BEGIN --> +- [ ] #1 Backend API updated to return structured JSON +- [ ] #2 ChatPanel updated to consume structured response +- [ ] #3 Markdown parsing logic removed +- [ ] #4 Integration tests pass +<!-- AC:END --> + +## Implementation Plan + +<!-- SECTION:PLAN:BEGIN --> +1. Analyze current response structure from OpenAI API and define target JSON schema +2. Update backend to transform OpenAI responses into structured JSON format +3. Update frontend ChatPanel to consume structured response +4. Remove markdown parsing logic from ChatPanel.tsx +5. Update integration tests to match new response format +6. Test end-to-end flow to ensure messages display correctly +<!-- SECTION:PLAN:END --> diff --git a/backlog/config.yml b/backlog/config.yml new file mode 100644 index 00000000..47353ac1 --- /dev/null +++ b/backlog/config.yml @@ -0,0 +1,14 @@ +project_name: "Writing Tools" +default_status: "To Do" +statuses: ["To Do", "In Progress", "Done"] +labels: [] +milestones: [] +date_format: yyyy-mm-dd +max_column_width: 20 +auto_open_browser: true +default_port: 6420 +remote_operations: true +auto_commit: false +bypass_git_hooks: false +check_active_branches: true +active_branch_days: 30 diff --git a/backlog/tasks/task-1 - Update-consent-form-placeholders.md b/backlog/tasks/task-1 - Update-consent-form-placeholders.md new file mode 100644 index 00000000..4c589641 --- /dev/null +++ b/backlog/tasks/task-1 - Update-consent-form-placeholders.md @@ -0,0 +1,23 @@ +--- +id: task-1 +title: Update consent form placeholders +status: To Do +assignee: [] +created_date: '2025-12-02 17:13' +labels: + - study + - content +dependencies: [] +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Replace [Research Institution] and [contact information] placeholders in FinalPage.tsx with actual research institution name and contact details. File: experiment/components/study/FinalPage.tsx:62 +<!-- SECTION:DESCRIPTION:END --> + +## Acceptance Criteria +<!-- AC:BEGIN --> +- [ ] #1 Placeholder text replaced with actual institution name +- [ ] #2 Contact information added +<!-- AC:END --> diff --git a/backlog/tasks/task-10 - Log-chat-interactions.md b/backlog/tasks/task-10 - Log-chat-interactions.md new file mode 100644 index 00000000..f00b94d9 --- /dev/null +++ b/backlog/tasks/task-10 - Log-chat-interactions.md @@ -0,0 +1,56 @@ +--- +id: task-10 +title: Log chat interactions +status: Done +assignee: + - '@claude' +created_date: '2025-12-03 00:57' +updated_date: '2025-12-03 02:04' +labels: [] +dependencies: [] +priority: high +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Chat messages sent and "received" must be logged. +<!-- SECTION:DESCRIPTION:END --> + +## Acceptance Criteria +<!-- AC:BEGIN --> +- [x] #1 Log user messages sent to the chat +- [x] #2 Log assistant messages received from the chat +- [x] #3 Include message content, timestamps, and role in logs +- [x] #4 Verify logs appear in backend log files +<!-- AC:END --> + +## Implementation Plan + +<!-- SECTION:PLAN:BEGIN --> +1. Understand current logging infrastructure: + - Backend: no logging in the `experiment` server (there is in the main-app `backend` but that's separate) + - Frontend: log() function available in src/api/index.ts sends logs to /api/log + +2. Add client-side logging to ChatPanel component: + - Import log() function from src/api/index.ts + - Log user messages when sendMessage is called + - Log assistant messages when they appear in the messages array + - Include relevant metadata (message role, content, timestamp) + +3. Test logging: + - Send test messages through chat + - Verify logs appear in backend log files + - Check log format and content +<!-- SECTION:PLAN:END --> + +## Implementation Notes + +<!-- SECTION:NOTES:BEGIN --> +- Added new LogEventType events: chatMessage:user and chatMessage:assistant +- Imported log function and studyParamsAtom into ChatPanel component +- Moved getMessageText and parseMessageContent to module-level functions +- Added useEffect to track and log messages as they are added to the messages array +- Logs include messageId, content, and timestamp in extra_data +- Build succeeds with no TypeScript errors +<!-- SECTION:NOTES:END --> diff --git a/backlog/tasks/task-11 - Refactor-log-message-types.md b/backlog/tasks/task-11 - Refactor-log-message-types.md new file mode 100644 index 00000000..391f43e1 --- /dev/null +++ b/backlog/tasks/task-11 - Refactor-log-message-types.md @@ -0,0 +1,15 @@ +--- +id: task-11 +title: Refactor log message types +status: To Do +assignee: [] +created_date: '2025-12-03 00:57' +labels: [] +dependencies: [] +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Maybe we should actually type log messages, e.g., LogEvent could be a PageViewEvent | DocumentUpdateEvent | AISuggestionShown | ... event +<!-- SECTION:DESCRIPTION:END --> diff --git a/backlog/tasks/task-12 - Make-a-CLAUDE.md-for-the-experiment-subfolder.md b/backlog/tasks/task-12 - Make-a-CLAUDE.md-for-the-experiment-subfolder.md new file mode 100644 index 00000000..583af7b1 --- /dev/null +++ b/backlog/tasks/task-12 - Make-a-CLAUDE.md-for-the-experiment-subfolder.md @@ -0,0 +1,48 @@ +--- +id: task-12 +title: Make a CLAUDE.md for the experiment subfolder +status: Done +assignee: + - '@myself' +created_date: '2025-12-03 01:55' +updated_date: '2025-12-03 22:00' +labels: [] +dependencies: [] +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +We should also update the root folder CLAUDE.md accordingly. + +There's a README.md in experiment that has some of the info, but it might be out of date. +<!-- SECTION:DESCRIPTION:END --> + +## Acceptance Criteria +<!-- AC:BEGIN --> +- [x] #1 Create CLAUDE.md in /experiment folder with project setup and architecture info +- [x] #2 Update root CLAUDE.md to clarify it applies to main app (backend/frontend) only +- [x] #3 Add section in root CLAUDE.md pointing to experiment/CLAUDE.md for separate instructions +<!-- AC:END --> + +## Implementation Plan + +<!-- SECTION:PLAN:BEGIN --> +1. Create experiment/CLAUDE.md with: + - Clarification that this is a separate app from main project + - Project structure and tech stack + - Package manager: npm (not uv) + - Environment setup (.env.local with OPENAI_API_KEY) + - Development commands + - Testing setup with vitest +2. Update root CLAUDE.md: + - Add section clarifying it applies to main app only + - Add prominent note pointing to experiment/CLAUDE.md +3. Test that both documents are clear and non-conflicting +<!-- SECTION:PLAN:END --> + +## Implementation Notes + +<!-- SECTION:NOTES:BEGIN --> +Created experiment/CLAUDE.md with straightforward setup instructions, emphasizing that `/experiment` is separate from `/backend` and `/frontend`. Updated root CLAUDE.md header to clearly point to separate CLAUDE.md files for each component. +<!-- SECTION:NOTES:END --> diff --git a/backlog/tasks/task-13 - Fix-oversize-sidebar.md b/backlog/tasks/task-13 - Fix-oversize-sidebar.md new file mode 100644 index 00000000..906be5d4 --- /dev/null +++ b/backlog/tasks/task-13 - Fix-oversize-sidebar.md @@ -0,0 +1,21 @@ +--- +id: task-13 +title: Fix oversize sidebar +status: To Do +assignee: [] +created_date: '2025-12-03 17:48' +labels: [] +dependencies: [] +priority: high +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +When the chat window has more than a few messages, it pushes everything outwards vertically, nothing scrolls. +<!-- SECTION:DESCRIPTION:END --> + +## Acceptance Criteria +<!-- AC:BEGIN --> +- [ ] #1 Both Chat and AI panels should be half height, internally scrolling (flex: 1 0 auto? flex-grow?) +<!-- AC:END --> diff --git a/backlog/tasks/task-14 - Write-the-complete_document-prompt.md b/backlog/tasks/task-14 - Write-the-complete_document-prompt.md new file mode 100644 index 00000000..1b8d7595 --- /dev/null +++ b/backlog/tasks/task-14 - Write-the-complete_document-prompt.md @@ -0,0 +1,19 @@ +--- +id: task-14 +title: Write the complete_document prompt +status: Done +assignee: [] +created_date: '2025-12-03 18:04' +updated_date: '2025-12-03 18:06' +labels: [] +dependencies: [] +priority: high +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Somehow we missed this in writing-support/route.ts. + +See nlp.py? +<!-- SECTION:DESCRIPTION:END --> diff --git a/backlog/tasks/task-15 - Account-for-reading-time-in-colleague-chat.md b/backlog/tasks/task-15 - Account-for-reading-time-in-colleague-chat.md new file mode 100644 index 00000000..b158e7c1 --- /dev/null +++ b/backlog/tasks/task-15 - Account-for-reading-time-in-colleague-chat.md @@ -0,0 +1,17 @@ +--- +id: task-15 +title: Account for reading time in colleague chat +status: To Do +assignee: [] +created_date: '2025-12-03 18:11' +updated_date: '2025-12-04 18:10' +labels: [] +dependencies: [] +priority: low +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Sarah should take longer to respond to a longer participant message than a short one. +<!-- SECTION:DESCRIPTION:END --> diff --git a/backlog/tasks/task-16 - Make-sure-colleague-chat-doesn't-stream-responses.md b/backlog/tasks/task-16 - Make-sure-colleague-chat-doesn't-stream-responses.md new file mode 100644 index 00000000..27442fdf --- /dev/null +++ b/backlog/tasks/task-16 - Make-sure-colleague-chat-doesn't-stream-responses.md @@ -0,0 +1,16 @@ +--- +id: task-16 +title: Make sure colleague chat doesn't stream responses +status: Done +assignee: [] +created_date: '2025-12-03 18:12' +updated_date: '2025-12-04 18:10' +labels: [] +dependencies: [] +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +cause messaging apps don't! +<!-- SECTION:DESCRIPTION:END --> diff --git a/backlog/tasks/task-17 - Refactor-initial-chat-messages-to-be-backend-delivered.md b/backlog/tasks/task-17 - Refactor-initial-chat-messages-to-be-backend-delivered.md new file mode 100644 index 00000000..de79b2f9 --- /dev/null +++ b/backlog/tasks/task-17 - Refactor-initial-chat-messages-to-be-backend-delivered.md @@ -0,0 +1,126 @@ +--- +id: task-17 +title: Refactor initial chat messages to be backend-delivered +status: Done +assignee: + - '@Claude' +created_date: '2025-12-03 19:30' +updated_date: '2025-12-03 22:18' +labels: + - frontend + - architecture +dependencies: [] +priority: medium +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Currently, the frontend has special-case logic to display Sarah's initial messages after a fixed delay. Instead, these messages should be delivered from the backend as if they were a response to an implicit initial greeting, treating them uniformly with all other messages through the normal message handling pipeline. + +This eliminates frontend special cases and ensures initial messages get the same realistic timing as subsequent messages. + +Realistic timing works as follows: Sarah finds a moment to read your message (~400-800ms), takes time to read and think through a response (depends on your message length), types an answer (depends on her response length), then sends it. The thinking/reading delay and typing duration both use the same calculation (40-80 chars/sec Β± 300ms variation) but applied to different message lengthsβ€”Sarah thinks proportionally to what you wrote, and types proportionally to what she's typing. This creates natural pacing. +<!-- SECTION:DESCRIPTION:END --> + +## Acceptance Criteria +<!-- AC:BEGIN --> +- [x] #1 Backend serves initial messages as response to implicit greeting +- [x] #2 Frontend removes setTimeout special case for initial messages +- [x] #3 Initial messages still use hardcoded content (not LLM-generated) +- [x] #4 Initial messages display with realistic delays and typing animations like normal messages + +- [x] #5 Backend endpoint returns hardcoded initial message(s) in structured format +- [x] #6 Frontend sends implicit greeting on component mount to trigger initial messages +- [x] #7 Notification badge appears for 5 seconds when new message arrives +- [x] #8 Read indicator appears 800Β±400ms after user message is sent +- [x] #9 Multi-message delay derived from message length: pause between messages scales with typing duration (no fixed arbitrary delay) +- [x] #10 Thinking/reading delay: Sarah takes time proportional to received message length before typing response (40-80 chars/sec baseline, Β±300ms variation) +- [x] #11 Typing indicator duration: shows while Sarah types her response (calculated from response message length: 40-80 chars/sec, Β±300ms variation) +<!-- AC:END --> + +## Implementation Plan + +<!-- SECTION:PLAN:BEGIN --> +1. Update backend chat API to serve initial messages: + - Create handler for implicit greeting (empty/starter message) + - Return hardcoded initial messages array with timing metadata + - Messages formatted as JSON array: ["msg1", "msg2"] + +2. Implement realistic timing calculations: + - Thinking/reading delay: proportional to received message length (40-80 chars/sec, Β±300ms) + - Typing delay: proportional to response message length (40-80 chars/sec, Β±300ms) + - Multi-message delay: derived from typing duration of previous message + - Helper function to calculate delays + +3. Update frontend ChatPanel component: + - Remove setTimeout special cases (lines 61-102) + - Create implicit greeting message on mount + - Send implicit greeting to trigger initial messages from backend + - Route initial messages through normal displayMessages() pipeline + +4. Implement visual indicators: + - Typing indicator while thinking/typing + - Read indicator appearing 800Β±400ms after user message + - Notification badge appearing for 5 seconds on new message + - Sequential message display with delays between them + +5. Testing: + - Verify initial messages appear with correct delays + - Test typing indicators show/hide correctly + - Verify notification badge timing + - Test read indicator timing +<!-- SECTION:PLAN:END --> + +## Implementation Notes + +<!-- SECTION:NOTES:BEGIN --> +## Refactored Initial Chat Messages to Backend-Delivered + +### Changes Made + +**Backend (app/api/chat/route.ts):** +- Added INITIAL_MESSAGES constant with hardcoded initial messages +- Detect implicit greeting (empty user message) and return hardcoded messages +- Replaced streaming (`streamText`) with non-streaming (`generateText`) for all responses +- All API responses now return JSON arrays of messages + +**Frontend (components/ChatPanel.tsx):** +- Removed setTimeout special cases (old lines 61-102) +- Send implicit greeting on component mount to trigger initial messages +- Implemented message sequencing with realistic timing delays +- Added timing calculation utilities based on message length + +**Timing System (lib/messageTiming.ts):** +- Implemented `calculateThinkingDelay()`: proportional to received message length (40-80 chars/sec Β± 300ms) +- Implemented `calculateTypingDuration()`: proportional to response message length (40-80 chars/sec Β± 300ms) +- Implemented `calculateInterMessageDelay()`: derived from previous message typing duration + +### Implementation Details + +1. **Initial Message Flow:** + - App mounts β†’ sends empty string message β†’ backend returns hardcoded initial messages + - Messages are parsed into array β†’ sequenced with delays + +2. **Message Display Sequencing:** + - First message appears immediately with typewriter effect + - Subsequent messages delayed based on previous message length + - Typing indicator shows during delays + - Notification badge appears for 5 seconds per message + - Read indicator visible on user messages + +3. **Realistic Timing:** + - All delays calculated from message length (40-80 chars/sec baseline) + - Β±300ms variation added to each calculation + - Thinking delay proportional to received message + - Typing duration proportional to sent message + - Multi-message delays derived from typing duration of previous message + +### Technical Details + +- No messages are streamed (all use `generateText`) +- Message parts sequenced via `visibleMessagePartCount` state +- Typing indicator shown during inter-message delays +- Notification badge triggers on new message part appearance +- All timers properly cleaned up on effect unmount +<!-- SECTION:NOTES:END --> diff --git a/backlog/tasks/task-18 - Add-company-voice-reference-document-with-popover-viewer.md b/backlog/tasks/task-18 - Add-company-voice-reference-document-with-popover-viewer.md new file mode 100644 index 00000000..579bb43e --- /dev/null +++ b/backlog/tasks/task-18 - Add-company-voice-reference-document-with-popover-viewer.md @@ -0,0 +1,25 @@ +--- +id: task-18 +title: Add company voice reference document with popover viewer +status: To Do +assignee: [] +created_date: '2026-01-08 19:57' +labels: + - experiment + - ui +dependencies: [] +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Create a 'company voice' reference document that describes the professional & empathetic tone expected in communications. Add a popover/modal viewer that can be accessed from task instructions and/or Sarah's chat messages. This helps participants understand how to represent the company in their writing. +<!-- SECTION:DESCRIPTION:END --> + +## Acceptance Criteria +<!-- AC:BEGIN --> +- [ ] #1 Create a company voice document with tone guidelines +- [ ] #2 Add popover/modal component to view the document +- [ ] #3 Link document from task instructions (StartTaskPage) +- [ ] #4 Optionally link document from Sarah's messages +<!-- AC:END --> diff --git a/backlog/tasks/task-19 - Refine-LLM-quality-analysis-prompts.md b/backlog/tasks/task-19 - Refine-LLM-quality-analysis-prompts.md new file mode 100644 index 00000000..281fea1e --- /dev/null +++ b/backlog/tasks/task-19 - Refine-LLM-quality-analysis-prompts.md @@ -0,0 +1,27 @@ +--- +id: task-19 +title: Refine LLM quality analysis prompts +status: To Do +assignee: [] +created_date: '2026-01-15 17:07' +labels: + - analysis + - llm +dependencies: [] +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Iterate on the prompts in llm_analysis.py based on actual results. Current prompts are first drafts and may need tuning for: +- More consistent scoring +- Better alignment with human judgment +- Scenario-specific nuances +<!-- SECTION:DESCRIPTION:END --> + +## Acceptance Criteria +<!-- AC:BEGIN --> +- [ ] #1 Run analysis on sample of 10+ emails +- [ ] #2 Compare LLM ratings to manual review +- [ ] #3 Adjust prompt wording based on discrepancies +<!-- AC:END --> diff --git a/backlog/tasks/task-2 - Setup-error-boundary-for-study-app.md b/backlog/tasks/task-2 - Setup-error-boundary-for-study-app.md new file mode 100644 index 00000000..4a95d545 --- /dev/null +++ b/backlog/tasks/task-2 - Setup-error-boundary-for-study-app.md @@ -0,0 +1,25 @@ +--- +id: task-2 +title: Setup error boundary for study app +status: To Do +assignee: [] +created_date: '2025-12-02 17:13' +labels: + - study + - infrastructure + - error-handling +dependencies: [] +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Pull Posthog error boundary implementation from frontend and integrate into experiment study app. This will be used to catch and handle errors from parameter validation. +<!-- SECTION:DESCRIPTION:END --> + +## Acceptance Criteria +<!-- AC:BEGIN --> +- [ ] #1 Error boundary component imported from frontend +- [ ] #2 Error boundary integrated into study app +- [ ] #3 Error boundary tested with intentional error +<!-- AC:END --> diff --git a/backlog/tasks/task-20 - Validate-factual-questions-extraction.md b/backlog/tasks/task-20 - Validate-factual-questions-extraction.md new file mode 100644 index 00000000..744e39d6 --- /dev/null +++ b/backlog/tasks/task-20 - Validate-factual-questions-extraction.md @@ -0,0 +1,24 @@ +--- +id: task-20 +title: Validate factual questions extraction +status: To Do +assignee: [] +created_date: '2026-01-15 17:07' +labels: + - analysis + - llm +dependencies: [] +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Review the auto-generated factual questions to ensure they make sense for each scenario. The current implementation may generate too many or irrelevant questions. +<!-- SECTION:DESCRIPTION:END --> + +## Acceptance Criteria +<!-- AC:BEGIN --> +- [ ] #1 Review generated questions for 5+ participants per scenario +- [ ] #2 Identify patterns in over/under-generation +- [ ] #3 Adjust prompt to improve relevance +<!-- AC:END --> diff --git a/backlog/tasks/task-21 - Add-inter-rater-reliability-for-LLM-analysis.md b/backlog/tasks/task-21 - Add-inter-rater-reliability-for-LLM-analysis.md new file mode 100644 index 00000000..f2d1846f --- /dev/null +++ b/backlog/tasks/task-21 - Add-inter-rater-reliability-for-LLM-analysis.md @@ -0,0 +1,24 @@ +--- +id: task-21 +title: Add inter-rater reliability for LLM analysis +status: To Do +assignee: [] +created_date: '2026-01-15 17:07' +labels: + - analysis + - validation +dependencies: [] +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Run analysis multiple times with temperature>0 or use multiple models to assess reliability of LLM ratings. This helps understand confidence in the scores. +<!-- SECTION:DESCRIPTION:END --> + +## Acceptance Criteria +<!-- AC:BEGIN --> +- [ ] #1 Run same analysis 3x on subset of participants +- [ ] #2 Calculate agreement metrics (Krippendorff alpha or similar) +- [ ] #3 Document reliability per metric +<!-- AC:END --> diff --git a/backlog/tasks/task-22 - Enhance-AI-influence-tracking-with-text-similarity.md b/backlog/tasks/task-22 - Enhance-AI-influence-tracking-with-text-similarity.md new file mode 100644 index 00000000..f0a46c0f --- /dev/null +++ b/backlog/tasks/task-22 - Enhance-AI-influence-tracking-with-text-similarity.md @@ -0,0 +1,24 @@ +--- +id: task-22 +title: Enhance AI influence tracking with text similarity +status: To Do +assignee: [] +created_date: '2026-01-15 17:07' +labels: + - analysis + - metrics +dependencies: [] +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Current AI influence analysis is purely LLM-based. Add quantitative text similarity metrics (e.g., n-gram overlap, embedding similarity) to complement LLM judgment. +<!-- SECTION:DESCRIPTION:END --> + +## Acceptance Criteria +<!-- AC:BEGIN --> +- [ ] #1 Add n-gram overlap calculation between suggestions and final text +- [ ] #2 Add embedding-based similarity score +- [ ] #3 Compare quantitative vs LLM-based influence scores +<!-- AC:END --> diff --git a/backlog/tasks/task-23 - Test-analysis-scripts-with-synthetic-log-data.md b/backlog/tasks/task-23 - Test-analysis-scripts-with-synthetic-log-data.md new file mode 100644 index 00000000..65e39258 --- /dev/null +++ b/backlog/tasks/task-23 - Test-analysis-scripts-with-synthetic-log-data.md @@ -0,0 +1,25 @@ +--- +id: task-23 +title: Test analysis scripts with synthetic log data +status: To Do +assignee: [] +created_date: '2026-01-15 17:08' +labels: + - testing + - analysis +dependencies: [] +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Since we don't have real logs yet, create synthetic test data to verify the extraction and analysis pipeline works end-to-end. +<!-- SECTION:DESCRIPTION:END --> + +## Acceptance Criteria +<!-- AC:BEGIN --> +- [ ] #1 Create sample JSONL log file with all event types +- [ ] #2 Run extract_experiment_data.py and verify output +- [ ] #3 Run llm_analysis.py on extracted data +- [ ] #4 Verify experiment_analysis.qmd renders correctly +<!-- AC:END --> diff --git a/backlog/tasks/task-3 - Add-parameter-validation-no-defaulting-condition-codes.md b/backlog/tasks/task-3 - Add-parameter-validation-no-defaulting-condition-codes.md new file mode 100644 index 00000000..5cec8cf8 --- /dev/null +++ b/backlog/tasks/task-3 - Add-parameter-validation-no-defaulting-condition-codes.md @@ -0,0 +1,25 @@ +--- +id: task-3 +title: Add parameter validation - no defaulting condition codes +status: To Do +assignee: [] +created_date: '2025-12-02 17:15' +labels: + - study + - validation + - error-handling +dependencies: [] +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Remove default condition codes ('n') in PostTaskSurvey.tsx:18 and TaskPage.tsx:16. Should throw errors for missing/invalid condition codes to catch bugs early. Requires error boundary setup first (task-2). +<!-- SECTION:DESCRIPTION:END --> + +## Acceptance Criteria +<!-- AC:BEGIN --> +- [ ] #1 Condition code validation added - throws on invalid/missing +- [ ] #2 Default fallback removed from both files +- [ ] #3 Error boundary catches and handles errors +<!-- AC:END --> diff --git a/backlog/tasks/task-4 - Evaluate-system-message-configuration-strategy.md b/backlog/tasks/task-4 - Evaluate-system-message-configuration-strategy.md new file mode 100644 index 00000000..62b21f5b --- /dev/null +++ b/backlog/tasks/task-4 - Evaluate-system-message-configuration-strategy.md @@ -0,0 +1,27 @@ +--- +id: task-4 +title: Evaluate system message configuration strategy +status: Done +assignee: [] +created_date: '2025-12-02 17:15' +updated_date: '2026-01-13 15:50' +labels: + - study + - configuration + - backend + - decision +dependencies: [] +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Decide whether to move system message from backend hardcoding to configuration. Options: 1) Keep hardcoded in backend, 2) Pass via studyConfig with each request, 3) Environment variable. Evaluate pro/cons of each approach considering: maintainability, flexibility, security. +<!-- SECTION:DESCRIPTION:END --> + +## Acceptance Criteria +<!-- AC:BEGIN --> +- [ ] #1 Current system message location documented +- [ ] #2 Configuration options evaluated with pros/cons +- [ ] #3 Recommendation documented +<!-- AC:END --> diff --git a/backlog/tasks/task-5 - Add-WritingArea-tests.md b/backlog/tasks/task-5 - Add-WritingArea-tests.md new file mode 100644 index 00000000..e7ef9764 --- /dev/null +++ b/backlog/tasks/task-5 - Add-WritingArea-tests.md @@ -0,0 +1,26 @@ +--- +id: task-5 +title: Add WritingArea tests +status: To Do +assignee: [] +created_date: '2025-12-02 17:16' +labels: + - study + - testing + - writingarea +dependencies: [] +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Add tests for WritingArea.tsx:36 cursor position tracking and text insertion/selection logic. Test document state updates and cursor position accuracy. +<!-- SECTION:DESCRIPTION:END --> + +## Acceptance Criteria +<!-- AC:BEGIN --> +- [ ] #1 Cursor position tracking tests written +- [ ] #2 Text insertion tests written +- [ ] #3 Selection handling tests written +- [ ] #4 All tests pass with >90% coverage +<!-- AC:END --> diff --git a/backlog/tasks/task-6 - Move-scenario-content-to-studyConfig.md b/backlog/tasks/task-6 - Move-scenario-content-to-studyConfig.md new file mode 100644 index 00000000..d91a30fb --- /dev/null +++ b/backlog/tasks/task-6 - Move-scenario-content-to-studyConfig.md @@ -0,0 +1,27 @@ +--- +id: task-6 +title: Move scenario content to studyConfig +status: To Do +assignee: [] +created_date: '2025-12-02 17:17' +labels: + - study + - configuration + - refactor + - scenario +dependencies: [] +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Extract email addresses from WritingArea.tsx and initial chat messages from ChatPanel.tsx to studyConfig. Makes scenarios easily configurable for different studies. Note: Consider system message location decision from task-4. +<!-- SECTION:DESCRIPTION:END --> + +## Acceptance Criteria +<!-- AC:BEGIN --> +- [ ] #1 Email scenario data extracted and centralized +- [ ] #2 Chat initial messages extracted and centralized +- [ ] #3 All scenario references updated to use studyConfig +- [ ] #4 Components render correctly with new configuration +<!-- AC:END --> diff --git a/backlog/tasks/task-8 - Enhance-AI-context-with-email-metadata.md b/backlog/tasks/task-8 - Enhance-AI-context-with-email-metadata.md new file mode 100644 index 00000000..c3d62db5 --- /dev/null +++ b/backlog/tasks/task-8 - Enhance-AI-context-with-email-metadata.md @@ -0,0 +1,26 @@ +--- +id: task-8 +title: Enhance AI context with email metadata +status: To Do +assignee: [] +created_date: '2025-12-02 17:18' +labels: + - study + - ai + - writingarea + - enhancement +dependencies: [] +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +Include email subject and recipient in AI request context (WritingArea.tsx:101). Improves AI awareness of full email composition and suggestion quality. +<!-- SECTION:DESCRIPTION:END --> + +## Acceptance Criteria +<!-- AC:BEGIN --> +- [ ] #1 Subject line extracted and included in context +- [ ] #2 Recipient email included in context +- [ ] #3 AI suggestions tested with full email metadata +<!-- AC:END --> diff --git a/backlog/tasks/task-9 - Move-chat-vs-AI-usage-expectations-to-pre-task.md b/backlog/tasks/task-9 - Move-chat-vs-AI-usage-expectations-to-pre-task.md new file mode 100644 index 00000000..f4351ecb --- /dev/null +++ b/backlog/tasks/task-9 - Move-chat-vs-AI-usage-expectations-to-pre-task.md @@ -0,0 +1,17 @@ +--- +id: task-9 +title: Move chat vs AI usage expectations to pre-task +status: To Do +assignee: [] +created_date: '2025-12-02 23:31' +labels: [] +dependencies: [] +--- + +## Description + +<!-- SECTION:DESCRIPTION:BEGIN --> +"The chat provides accurate information, while the AI-generated suggestions should be carefully evaluated." + +is currently in post-task survey but it should be pre-task instructions +<!-- SECTION:DESCRIPTION:END --> diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index e4c2eab6..1b72e7fa 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -14,6 +14,13 @@ services: volumes: - ./backend/logs:/app/backend/logs + experiment: + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - PORT=3000 + networks: default: driver: bridge diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index 8e435b8b..7b41c1d5 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -14,6 +14,15 @@ services: volumes: - /opt/thoughtful/logs:/app/backend/logs + experiment: + ports: + - "19572:3000" + environment: + - NODE_ENV=production + - PORT=3000 + volumes: + - /opt/thoughtful/experiment-logs:/app/logs + networks: default: driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index 42da18a8..ebdb3fca 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,16 @@ services: - LOG_SECRET=${LOG_SECRET} restart: unless-stopped + experiment: + build: + context: ./experiment + dockerfile: Dockerfile + args: + GIT_COMMIT: ${GIT_COMMIT:-unknown} + environment: + - OPENAI_API_KEY=${OPENAI_API_KEY} + restart: unless-stopped + networks: default: driver: bridge diff --git a/experiment/.dockerignore b/experiment/.dockerignore new file mode 100644 index 00000000..41326b1b --- /dev/null +++ b/experiment/.dockerignore @@ -0,0 +1,49 @@ +# dependencies +node_modules +npm-debug.log* +.pnp +.pnp.* + +# testing +coverage +__tests__ +*.test.ts +*.test.tsx +vitest.config.ts +vitest.setup.ts + +# next.js +.next +out + +# production +build + +# misc +.DS_Store +*.pem + +# debug +*.log + +# env files (will be provided by compose) +.env* +!.env.example + +# git +.git +.gitignore + +# IDE +.vscode +.idea + +# documentation +README.md +CLAUDE.md +backlog + +# experiment-specific +experiment +logs +*.jsonl diff --git a/experiment/.gitignore b/experiment/.gitignore new file mode 100644 index 00000000..7815fc41 --- /dev/null +++ b/experiment/.gitignore @@ -0,0 +1,46 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* +.env.local + +# Study logs +logs/ +*.jsonl + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/experiment/CLAUDE.md b/experiment/CLAUDE.md new file mode 100644 index 00000000..7f749b5b --- /dev/null +++ b/experiment/CLAUDE.md @@ -0,0 +1,164 @@ +# CLAUDE.md - Experiment App + +**IMPORTANT: You are working in `/experiment`. The code in `/backend` and `/frontend` is NOT relevant to this project.** + +This is a separate Next.js application for experimentation. It does not depend on or interact with the main writing-tools app. + +## Quick Facts + +- **Framework**: Next.js 15 (App Router) +- **Language**: TypeScript +- **Package Manager**: `npm` (NOT `uv`) +- **Styling**: Tailwind CSS +- **AI Integration**: Vercel AI SDK + OpenAI + +## Experiment Overview + +This is a "measuring thinking" experiment studying how writers use AI assistance and information-seeking behavior. + +### Core Research Question + +This experiment investigates **over-reliance on AI writing suggestions**. When people receive AI-generated text, they risk "premature closure"β€”accepting plausible-sounding output without thinking through what the situation actually requires. + +### Measurement Approach + +We measure over-reliance through two channels: +1. **Process**: What questions participants ask the colleague (reveals what they thought to think about) +2. **Outcome**: Whether the final email reflects genuine understanding (care for recipient, consequences considered) vs. superficially professional but problem-unaware text + +### The Colleague Chat as Measurement Instrument + +The colleague serves dual purposes: +- **Realistic information source**: Writers need details to compose a good email +- **Measurement instrument**: Questions asked reveal cognitive engagement + +**Critical constraint**: The colleague must NOT volunteer information proactively. If the LLM dumps all relevant facts without being asked, we can't measure what the participant thought to ask about. The colleague should be reactive, answering questions when asked but not anticipating needs. The system prompt includes "DON'T be proactive" for this reason. + +### Research Goals +1. **AI Writing Assistance**: Measure how participants use different types of AI suggestions (complete drafts, example sentences, analysis questions, etc.) +2. **Information-Seeking**: Measure whether participants ask questions to gather information needed for their task +3. **Company Reputation Awareness**: Measure whether participants consider how their writing reflects on the company + +### Task Scenarios +The experiment supports multiple configurable scenarios. Each scenario includes a unique colleague, recipient, and situation. Key design decisions: +- **Information gap**: The colleague's initial messages explain the problem but don't specify all details, encouraging participants to ask questions +- **Company framing**: Task instructions and colleague messages emphasize representing the company professionally +- **Follow-up nudge**: If participants don't engage with the chat, the colleague sends a brief follow-up after ~75 seconds (e.g., "still here if you need anything")β€”this is a conversation nudge, NOT an information dump + +**Available Scenarios:** +1. **Room Double-Booking** (`roomDoubleBooking`): Event coordinator Sarah Martinez asks you to email panelist Jaden Thompson about a scheduling conflict +2. **Demo Rescheduling** (`demoRescheduling`): Solutions Engineer Marcus Chen asks you to email client Dr. Lisa Patel about rescheduling a product demo due to a critical bug + +### Study Conditions +- `n` = no_ai (baseline - no AI suggestions) +- `c` = complete_document (AI suggests full email) +- `e` = example_sentences (AI gives example text) +- `a` = analysis_readerPerspective (AI asks reader perspective questions) +- `p` = proposal_advice (AI gives advice on next words) + +## Key File Locations + +### Study Flow (in order) +1. `components/study/ConsentPage.tsx` - Consent form +2. `components/study/IntroPage.tsx` - Study introduction +3. `components/study/IntroSurvey.tsx` - Pre-task survey +4. `components/study/StartTaskPage.tsx` - Task instructions (mentions chat, company framing) +5. `components/study/TaskPage.tsx` - Main writing task with chat + AI panels +6. `components/study/PostTaskSurvey.tsx` - Post-task survey +7. `components/study/FinalPage.tsx` - Completion page + +### Core Components +- `components/ChatPanel.tsx` - Chat with simulated colleague (persona varies by scenario) +- `components/WritingArea.tsx` - Email composition area +- `components/AIPanel.tsx` - AI writing suggestions (varies by condition) + +### Configuration +- `lib/studyConfig.ts` - Study page order, conditions, timing, **scenario definitions** +- `lib/messageTiming.ts` - Realistic chat timing calculations +- `lib/logging.ts` - Event logging utilities + +### API Routes +- `app/api/chat/route.ts` - Chat endpoint (GPT-5.2 with scenario-specific system prompt) +- `app/api/writing-support/route.ts` - AI writing suggestions +- `app/api/log/route.ts` - Event logging endpoint + +### Pages (IMPORTANT: Don't confuse these!) +- `app/page.tsx` - **Standalone demo** for AI writing assistance only (NO chat, NOT used in study) +- `components/study/TaskPage.tsx` - **Actual study task page** with collapsible chat + AI panel + +### Timing for the Simulated Colleague + +Realistic timing works as follows: The colleague finds a moment to read your message (~400-800ms), takes time to read and think through a response (depends on your message length), types an answer (depends on their response length), then sends it. The thinking/reading delay and typing duration both use the same calculation (40-80 chars/sec Β± 300ms variation) but applied to different message lengthsβ€”they think proportionally to what you wrote, and type proportionally to what they're typing. This creates natural pacing. + +### Adding New Scenarios + +To add a new scenario, edit `lib/studyConfig.ts` and add a new entry to the `SCENARIOS` object. Each scenario requires: +- **colleague**: name, firstName, role +- **recipient**: name, email +- **taskInstructions**: title, description, companyFraming +- **chat**: initialMessages, followUpMessage, systemPrompt + +Then pass the scenario ID via URL: `?scenario=yourScenarioId` + + +## Getting Started + +```bash +cd experiment +npm install +``` + +Create `.env.local`: +``` +OPENAI_API_KEY=sk-... +``` + +Run dev server: +```bash +npm run dev +``` + +Open http://localhost:3000 + +## Project Structure + +``` +experiment/ +β”œβ”€β”€ app/ +β”‚ β”œβ”€β”€ api/ # API routes (chat, writing-support) +β”‚ β”œβ”€β”€ layout.tsx # Root layout +β”‚ └── page.tsx # Main page +β”œβ”€β”€ components/ # React components +β”œβ”€β”€ contexts/ # Context providers +β”œβ”€β”€ lib/ # Utilities +β”œβ”€β”€ types/ # TypeScript types +└── package.json +``` + +## Testing + +```bash +npm run test # Run tests with vitest +``` + +## Common Commands + +```bash +npm run dev # Development server +npm run build # Build for production +npm run lint # Run ESLint +npm test # Run tests +``` + +## Linting & Formatting + +- **Import sorting** is handled automatically by Biome on save. Don't manually fix import sorting warningsβ€”they resolve automatically. + +## Key Files + +- **API Routes**: `app/api/` (chat, writing-support endpoints) +- **Demo Page**: `app/page.tsx` (standalone AI demo, NO chat) +- **Study Task Page**: `components/study/TaskPage.tsx` (the actual study with chat) +- **Components**: `components/` folder + +See `README.md` for more details on features and API documentation. diff --git a/experiment/Dockerfile b/experiment/Dockerfile new file mode 100644 index 00000000..3c1104ba --- /dev/null +++ b/experiment/Dockerfile @@ -0,0 +1,69 @@ +# Stage 1: Install dependencies +FROM node:24-slim AS deps +WORKDIR /app + +# Copy package files +COPY package.json package-lock.json ./ + +# Install all dependencies (including devDependencies for build) +RUN npm ci + +# Stage 2: Build the application +FROM node:24-slim AS builder +WORKDIR /app + +# Copy dependencies from deps stage +COPY --from=deps /app/node_modules ./node_modules + +# Copy application code +COPY . . + +# Build argument for git commit hash +ARG GIT_COMMIT=unknown + +# Set environment variables for build +ENV NEXT_PUBLIC_GIT_COMMIT=${GIT_COMMIT} +ENV DOCKER_BUILD=true + +# Build the Next.js application +RUN npm run build + +# Stage 3: Production runtime +FROM node:24-slim AS runner +# Note: unlike the backend Dockerfile, this container doesn't set WORKDIR to a subfolder of /app +WORKDIR /app + +# Set production environment +ENV NODE_ENV=production + +# Build argument for group ID (to match host permissions) +ARG EXP_LOGS_GID=1001 + +# Create non-root user for security +RUN addgroup --system --gid ${EXP_LOGS_GID} appgroup \ + && adduser --system --uid 1001 --ingroup appgroup nextjs + +# Copy standalone output from builder +COPY --from=builder /app/.next/standalone ./ +# Copy static files (Next.js doesn't include these in standalone by default) +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/public ./public + +# Set correct permissions +RUN chown -R nextjs:appgroup /app + +# Switch to non-root user +USER nextjs + +# Expose the port +EXPOSE 3000 + +# Set default port +ENV PORT=3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})" + +# Start the application +CMD ["node", "server.js"] diff --git a/experiment/README.md b/experiment/README.md new file mode 100644 index 00000000..6448f0c7 --- /dev/null +++ b/experiment/README.md @@ -0,0 +1,126 @@ +# Controlled Open-Ended Writing Task + +A Next.js application that simulates a communicative writing task with AI-powered chat support. + +## Features + +- **Email Writing Interface**: Compose emails with a realistic email editor +- **AI Chat Assistant**: Chat with an AI-powered colleague who provides context for your writing task +- **Writing Support API**: AI writing assistance of various kinds +- **Streaming Responses**: Real-time streaming chat responses using Vercel AI SDK +- **Configurable Scenarios**: Multiple writing scenarios (room double-booking, demo rescheduling, etc.) + +## Tech Stack + +- **Framework**: Next.js 15 (App Router) +- **Language**: TypeScript +- **Styling**: Tailwind CSS +- **AI Integration**: Vercel AI SDK with OpenAI + +## Getting Started + +### Prerequisites + +- Node.js 18+ installed +- OpenAI API key + +### Installation + +1. Navigate to the project directory: + +```bash +cd experiment +``` + +2. Install dependencies: + +```bash +npm install +``` + +3. Create a `.env.local` file and add your OpenAI API key: + +```bash +cp .env.example .env.local +``` + +Then edit `.env.local` and add your API key: + +``` +OPENAI_API_KEY=sk-... +``` + +4. Run the development server: + +```bash +npm run dev +``` + +5. Open [http://localhost:3000](http://localhost:3000) in your browser + +## Project Structure + +``` +writing-task-app/ +β”œβ”€β”€ app/ +β”‚ β”œβ”€β”€ api/ +β”‚ β”‚ β”œβ”€β”€ chat/route.ts # Streaming chat endpoint +β”‚ β”‚ └── writing-support/route.ts # AI writing assistance endpoint +β”‚ β”œβ”€β”€ globals.css # Global styles with animations +β”‚ β”œβ”€β”€ layout.tsx # Root layout +β”‚ └── page.tsx # Main application page +β”œβ”€β”€ components/ +β”‚ β”œβ”€β”€ AIPanel.tsx # AI writing assistant placeholder +β”‚ β”œβ”€β”€ ChatPanel.tsx # Chat interface component +β”‚ └── WritingArea.tsx # Email editor component +└── types/ + └── index.ts # TypeScript type definitions +``` + +## API Routes + +### POST /api/chat + +Streaming chat endpoint that simulates a conversation with an AI colleague. The colleague's persona and context vary by scenario. + +**Request:** +```json +{ + "messages": [ + { "role": "user", "content": "What room is available?" } + ], + "scenario": "roomDoubleBooking" +} +``` + +**Response:** Server-Sent Events stream with AI responses + +### POST /api/writing-support + +AI writing assistance endpoint. + +**Request:** +```json +{ + "editorState": { + "beforeCursor": "Dear [Recipient],\n\nI hope this email finds you well. ", + "selectedText": "", + "afterCursor": "" + } +} +``` + +**Response:** +```json +{ + "suggestions": [ + "Consider adding a greeting or asking about their availability.", + "You might want to mention the purpose of the meeting." + ] +} +``` + +## Development + +- The chat interface uses the `useChat` hook from `@ai-sdk/react` +- Messages are displayed with typing indicators and read receipts diff --git a/experiment/__tests__/components/ChatPanel.test.tsx b/experiment/__tests__/components/ChatPanel.test.tsx new file mode 100644 index 00000000..2f2063e4 --- /dev/null +++ b/experiment/__tests__/components/ChatPanel.test.tsx @@ -0,0 +1,1121 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { waitFor, fireEvent, screen, act } from '@testing-library/react'; +import ChatPanel from '@/components/ChatPanel'; +import { studyParamsAtom } from '@/contexts/StudyContext'; +import * as logging from '@/lib/logging'; +import { renderWithJotai } from '../utils/test-utils'; +import { createUserMessage, createAssistantMessage, createMockChatHelpers } from '../utils/mock-factories'; + +// Mock the logging module +vi.mock('@/lib/logging', () => ({ + log: vi.fn().mockResolvedValue(undefined), +})); + +// Mock useChat hook +vi.mock('@ai-sdk/react', () => ({ + useChat: vi.fn(() => createMockChatHelpers()), +})); + +// Mock timing functions for predictable delays +vi.mock('@/lib/messageTiming', () => ({ + calculateThinkingDelay: vi.fn(() => 100), + calculateTypingDuration: vi.fn(() => 100), + calculateInterMessageDelay: vi.fn(() => 100), +})); + +describe('ChatPanel - Message Logging', () => { + const mockLog = vi.mocked(logging.log); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + // Test 1: User Message Logging (via form submission) + it('should log user messages with correct event type and payload structure', async () => { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + mockUseChat.mockReturnValue(createMockChatHelpers({ + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + mockLog.mockClear(); + + renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + // Wait for initialization + await waitFor(() => { + expect(mockSetMessages).toHaveBeenCalled(); + }); + mockLog.mockClear(); + + // Type and submit a message + const input = screen.getByPlaceholderText(/Message Sarah/i); + fireEvent.change(input, { target: { value: 'Hello Sarah' } }); + fireEvent.submit(input.closest('form')!); + + // Check log was called with correct structure + await waitFor(() => { + expect(mockLog).toHaveBeenCalledWith({ + username: 'test-user', + event: 'chatMessage:user', + extra_data: { + messageId: expect.stringMatching(/^user-\d+$/), + content: 'Hello Sarah', + timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/), + }, + }); + }); + }); + + // Test 2: Assistant Message Logging (via typing animation) + it('should log assistant messages with correct event type and partIndex', async () => { + vi.useFakeTimers(); + try { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + + // Start with initialized state (empty user + assistant message) + const emptyUserMessage = createUserMessage('', 'initial-user-message'); + const assistantMessage = createAssistantMessage('How can I help?', 'assistant-msg-1'); + mockUseChat.mockReturnValue(createMockChatHelpers({ + messages: [emptyUserMessage, assistantMessage], + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + mockLog.mockClear(); + + // Advance timers past the typing duration (100ms mocked) to reveal the message + await act(async () => { + await vi.advanceTimersByTimeAsync(200); + }); + + expect(mockLog).toHaveBeenCalledWith( + expect.objectContaining({ + event: 'chatMessage:assistant', + extra_data: expect.objectContaining({ + messageId: 'assistant-msg-1', + partIndex: 0, + content: 'How can I help?', + }), + }) + ); + } finally { + vi.useRealTimers(); + } + }); + + // Test 3: User messages are only logged on form submission, not via effects (regression test) + it('should not duplicate user message logs on re-render', async () => { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + mockUseChat.mockReturnValue(createMockChatHelpers({ + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + const { rerender } = renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + await waitFor(() => { + expect(mockSetMessages).toHaveBeenCalled(); + }); + mockLog.mockClear(); + + // Submit a message via form + const input = screen.getByPlaceholderText(/Message Sarah/i); + fireEvent.change(input, { target: { value: 'Test message' } }); + fireEvent.submit(input.closest('form')!); + + await waitFor(() => { + expect(mockLog).toHaveBeenCalledTimes(1); + }); + + // Re-render should not log again (user messages are event-driven) + mockLog.mockClear(); + rerender(<ChatPanel />); + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(mockLog).toHaveBeenCalledTimes(0); + }); + + // Test 4: Multiple user messages via form submission + it('should log multiple messages in sequence', async () => { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + mockUseChat.mockReturnValue(createMockChatHelpers({ + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + await waitFor(() => { + expect(mockSetMessages).toHaveBeenCalled(); + }); + mockLog.mockClear(); + + const input = screen.getByPlaceholderText(/Message Sarah/i); + + // Send first message + fireEvent.change(input, { target: { value: 'First' } }); + fireEvent.submit(input.closest('form')!); + + // Send second message + fireEvent.change(input, { target: { value: 'Second' } }); + fireEvent.submit(input.closest('form')!); + + await waitFor(() => { + expect(mockLog).toHaveBeenCalledTimes(2); + }); + + expect(mockLog).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + event: 'chatMessage:user', + extra_data: expect.objectContaining({ content: 'First' }), + }) + ); + + expect(mockLog).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + event: 'chatMessage:user', + extra_data: expect.objectContaining({ content: 'Second' }), + }) + ); + }); + + // Test 5: User message content is logged exactly as submitted + it('should correctly extract text from message parts', async () => { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + mockUseChat.mockReturnValue(createMockChatHelpers({ + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + await waitFor(() => { + expect(mockSetMessages).toHaveBeenCalled(); + }); + mockLog.mockClear(); + + const input = screen.getByPlaceholderText(/Message Sarah/i); + fireEvent.change(input, { target: { value: 'Part 1 Part 2 Part 3' } }); + fireEvent.submit(input.closest('form')!); + + await waitFor(() => { + expect(mockLog).toHaveBeenCalledWith( + expect.objectContaining({ + extra_data: expect.objectContaining({ + content: 'Part 1 Part 2 Part 3', + }), + }) + ); + }); + }); + + // Test 6: Empty messages are not submitted (guard in onSubmit) + it('should handle messages with empty or missing text', async () => { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + mockUseChat.mockReturnValue(createMockChatHelpers({ + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + await waitFor(() => { + expect(mockSetMessages).toHaveBeenCalled(); + }); + mockLog.mockClear(); + + const input = screen.getByPlaceholderText(/Message Sarah/i); + // Try to submit empty message + fireEvent.change(input, { target: { value: '' } }); + fireEvent.submit(input.closest('form')!); + + // Should not log anything for empty message + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(mockLog).not.toHaveBeenCalled(); + }); + + // Test 7: Username from Atom + it('should use username from studyParamsAtom', async () => { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + mockUseChat.mockReturnValue(createMockChatHelpers({ + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'custom-username-123', + condition: 'c', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + await waitFor(() => { + expect(mockSetMessages).toHaveBeenCalled(); + }); + mockLog.mockClear(); + + const input = screen.getByPlaceholderText(/Message Sarah/i); + fireEvent.change(input, { target: { value: 'Test message' } }); + fireEvent.submit(input.closest('form')!); + + await waitFor(() => { + expect(mockLog).toHaveBeenCalledWith( + expect.objectContaining({ + username: 'custom-username-123', + }) + ); + }); + }); + + // Test 9: Timestamp Format + it('should use ISO 8601 timestamp format', async () => { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + mockUseChat.mockReturnValue(createMockChatHelpers({ + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + await waitFor(() => { + expect(mockSetMessages).toHaveBeenCalled(); + }); + mockLog.mockClear(); + + const input = screen.getByPlaceholderText(/Message Sarah/i); + fireEvent.change(input, { target: { value: 'Test' } }); + fireEvent.submit(input.closest('form')!); + + await waitFor(() => { + expect(mockLog).toHaveBeenCalled(); + }); + + const logCall = mockLog.mock.calls[0][0]; + const timestamp = logCall.extra_data?.timestamp as string; + + expect(timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + + const date = new Date(timestamp); + expect(date.toString()).not.toBe('Invalid Date'); + + const now = Date.now(); + const timestampMs = date.getTime(); + expect(now - timestampMs).toBeLessThan(1000); + }); + + // Test 10: Assistant messages are logged only when displayed, not before + it('should log new messages when added incrementally', async () => { + vi.useFakeTimers(); + try { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + + // Start with initialized state (empty user + assistant message with 2 parts) + const emptyUserMessage = createUserMessage('', 'initial-user-message'); + const msg1 = createAssistantMessage( + JSON.stringify(['First part', 'Second part']), + 'msg-1' + ); + mockUseChat.mockReturnValue(createMockChatHelpers({ + messages: [emptyUserMessage, msg1], + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + mockLog.mockClear(); + + // BEFORE any time passes: no messages should be logged yet + expect(mockLog).not.toHaveBeenCalled(); + + // Advance timers to reveal first part only (100ms typing duration) + await act(async () => { + await vi.advanceTimersByTimeAsync(150); + }); + + // First message should now be logged + expect(mockLog).toHaveBeenCalledTimes(1); + expect(mockLog).toHaveBeenCalledWith( + expect.objectContaining({ + event: 'chatMessage:assistant', + extra_data: expect.objectContaining({ + messageId: 'msg-1', + partIndex: 0, + content: 'First part', + }), + }) + ); + + mockLog.mockClear(); + + // Advance timers to reveal second part (inter-message delay 100ms + typing 100ms) + await act(async () => { + await vi.advanceTimersByTimeAsync(250); + }); + + // Second message should now be logged + expect(mockLog).toHaveBeenCalledTimes(1); + expect(mockLog).toHaveBeenCalledWith( + expect.objectContaining({ + event: 'chatMessage:assistant', + extra_data: expect.objectContaining({ + messageId: 'msg-1', + partIndex: 1, + content: 'Second part', + }), + }) + ); + } finally { + vi.useRealTimers(); + } + }); + + // Test 11: Regression - first part of new assistant message should not be logged with stale visibleMessagePartCount + it('should not log assistant message parts before they are visible (race condition)', async () => { + vi.useFakeTimers(); + try { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + + // Start with initialized state - initial messages already visible + const emptyUserMessage = createUserMessage('', 'initial-user-message'); + const initialAssistantMessage = createAssistantMessage( + JSON.stringify(['Hello!', 'How can I help?']), + 'initial-assistant' + ); + mockUseChat.mockReturnValue(createMockChatHelpers({ + messages: [emptyUserMessage, initialAssistantMessage], + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + const { rerender } = renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + // Advance timers to reveal all initial message parts + await act(async () => { + await vi.advanceTimersByTimeAsync(500); + }); + + mockLog.mockClear(); + + // Now simulate user sending a message and receiving a NEW assistant response + // The key bug we're testing: visibleMessagePartCount was high from the previous message + // but now with the fix, visibleMessageId prevents logging until parts are actually revealed + const userMessage = createUserMessage('What about X?', 'user-msg-1'); + const newAssistantMessage = createAssistantMessage( + JSON.stringify(['New response part 1', 'New response part 2']), + 'new-assistant' + ); + mockUseChat.mockReturnValue(createMockChatHelpers({ + messages: [emptyUserMessage, initialAssistantMessage, userMessage, newAssistantMessage], + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + // Force re-render with new messages (simulating the async response arriving) + await act(async () => { + rerender(<ChatPanel />); + }); + + // At this point, NO parts of the new message should be logged yet + // because visibleMessageId hasn't been set for the new message + const prematureCalls = mockLog.mock.calls.filter( + (call) => call[0]?.event === 'chatMessage:assistant' && call[0]?.extra_data?.messageId === 'new-assistant' + ); + expect(prematureCalls).toHaveLength(0); + + mockLog.mockClear(); + + // Advance timers to reveal first part of NEW message + // With mocked timing (100ms each) and busyLag (1200ms): + // - readingDelay = 100 + 1200 = 1300ms + // - part 1 at: 1300 + 100 = 1400ms + // - part 2 at: 1400 + 100 + 100 = 1600ms + // So advance 1500ms to reveal only part 1 + await act(async () => { + await vi.advanceTimersByTimeAsync(1500); + }); + + // The first part should now be logged with actual content + const assistantCalls = mockLog.mock.calls.filter( + (call) => call[0]?.event === 'chatMessage:assistant' && call[0]?.extra_data?.messageId === 'new-assistant' + ); + + expect(assistantCalls.length).toBeGreaterThan(0); + const firstPartCall = assistantCalls.find((call) => call[0]?.extra_data?.partIndex === 0); + expect(firstPartCall).toBeDefined(); + expect(firstPartCall?.[0]?.extra_data?.content).toBe('New response part 1'); + + mockLog.mockClear(); + + // Advance 200ms more to reveal second part (need to cross 1600ms threshold) + await act(async () => { + await vi.advanceTimersByTimeAsync(200); + }); + + const secondPartCalls = mockLog.mock.calls.filter( + (call) => call[0]?.event === 'chatMessage:assistant' && call[0]?.extra_data?.messageId === 'new-assistant' + ); + + const part1Call = secondPartCalls.find((call) => call[0]?.extra_data?.partIndex === 1); + expect(part1Call).toBeDefined(); + expect(part1Call?.[0]?.extra_data?.content).toBe('New response part 2'); + } finally { + vi.useRealTimers(); + } + }); + + // Test 12: System Messages + it('should not log system messages', async () => { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const systemMessage = { + id: 'system-msg', + role: 'system' as const, + parts: [{ type: 'text' as const, text: 'System message' }], + }; + + mockUseChat.mockReturnValue(createMockChatHelpers({ + messages: [systemMessage], + })); + + renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(mockLog).not.toHaveBeenCalled(); + }); + + // Test 13: Initial Messages on Mount + it('should initialize with pre-filled assistant messages when component mounts', async () => { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + mockUseChat.mockReturnValue(createMockChatHelpers({ + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + // Should initialize messages with an empty user message and initial assistant messages + await waitFor(() => { + expect(mockSetMessages).toHaveBeenCalled(); + }); + + const setMessagesCall = mockSetMessages.mock.calls[0][0]; + expect(setMessagesCall).toHaveLength(2); + expect(setMessagesCall[0].role).toBe('user'); + expect(setMessagesCall[1].role).toBe('assistant'); + }); + + // Test 14: Message Sequencing with Multiple Parts + it('should log individual message parts as they become visible', async () => { + vi.useFakeTimers(); + try { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + + // Start with initialized state + const emptyUserMessage = createUserMessage('', 'initial-user-message'); + const msg = createAssistantMessage( + JSON.stringify(['First message', 'Second message']), + 'multi-msg' + ); + mockUseChat.mockReturnValue(createMockChatHelpers({ + messages: [emptyUserMessage, msg], + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + mockLog.mockClear(); + + // Advance timers to reveal first part (100ms typing duration) + await act(async () => { + await vi.advanceTimersByTimeAsync(200); + }); + + // First part should be logged with partIndex: 0 + expect(mockLog).toHaveBeenCalledWith( + expect.objectContaining({ + event: 'chatMessage:assistant', + extra_data: expect.objectContaining({ + messageId: 'multi-msg', + partIndex: 0, + content: 'First message', + }), + }) + ); + + mockLog.mockClear(); + + // Advance timers more to reveal second part (inter-delay + typing = 200ms) + await act(async () => { + await vi.advanceTimersByTimeAsync(300); + }); + + // Second part should now be logged + expect(mockLog).toHaveBeenCalledWith( + expect.objectContaining({ + event: 'chatMessage:assistant', + extra_data: expect.objectContaining({ + messageId: 'multi-msg', + partIndex: 1, + content: 'Second message', + }), + }) + ); + } finally { + vi.useRealTimers(); + } + }); + + // Test 15: Typing Indicator Display + it('should display typing indicator during message sequencing', async () => { + vi.useFakeTimers(); + try { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + + // Start with initialized state + const emptyUserMessage = createUserMessage('', 'initial-user-message'); + const msg = createAssistantMessage( + JSON.stringify(['First message part', 'Second message part']), + 'typing-test-msg' + ); + mockUseChat.mockReturnValue(createMockChatHelpers({ + messages: [emptyUserMessage, msg], + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + const { container } = renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + // Advance timers to reveal first part + await act(async () => { + await vi.advanceTimersByTimeAsync(200); + }); + + // Check that first message part is visible + const messages = container.querySelectorAll('.mb-3.text-sm.leading-snug'); + expect(messages.length).toBeGreaterThan(0); + } finally { + vi.useRealTimers(); + } + }); + + // Test 16: Notification Badge Timing + it('should show notification badge for 5 seconds when message appears', async () => { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + mockUseChat.mockReturnValue(createMockChatHelpers({ + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + const { rerender, container } = renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + await waitFor(() => { + expect(mockSetMessages).toHaveBeenCalled(); + }); + mockSetMessages.mockClear(); + mockSendMessage.mockClear(); + + // Add assistant message + const msg = createAssistantMessage('Test message', 'notif-test-msg'); + mockUseChat.mockReturnValue(createMockChatHelpers({ + messages: [msg], + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + rerender(<ChatPanel />); + + // Notification badge should appear immediately when message renders + await new Promise((resolve) => setTimeout(resolve, 50)); + + // Note: The notification badge is controlled by state, not directly visible in DOM + // This test verifies the component renders without errors when messages appear + expect(container.querySelector('.bg-white')).toBeInTheDocument(); + + // Wait to ensure no runtime errors during 5 second notification window + await new Promise((resolve) => setTimeout(resolve, 100)); + }); + + // Test 17: Scroll to bottom when displayed messages change + it('should scroll to bottom when displayed messages change', async () => { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + mockUseChat.mockReturnValue(createMockChatHelpers({ + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + const { rerender } = renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + await waitFor(() => { + expect(mockSetMessages).toHaveBeenCalled(); + }); + mockSetMessages.mockClear(); + + // Add a new message + const msg = createUserMessage('New message', 'scroll-test-msg'); + mockUseChat.mockReturnValue(createMockChatHelpers({ + messages: [msg], + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + // Mock scrollIntoView to verify it's called + const mockScrollIntoView = vi.fn(); + Element.prototype.scrollIntoView = mockScrollIntoView; + + rerender(<ChatPanel />); + + // scrollIntoView should be called when displayed messages change + await waitFor(() => { + expect(mockScrollIntoView).toHaveBeenCalled(); + }); + }); + + // Test 18: Display limited message parts when streaming + it('should hide last assistant message while streaming', async () => { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + const msg = createAssistantMessage('Streaming response', 'stream-msg'); + + mockUseChat.mockReturnValue(createMockChatHelpers({ + messages: [msg], + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + status: 'streaming', + })); + + const { container } = renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // While streaming, the last message should be hidden + const messageDivs = container.querySelectorAll('.mb-3.text-sm.leading-snug'); + expect(messageDivs.length).toBe(0); + }); + + // Test 19: Show last message when not streaming (after typing animation) + it('should show last assistant message when not streaming', async () => { + vi.useFakeTimers(); + try { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + const emptyUserMessage = createUserMessage('', 'initial-user-message'); + const msg = createAssistantMessage('Complete response', 'complete-msg'); + + mockUseChat.mockReturnValue(createMockChatHelpers({ + messages: [emptyUserMessage, msg], + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + const { container } = renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + // Advance timers to reveal message (typing animation) + await act(async () => { + await vi.advanceTimersByTimeAsync(200); + }); + + const messageDivs = container.querySelectorAll('.mb-3.text-sm.leading-snug'); + expect(messageDivs.length).toBeGreaterThan(0); + } finally { + vi.useRealTimers(); + } + }); + + // Test 20: Display only visible message parts (controlled by typing animation) + it('should respect visibleMessagePartCount for last assistant message', async () => { + vi.useFakeTimers(); + try { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + const emptyUserMessage = createUserMessage('', 'initial-user-message'); + const msg = createAssistantMessage( + JSON.stringify(['Part 1', 'Part 2', 'Part 3']), + 'multi-part-msg' + ); + + mockUseChat.mockReturnValue(createMockChatHelpers({ + messages: [emptyUserMessage, msg], + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + const { container } = renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + // Initially no assistant parts visible (need to wait for typing animation) + let messages = container.querySelectorAll('.mb-3.text-sm.leading-snug'); + expect(messages.length).toBe(0); + + // Advance timers to reveal first part + await act(async () => { + await vi.advanceTimersByTimeAsync(200); + }); + + messages = container.querySelectorAll('.mb-3.text-sm.leading-snug'); + expect(messages.length).toBe(1); + + // Advance timers more to reveal second part + await act(async () => { + await vi.advanceTimersByTimeAsync(200); + }); + + messages = container.querySelectorAll('.mb-3.text-sm.leading-snug'); + expect(messages.length).toBe(2); + } finally { + vi.useRealTimers(); + } + }); + + // Test 21: Scroll triggers on visibleMessagePartCount change + it('should scroll when visibleMessagePartCount changes', async () => { + const { useChat } = await import('@ai-sdk/react'); + const mockUseChat = vi.mocked(useChat); + + const mockSendMessage = vi.fn(); + const mockSetMessages = vi.fn(); + mockUseChat.mockReturnValue(createMockChatHelpers({ + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + const { rerender } = renderWithJotai(<ChatPanel />, { + initialValues: [ + [ + studyParamsAtom, + { + username: 'test-user', + condition: 'n', + page: 'task', + autoRefreshInterval: 15000, + }, + ], + ], + }); + + await waitFor(() => { + expect(mockSetMessages).toHaveBeenCalled(); + }); + mockSetMessages.mockClear(); + + // Add message with multiple parts + const msg = createAssistantMessage( + JSON.stringify(['Part 1', 'Part 2']), + 'scroll-trigger-msg' + ); + mockUseChat.mockReturnValue(createMockChatHelpers({ + messages: [msg], + sendMessage: mockSendMessage, + setMessages: mockSetMessages, + })); + + const mockScrollIntoView = vi.fn(); + Element.prototype.scrollIntoView = mockScrollIntoView; + + rerender(<ChatPanel />); + + // scrollIntoView should be called when message parts become visible + await waitFor(() => { + expect(mockScrollIntoView).toHaveBeenCalled(); + }); + }); +}); diff --git a/experiment/__tests__/utils/mock-factories.ts b/experiment/__tests__/utils/mock-factories.ts new file mode 100644 index 00000000..51a6388f --- /dev/null +++ b/experiment/__tests__/utils/mock-factories.ts @@ -0,0 +1,41 @@ +import type { UIMessage, UseChatHelpers } from '@ai-sdk/react'; +import { vi } from 'vitest'; + +type MockChatHelpers = Partial<UseChatHelpers<UIMessage>>; + +export function createMockChatHelpers(overrides: MockChatHelpers = {}) { + return { + id: 'test-chat', + messages: [] as UIMessage[], + sendMessage: vi.fn(), + setMessages: vi.fn(), + status: 'ready' as const, + error: undefined, + ...overrides, + } as unknown as UseChatHelpers<UIMessage>; +} + +export function createMockMessage(overrides: Partial<UIMessage> = {}): UIMessage { + return { + id: Math.random().toString(36).substring(7), + role: 'user', + parts: [{ type: 'text', text: 'Test message' }], + ...overrides, + }; +} + +export function createUserMessage(text: string, id?: string): UIMessage { + return createMockMessage({ + id: id || Math.random().toString(36).substring(7), + role: 'user', + parts: [{ type: 'text', text }], + }); +} + +export function createAssistantMessage(text: string, id?: string): UIMessage { + return createMockMessage({ + id: id || Math.random().toString(36).substring(7), + role: 'assistant', + parts: [{ type: 'text', text }], + }); +} diff --git a/experiment/__tests__/utils/test-utils.tsx b/experiment/__tests__/utils/test-utils.tsx new file mode 100644 index 00000000..ce3ee844 --- /dev/null +++ b/experiment/__tests__/utils/test-utils.tsx @@ -0,0 +1,34 @@ +import { render } from '@testing-library/react'; +import { createStore, Provider } from 'jotai'; +import type { ReactElement } from 'react'; + +import type { RenderOptions } from '@testing-library/react'; + +/** + * Custom render that wraps component with Jotai Provider + * Allows setting initial atom values for testing + */ +export function renderWithJotai( + ui: ReactElement, + options?: RenderOptions & { + initialValues?: Array<[unknown, unknown]>; + } +) { + const { initialValues = [], ...renderOptions } = options || {}; + + // Create a fresh store for each test + const store = createStore(); + + // Set initial values on the store + initialValues.forEach(([atom, value]: [unknown, unknown]) => { + store.set(atom as Parameters<typeof store.set>[0], value); + }); + + return render( + <Provider store={store}>{ui}</Provider>, + renderOptions + ); +} + +// Re-export everything from testing-library +export * from '@testing-library/react'; diff --git a/experiment/app/api/chat/route.ts b/experiment/app/api/chat/route.ts new file mode 100644 index 00000000..0544bc61 --- /dev/null +++ b/experiment/app/api/chat/route.ts @@ -0,0 +1,19 @@ +import { openai } from '@ai-sdk/openai'; +import { convertToModelMessages, streamText } from 'ai'; +import { getScenario } from '@/lib/studyConfig'; + +export const runtime = 'edge'; + +export async function POST(req: Request) { + const { messages, scenario: scenarioId } = await req.json(); + const scenario = getScenario(scenarioId); + + const result = streamText({ + model: openai('gpt-5.2'), + system: scenario.chat.systemPrompt, + messages: convertToModelMessages(messages), + maxOutputTokens: 300, + }); + + return result.toUIMessageStreamResponse(); +} diff --git a/experiment/app/api/log/route.ts b/experiment/app/api/log/route.ts new file mode 100644 index 00000000..a05ec548 --- /dev/null +++ b/experiment/app/api/log/route.ts @@ -0,0 +1,63 @@ +import type { LogEntry } from '@/types/study'; +import { appendFile, mkdir, realpath } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +const LOGS_DIR = resolve(process.cwd(), 'logs'); + +/** + * Validate username format + */ +function isValidUsername(username: string): boolean { + return /^[a-zA-Z0-9\-_]+$/.test(username) && username.length > 0; +} + +/** + * POST /api/log - Log an event to a JSONL file + */ +export async function POST(request: Request) { + try { + const body = await request.json(); + const entry = body as LogEntry; + const username = entry.username; + + // Validate username format + if (!isValidUsername(username)) { + return Response.json( + { error: 'Invalid username format' }, + { status: 400 } + ); + } + + // Create logs directory if it doesn't exist and get its real path + await mkdir(LOGS_DIR, { recursive: true }); + const realLogsDir = await realpath(LOGS_DIR); + + // Construct log file path using validated username + const logFilePath = resolve(realLogsDir, `${username}.jsonl`); + + // Verify the resolved path is within the logs directory (prevent directory traversal) + if (!logFilePath.startsWith(`${realLogsDir}/`)) { + return Response.json( + { error: 'Invalid log file path' }, + { status: 400 } + ); + } + + // Append entry to participant's log file as JSONL + const logLine = JSON.stringify(entry) + '\n'; + + await appendFile(logFilePath, logLine, 'utf-8'); + + return Response.json( + { success: true, message: 'Log entry written' }, + { status: 200 } + ); + } catch (error) { + console.error('Logging error:', error); + + return Response.json( + { success: false, message: 'Internal error logging' }, + { status: 500 } + ); + } +} diff --git a/experiment/app/api/writing-support/route.ts b/experiment/app/api/writing-support/route.ts new file mode 100644 index 00000000..b1a3557c --- /dev/null +++ b/experiment/app/api/writing-support/route.ts @@ -0,0 +1,108 @@ +import { openai } from '@ai-sdk/openai'; +import { generateObject } from 'ai'; +import { NextResponse } from 'next/server'; +import { z } from 'zod'; +import type { WritingSupportRequest } from '@/types'; + +export const runtime = 'edge'; + +const prompts = { + example_sentences: `You are assisting a writer in drafting a document. Generate three possible options for inspiring and fresh possible next sentences that would help the writer think about what they should write next. + +Guidelines: +- Focus on the area of the document that is closest to the writer's cursor. +- If the writer is in the middle of a sentence, output three possible continuations of that sentence. +- If the writer is at the end of a paragraph, output three possible sentences that would start the next paragraph. +- The three sentences should be three different paths that the writer could take, each starting from the current point in the document; they do **NOT** go in sequence. +- Each output should be *at most one sentence* long. +- Use ellipses to truncate sentences that are longer than about **10 words**.`, + + complete_document: `You are assisting a writer complete and polish their document. Please provide a completed and polished version of the document that the writer has started writing. + +Guidelines: +- Use the text in the document as a starting point, but make any changes needed to make the document complete and polished. +- Maintain the writer's tone, style, and voice throughout. +- Polish the text for clarity and coherence.`, + + proposal_advice: `You are assisting a writer in drafting a document by providing three directive (but not prescriptive) advice to help them develop their work. Your advice must be tailored to the document's genre. Use your best judgment to offer the most relevant and helpful advice, drawing from the following types of support as appropriate for the context: +- Support the writer in adhering to their stated writing goals or assignment guidelines. +- Help the writer think about what they could write next. +- Encourage the writer to maintain focus on their main idea and avoid introducing unrelated material. +- Recommend strengthening arguments by adding supporting evidence, specific examples, or clear reasoning. +- Advise on structuring material to achieve a clear and logical flow. +- Guide the writer in choosing language that is accessible and engaging for the intended audience. + +Guidelines: +- Focus on the area of the document that is closest to the writer's cursor. +- Keep each piece of advice under 20 words. +- Express the advice in the form of a directive instruction, not a question. +- Avoid providing specific words or phrases that the writer could directly copy into their document. +- Make each piece of advice very specific to the current document, not general advice that could apply to any document.`, + + analysis_readerPerspective: `You are assisting a writer in drafting a document for a specific person. Generate three possible reactions (questions, feelings, perspectives, etc.) the person might have about the document. + +Guidelines: +- Limit each perspective to under 20 words. +- Ensure all perspectives specifically reflect details or qualities from the current document, avoiding broad or generic statements. +- The three perspectives should be diverse (in emotion, focus, tone, etc.) +- Each perspective should be expressed in 1st-person ("I like", "I wonder", "I feel", ...) +- Avoid telling the writer what to do; focus on the reader's viewpoint. +- The writer may not be finished writing the document; if the last sentence is incomplete, ignore that and focus on the content that is already written. +- Avoid providing specific words or phrases that the writer could directly copy into their document. +- If there is insufficient context to generate genuine perspectives, return an empty list.`, +}; + +const listResponseSchema = z.object({ + responses: z.array(z.string()).describe('List of suggestions'), +}); + +export async function POST(req: Request) { + const body: WritingSupportRequest = await req.json(); + + // Validate the request body + if (!body.editorState) { + return NextResponse.json( + { error: 'Missing editorState in request body' }, + { status: 400 } + ); + } + + const { beforeCursor, selectedText, afterCursor } = body.editorState; + const context = (body.context as keyof typeof prompts) || 'proposal_advice'; + const promptTemplate = prompts[context]; + + try { + const documentText = `${beforeCursor}${selectedText}${afterCursor}`; + const beforeCursorTrim = beforeCursor.slice(-100); + const afterCursorTrim = afterCursor.slice(0, 100); + + let fullPrompt = promptTemplate; + fullPrompt += `\n\n# Writer's Document So Far\n\n<document>\n${documentText}</document>\n\n`; + + if (selectedText === '') { + fullPrompt += `## Text Right Before the Cursor\n\n"${beforeCursorTrim}"`; + } else { + fullPrompt += `## Current Selection\n\n${selectedText}`; + fullPrompt += `\n\n## Text Nearby The Selection\n\n"${beforeCursorTrim}${selectedText}${afterCursorTrim}"`; + } + + const result = await generateObject({ + model: openai('gpt-5.2'), + schema: listResponseSchema, + prompt: fullPrompt, + system: 'You are a helpful and insightful writing assistant.', + }); + + const suggestions = result.object.responses.length > 0 + ? [result.object.responses.map(item => `- ${item}`).join('\n\n')] + : ['Insufficient content to generate suggestions.']; + + return NextResponse.json({ suggestions }); + } catch (error) { + console.error('Error generating suggestion:', error); + return NextResponse.json( + { error: 'Failed to generate suggestion' }, + { status: 500 } + ); + } +} diff --git a/experiment/app/favicon.ico b/experiment/app/favicon.ico new file mode 100644 index 00000000..718d6fea Binary files /dev/null and b/experiment/app/favicon.ico differ diff --git a/experiment/app/globals.css b/experiment/app/globals.css new file mode 100644 index 00000000..147376dd --- /dev/null +++ b/experiment/app/globals.css @@ -0,0 +1,41 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-fadeIn { + animation: fadeIn 0.3s ease-in; +} diff --git a/experiment/app/layout.tsx b/experiment/app/layout.tsx new file mode 100644 index 00000000..167a7f9a --- /dev/null +++ b/experiment/app/layout.tsx @@ -0,0 +1,37 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; +import { Provider } from "jotai"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Writing Task", + description: "AI-powered writing task with chat support", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + <html lang="en"> + <body + className={`${geistSans.variable} ${geistMono.variable} antialiased`} + > + <Provider> + {children} + </Provider> + </body> + </html> + ); +} diff --git a/experiment/app/page.tsx b/experiment/app/page.tsx new file mode 100644 index 00000000..8e9bb4f0 --- /dev/null +++ b/experiment/app/page.tsx @@ -0,0 +1,29 @@ +'use client'; + +/** + * Standalone demo page for AI writing assistance. + * This page is NOT used in the study - see components/study/TaskPage.tsx for the study task page. + */ + +import { useRef } from 'react'; +import AIPanel from '@/components/AIPanel'; +import WritingArea from '@/components/WritingArea'; +import type { WritingAreaRef } from '@/components/WritingArea'; + +export default function Home() { + const writingAreaRef = useRef<WritingAreaRef>(null); + + return ( + <div className="flex h-screen gap-5 p-10 bg-gray-100 overflow-hidden"> + {/* Left side - Writing Area */} + <div className="flex-1 flex flex-col min-w-0"> + <WritingArea ref={writingAreaRef} /> + </div> + + {/* Right side - AI Panel */} + <div className="flex flex-col gap-2.5 w-110 border border-gray-300 rounded overflow-hidden shadow-sm bg-white"> + <AIPanel writingAreaRef={writingAreaRef} /> + </div> + </div> + ); +} diff --git a/experiment/app/study/page.tsx b/experiment/app/study/page.tsx new file mode 100644 index 00000000..111980e6 --- /dev/null +++ b/experiment/app/study/page.tsx @@ -0,0 +1,124 @@ +'use client'; + +import { Suspense, useEffect } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { useAtom } from 'jotai'; +import { log } from '@/lib/logging'; +import { + DEFAULT_AUTO_REFRESH_INTERVAL, + DEFAULT_SCENARIO_ID, + VALID_CONDITIONS, + type letterToCondition, +} from '@/lib/studyConfig'; +import type { LogEventType, StudyParams } from '@/types/study'; +import { studyParamsAtom } from '@/contexts/StudyContext'; +import ScreenSizeCheck from '@/components/study/ScreenSizeCheck'; +import ConsentPage from '@/components/study/ConsentPage'; +import IntroPage from '@/components/study/IntroPage'; +import IntroSurvey from '@/components/study/IntroSurvey'; +import StartTaskPage from '@/components/study/StartTaskPage'; +import TaskPage from '@/components/study/TaskPage'; +import PostTaskSurvey from '@/components/study/PostTaskSurvey'; +import FinalPage from '@/components/study/FinalPage'; + +const pageComponents = { + consent: ConsentPage, + intro: IntroPage, + 'intro-survey': IntroSurvey, + 'start-task': StartTaskPage, + task: TaskPage, + 'post-task-survey': PostTaskSurvey, + final: FinalPage, +} as const; + +type PageKey = keyof typeof pageComponents; + +// Returns a StudyParams object or an error string +function parseStudyParams(searchParams: URLSearchParams): StudyParams | string { + const username = searchParams.get('username') || ''; + if (username.length === 0 || !/^[a-zA-Z0-9\-_]+$/.test(username)) { + return 'Invalid username: must be alphanumeric with dashes/underscores'; + } + + const conditionStr = searchParams.get('condition'); + if (!conditionStr || !(VALID_CONDITIONS as readonly string[]).includes(conditionStr)) { + return `Invalid condition: "${conditionStr ?? 'missing'}". Valid: ${VALID_CONDITIONS.join(', ')}`; + } + + const page = searchParams.get('page') || 'consent'; + if (!(page in pageComponents)) { + return `Invalid page: "${page}". Valid: ${Object.keys(pageComponents).join(', ')}`; + } + + const experiment = searchParams.get('experiment'); + const autoRefreshStr = searchParams.get('autoRefreshInterval'); + + return { + username, + condition: conditionStr as keyof typeof letterToCondition, + page, + experiment: experiment === 'type' ? 'type' : 'amount', + isProlific: searchParams.get('isProlific') === 'true', + autoRefreshInterval: autoRefreshStr ? parseInt(autoRefreshStr, 10) : DEFAULT_AUTO_REFRESH_INTERVAL, + scenario: searchParams.get('scenario') || DEFAULT_SCENARIO_ID + }; +} + +function StudyRouter() { + const searchParams = useSearchParams(); + const [, setStudyParams] = useAtom(studyParamsAtom); + + const paramsOrError = parseStudyParams(searchParams); + + // Update study params atom and log page view + useEffect(() => { + if (typeof paramsOrError === 'string') return; + const studyParams = paramsOrError; + + setStudyParams(studyParams); + + log({ + username: studyParams.username, + event: `view:${studyParams.page}` as LogEventType, + extra_data: { studyParams }, + }).catch((e) => console.error('Failed to log page view:', e)); + }, [paramsOrError, setStudyParams]); + + // Error state + if (typeof paramsOrError === 'string') { + return ( + <div className="flex items-center justify-center min-h-screen bg-gray-100"> + <div className="bg-white p-8 rounded-lg shadow-lg max-w-md"> + <h1 className="text-2xl font-bold text-red-600 mb-4">Invalid Parameters</h1> + <p className="text-gray-700 mb-4">{paramsOrError}</p> + <a + href="/study?page=consent&username=test&condition=a&scenario=roomDoubleBooking" + className="inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700" + > + Try Test URL + </a> + </div> + </div> + ); + } + + // TypeScript knows paramsOrError is StudyParams here + const PageComponent = pageComponents[paramsOrError.page as PageKey]; + + return ( + <ScreenSizeCheck> + <PageComponent /> + </ScreenSizeCheck> + ); +} + +/** + * Main study page - wraps router in Suspense boundary for useSearchParams + */ +export default function StudyPage() { + return ( + <Suspense fallback={<div className="p-8">Loading study...</div>}> + <StudyRouter /> + </Suspense> + ); +} diff --git a/experiment/components/AIPanel.tsx b/experiment/components/AIPanel.tsx new file mode 100644 index 00000000..c2fb8d7b --- /dev/null +++ b/experiment/components/AIPanel.tsx @@ -0,0 +1,325 @@ +'use client'; + +import { Fragment, useCallback, useRef, useState, useEffect } from 'react'; +import type { RefObject } from 'react'; +import type { WritingAreaRef } from '@/components/WritingArea'; +import type { GenerationResult, SavedItem, TextEditorState } from '@/types'; +import { log } from '@/lib/logging'; +import { API_TIMEOUT_MS } from '@/lib/studyConfig'; +import { useAtomValue } from 'jotai'; +import { studyConditionAtom, studyParamsAtom } from '@/contexts/StudyContext'; + +const visibleNameForMode = { + example_sentences: 'Examples of what you could write next:', + complete_document: 'Completed document suggestion:', + analysis_readerPerspective: 'Possible perspectives your reader might have:', + proposal_advice: 'Advice for your next words:', +}; + +const modes = ['example_sentences', 'complete_document', 'analysis_readerPerspective', 'proposal_advice'] as const; +const MIN_TEXT_LENGTH_FOR_SUGGESTION = 25; + +function GenerationResultDisplay({ generation }: { generation: GenerationResult }) { + return ( + <div className="prose prose-sm max-w-none"> + <div className="font-bold text-sm mb-2 text-gray-900"> + {visibleNameForMode[generation.generation_type as keyof typeof visibleNameForMode]} + </div> + <div className="text-sm text-gray-800 whitespace-pre-wrap leading-relaxed">{generation.result}</div> + </div> + ); +} + +function SavedGenerations({ + savedItems, + deleteSavedItem, +}: { + savedItems: SavedItem[]; + deleteSavedItem: (dateSaved: Date) => void; +}) { + return ( + <div className="flex-1 overflow-y-auto space-y-3"> + {savedItems.length === 0 ? ( + <div className="flex items-center justify-center h-24 text-center"> + <div className="text-sm text-gray-500 font-medium">No suggestions yet...</div> + </div> + ) : ( + savedItems.map((savedItem) => { + const key = savedItem.dateSaved.toString(); + return ( + <div + key={key} + className="bg-blue-50 border border-blue-200 rounded p-3 hover:bg-blue-100 hover:shadow-md transition-all" + > + <div className="text-xs font-semibold text-blue-700 mb-2"> + {savedItem.dateSaved.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + })} + </div> + <div className="mb-3"> + <GenerationResultDisplay generation={savedItem.generation} /> + </div> + <button + type="button" + onClick={() => deleteSavedItem(savedItem.dateSaved)} + className="text-xs px-2 py-1 rounded font-medium bg-red-100 text-red-800 hover:bg-red-200 active:bg-red-300 transition-colors" + > + Delete + </button> + </div> + ); + }) + )} + </div> + ); +} + +interface AIPanelProps { + writingAreaRef?: RefObject<WritingAreaRef | null>; + isStudyMode?: boolean; +} + +export default function AIPanel({ + writingAreaRef, + isStudyMode = false, +}: AIPanelProps) { + const [isLoading, setIsLoading] = useState(false); + const [savedItems, setSavedItems] = useState<SavedItem[]>([]); + const [errorMsg, setErrorMsg] = useState(''); + const docContextRef = useRef<TextEditorState | null>(null); + const studyParams = useAtomValue(studyParamsAtom); + const studyCondition = useAtomValue(studyConditionAtom); + const mode = isStudyMode ? studyCondition : undefined; + const autoRefreshInterval = studyParams.autoRefreshInterval; + const autoRefreshIntervalRef = useRef<NodeJS.Timeout | null>(null); + const previousRequestRef = useRef<{ editorState: TextEditorState; mode: string } | null>(null); + const previousResponseRef = useRef<GenerationResult | null>(null); + + const save = useCallback((generation: GenerationResult, document: TextEditorState) => { + setSavedItems((prev) => [ + { + generation, + document, + dateSaved: new Date(), + }, + ...prev, + ]); + }, []); + + const deleteSavedItem = useCallback((dateSaved: Date) => { + setSavedItems((prev) => prev.filter((item) => item.dateSaved !== dateSaved)); + }, []); + + const getSuggestion = useCallback( + async (suggestionMode?: string, isAutoRefresh = false) => { + setErrorMsg(''); + setIsLoading(true); + + // Use provided mode or fall back to component mode + const modeToUse = suggestionMode || mode; + if (!modeToUse) { + setErrorMsg('No mode specified'); + setIsLoading(false); + return; + } + + try { + // Get current editor state + const editorState = writingAreaRef?.current?.getEditorState(); + if (!editorState) { + setErrorMsg('Unable to read editor state'); + return; + } + + // Check if there's not enough text to bother making a request + const currentText = editorState.beforeCursor + editorState.selectedText + editorState.afterCursor; + if (currentText.trim().length < MIN_TEXT_LENGTH_FOR_SUGGESTION) { + // Too little text to generate a suggestion + // But suggestions are automatically requested, so just silently skip + setIsLoading(false); + return; + } + + // Check if this is a duplicate request (same content and mode) + if ( + previousRequestRef.current && + JSON.stringify(previousRequestRef.current.editorState) === JSON.stringify(editorState) && + previousRequestRef.current.mode === modeToUse + ) { + setIsLoading(false); + return; + } + + docContextRef.current = editorState; + previousRequestRef.current = { editorState, mode: modeToUse }; + + // Log AI request in study mode + if (isStudyMode) { + await log({ + username: studyParams.username, + event: `aiRequest:${modeToUse}`, + extra_data: { + isAutoRefresh, + // Don't log document content right now; we'll log it with the response + }, + }); + } + + const response = await fetch('/api/writing-support', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + editorState, + context: modeToUse, + }), + signal: AbortSignal.timeout(API_TIMEOUT_MS), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = (await response.json()) as { suggestions: string[] }; + + // Convert response to GenerationResult + const result = data.suggestions[0] || ''; + const generation: GenerationResult = { + result, + generation_type: modeToUse as GenerationResult['generation_type'], + }; + + if (result) { + // Check if this response is identical to the previous one + const isDuplicate = + previousResponseRef.current && + previousResponseRef.current.result === generation.result && + previousResponseRef.current.generation_type === generation.generation_type; + + if (!isDuplicate) { + save(generation, editorState); + previousResponseRef.current = generation; + + // Log AI response in study mode + if (isStudyMode) { + await log({ + username: studyParams.username, + event: `aiResponse:${modeToUse}`, + extra_data: { isAutoRefresh, generation, editorState }, + }); + } + } + } else { + setErrorMsg('Received empty suggestion.'); + } + } catch (err: unknown) { + let errMsg = ''; + if (err instanceof Error) { + if (err.name === 'AbortError') { + errMsg = 'Generating a suggestion took too long, please try again.'; + } else { + errMsg = `${err.name}: ${err.message}. Please try again.`; + } + } else { + errMsg = 'An error occurred while generating the suggestion.'; + } + setErrorMsg(errMsg); + } finally { + setIsLoading(false); + } + }, + [writingAreaRef, save, mode, isStudyMode, studyParams] + ); + + // Auto-refresh logic for study mode + useEffect(() => { + if (!isStudyMode || !mode || mode === 'no_ai') { + return; + } + + // Set up auto-refresh interval + autoRefreshIntervalRef.current = setInterval(() => { + getSuggestion(mode, true); + }, autoRefreshInterval); + + return () => { + if (autoRefreshIntervalRef.current) { + clearInterval(autoRefreshIntervalRef.current); + } + }; + }, [isStudyMode, mode, autoRefreshInterval, getSuggestion]); + + let alerts = null; + + if (errorMsg !== '') { + alerts = ( + <div className="p-3 bg-red-100 border-l-4 border-red-600 rounded"> + <div className="text-sm font-semibold text-red-900 text-center">{errorMsg}</div> + </div> + ); + } else if (mode !== 'no_ai' && autoRefreshInterval > 0) { + alerts = ( + <div className="p-3"> + <div className="text-xs font-medium text-gray-700 text-center"> + Suggestions will refresh every {(autoRefreshInterval / 1000).toFixed(0)} seconds. + </div> + </div> + ); + } else if (savedItems.length === 0) { + alerts = ( + <div className="p-3 bg-blue-50"> + <div className="text-xs font-medium text-gray-700 text-center"> + Click a button above to generate a suggestion. + </div> + </div> + ); + } + + if (isLoading) { + alerts = ( + <div className="flex justify-center py-4"> + <div className="w-5 h-5 border-2 border-blue-400 border-t-blue-600 rounded-full animate-spin" /> + </div> + ); + } + + return ( + <div className="h-full p-4 text-sm text-gray-700 flex flex-col overflow-hidden"> + <h3 className="text-sm font-bold text-gray-900 mb-3">AI Writing Assistant</h3> + + {!isStudyMode && ( + <div className="flex gap-1.5 mb-3 flex-wrap"> + {modes.map((mode) => ( + <Fragment key={mode}> + <button + type="button" + disabled={isLoading} + onClick={() => getSuggestion(mode)} + className="px-3 py-1.5 text-xs font-semibold text-gray-700 border border-gray-400 rounded bg-gray-50 hover:bg-blue-50 hover:border-blue-400 active:bg-blue-100 disabled:opacity-40 disabled:cursor-not-allowed transition-colors" + title={visibleNameForMode[mode]} + > + {mode === 'example_sentences' + ? 'Examples' + : mode === 'analysis_readerPerspective' + ? 'Reader Perspectives' + : mode === 'complete_document' + ? 'Complete Document' + : 'Advice'} + </button> + </Fragment> + ))} + </div> + )} + + {alerts} + + <SavedGenerations savedItems={savedItems} deleteSavedItem={deleteSavedItem} /> + + <div className="text-xs text-gray-500 text-center mt-3 pt-2 border-t border-gray-300 font-medium"> + AI-generated text may vary in quality + </div> + </div> + ); +} diff --git a/experiment/components/ChatPanel.tsx b/experiment/components/ChatPanel.tsx new file mode 100644 index 00000000..067ee955 --- /dev/null +++ b/experiment/components/ChatPanel.tsx @@ -0,0 +1,452 @@ +'use client'; + +import { useChat } from '@ai-sdk/react'; +import { DefaultChatTransport } from 'ai'; +import { useEffect, useEffectEvent, useMemo, useRef, useState } from 'react'; +import { useAtomValue } from 'jotai'; +import { studyParamsAtom } from '@/contexts/StudyContext'; +import { log } from '@/lib/logging'; +import { calculateTypingDuration, calculateInterMessageDelay, calculateThinkingDelay } from '@/lib/messageTiming'; +import { getScenario } from '@/lib/studyConfig'; + +// Utility function to extract text from message parts +function getMessageText(message: { parts: Array<{ type: string; text?: string }> }): string { + return message.parts + .filter((part) => part.type === 'text') + .map((part) => part.text || '') + .join(''); +} + +// Utility function to parse JSON array responses from the assistant +function parseMessageContent(content: string): string[] { + try { + // Remove markdown code blocks if present + const cleaned = content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim(); + const parsed = JSON.parse(cleaned); + + if (Array.isArray(parsed)) { + return parsed; + } + return [content]; + } catch { + return [content]; + } +} + +const FOLLOWUP_DELAY_MS = 75000; // 75 seconds (between 60-90s) + +interface ChatPanelProps { + onNewMessage?: () => void; +} + +export default function ChatPanel({ onNewMessage }: ChatPanelProps) { + const studyParams = useAtomValue(studyParamsAtom); + const username = studyParams.username || 'demo'; + const scenario = getScenario(studyParams.scenario); + + const { messages, sendMessage, status, setMessages } = useChat({ + transport: new DefaultChatTransport({ + api: '/api/chat', + body: { + scenario: studyParams.scenario, + }, + }), + }); + + const [input, setInput] = useState(''); + const messagesEndRef = useRef<HTMLDivElement>(null); + + const [visibleMessagePartCount, setVisibleMessagePartCount] = useState(0); + const [visibleMessageId, setVisibleMessageId] = useState<string | null>(null); + const [showTypingIndicator, setShowTypingIndicator] = useState(false); + const [deliveredMessageIds, setDeliveredMessageIds] = useState<Set<string>>(new Set()); + const [readMessageIds, setReadMessageIds] = useState<Set<string>>(new Set()); + const loggedMessagePartsRef = useRef<Set<string>>(new Set()); + const hasInitializedRef = useRef(false); + const followupSentRef = useRef(false); + const followupTimerRef = useRef<NodeJS.Timeout | null>(null); + + const isLoading = status === 'submitted' || status === 'streaming'; + + const scrollToBottom = useEffectEvent(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }); + + // Derive the messages that are actually displayed to the user + const displayedMessages = useMemo(() => { + return messages.map((message, messageIdx) => { + const isUser = message.role === 'user'; + const messageText = getMessageText(message); + if (!messageText) return { parts: [], isUser, messageId: message.id }; + + const messageParts = isUser ? [messageText] : parseMessageContent(messageText); + const isLastMessage = messageIdx === messages.length - 1; + + // For assistant messages, limit visible parts if it's the last message + let partsToShow = messageParts; + if (!isUser && isLastMessage) { + // Hide last message while streaming to avoid showing partial JSON + if (status === 'streaming') { + partsToShow = []; + } else if (visibleMessageId === message.id && visibleMessagePartCount > 0) { + // Only use visibleMessagePartCount if it's for THIS message + partsToShow = messageParts.slice(0, visibleMessagePartCount); + } else { + partsToShow = []; + } + } + + return { parts: partsToShow, isUser, messageId: message.id }; + }); + }, [messages, visibleMessagePartCount, visibleMessageId, status]); + + // Scroll to bottom whenever displayed messages change + // biome-ignore lint/correctness/useExhaustiveDependencies: scroll target position depends on displayed messages rendering. + useEffect(() => { + scrollToBottom(); + }, [displayedMessages]); + + // Initialize conversation on mount + useEffect(() => { + if (!hasInitializedRef.current && messages.length === 0) { + hasInitializedRef.current = true; + setMessages([ + { + id: 'initial-user-message', + role: 'user', + parts: [{ type: 'text', text: '' }], + }, + { + id: 'initial-assistant-message', + role: 'assistant', + parts: [{ type: 'text', text: JSON.stringify(scenario.chat.initialMessages) }], + } + ]); + } + }, [messages.length, setMessages, scenario.chat.initialMessages]); + + // Proactive follow-up timer: if user hasn't sent a message after FOLLOWUP_DELAY_MS, colleague sends a nudge + useEffect(() => { + // Check if user has sent any real messages (beyond the initial empty one) + const userHasSentMessage = messages.some( + (m) => m.role === 'user' && m.id !== 'initial-user-message' && getMessageText(m).trim() !== '' + ); + + // If user has engaged or follow-up already sent, clear any pending timer + if (userHasSentMessage || followupSentRef.current) { + if (followupTimerRef.current) { + clearTimeout(followupTimerRef.current); + followupTimerRef.current = null; + } + return; + } + + // Start the follow-up timer if not already running + if (!followupTimerRef.current && hasInitializedRef.current) { + followupTimerRef.current = setTimeout(() => { + if (!followupSentRef.current) { + followupSentRef.current = true; + // Add follow-up message from colleague + setMessages((prev) => [ + ...prev, + { + id: 'followup-message', + role: 'assistant', + parts: [{ type: 'text', text: JSON.stringify([scenario.chat.followUpMessage]) }], + }, + ]); + // Reset visible count so it goes through the typing animation + setVisibleMessagePartCount(0); + } + }, FOLLOWUP_DELAY_MS); + } + + return () => { + if (followupTimerRef.current) { + clearTimeout(followupTimerRef.current); + followupTimerRef.current = null; + } + }; + }, [messages, setMessages, scenario.chat.followUpMessage]); + + // Sequence message display with delays and typing indicators + useEffect(() => { + if (messages.length === 0) return; + + const lastMessage = messages[messages.length - 1]; + if (lastMessage.role !== 'assistant') return; + + const messageText = getMessageText(lastMessage); + const parsedMessages = parseMessageContent(messageText); + + if (parsedMessages.length === 0) return; + + const timers: NodeJS.Timeout[] = []; + + // Busy/read delay before typing indicator shows up + const thinkingDelay = calculateThinkingDelay(parsedMessages[0].length); + const busyLag = messages.length === 2 ? 0 : 1200; // Skip lag for first message (length 2 because of initial empty user message + assistant response) + const readingDelay = messages.length === 2 ? 0 : thinkingDelay + busyLag; + + const firstTypingDuration = calculateTypingDuration(parsedMessages[0].length); + + // Start typing indicator after she has "read" the message + timers.push( + setTimeout(() => { + setShowTypingIndicator(true); + scrollToBottom(); + }, readingDelay) + ); + + // Reveal first part after typing duration + timers.push( + setTimeout(() => { + setVisibleMessageId(lastMessage.id); + setVisibleMessagePartCount(1); + setShowTypingIndicator(false); + }, readingDelay + firstTypingDuration) + ); + + // For multiple messages (array response), add typing indicator and delay between them + if (parsedMessages.length > 1) { + let currentDelay = readingDelay + firstTypingDuration; + + parsedMessages.forEach((messagePart, index) => { + if (index > 0) { + // Calculate delay based on previous message length + const previousMessageLength = parsedMessages[index - 1].length; + const interDelay = calculateInterMessageDelay(previousMessageLength); + currentDelay += interDelay; + + // Calculate typing duration for this message + const typingDuration = calculateTypingDuration(messagePart.length); + + // Show typing indicator for the delay duration + timers.push( + setTimeout(() => { + setShowTypingIndicator(true); + scrollToBottom(); + }, currentDelay) + ); + + // Show next part and hide typing indicator + timers.push( + setTimeout(() => { + setVisibleMessagePartCount((prev) => prev + 1); + setShowTypingIndicator(false); + }, currentDelay + typingDuration) + ); + + currentDelay += typingDuration; + } + }); + } + + return () => { + timers.forEach((timer) => { + clearTimeout(timer); + }); + }; + }, [messages]); + + const formatTime = (date: Date) => { + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + }; + + // Track user messages for delivered/read status (uses useChat's message IDs for UI) + const trackedUserMessageIdsRef = useRef<Set<string>>(new Set()); + useEffect(() => { + const deliveredTimers: NodeJS.Timeout[] = []; + const readTimers: NodeJS.Timeout[] = []; + messages.forEach((message) => { + if (message.role !== 'user' || !message.id || message.id === 'initial-user-message') return; + if (trackedUserMessageIdsRef.current.has(message.id)) return; + trackedUserMessageIdsRef.current.add(message.id); + + // Mark message as delivered after a short delay + const deliveredDelay = 500 + Math.random() * 500; // 0.5-1s + const deliveredTimer = setTimeout(() => { + setDeliveredMessageIds((prev) => new Set(prev).add(message.id)); + }, deliveredDelay); + deliveredTimers.push(deliveredTimer); + + // Mark message as read after a short delay to feel more human + const readDelay = 3000 + Math.random() * 5000; // 3-8 seconds + const readTimer = setTimeout(() => { + setReadMessageIds((prev) => new Set(prev).add(message.id)); + }, readDelay); + readTimers.push(readTimer); + }); + + // Cleanup the timers on unmount + return () => { + deliveredTimers.forEach((timer) => void clearTimeout(timer)); + readTimers.forEach((timer) => void clearTimeout(timer)); + }; + }, [messages]); + + // Log assistant message part event - called when a new part becomes visible + const logAssistantMessagePart = useEffectEvent((messageId: string, partIndex: number, content: string) => { + const partId = `${messageId}-${partIndex}`; + if (!loggedMessagePartsRef.current.has(partId)) { + log({ + username, + event: 'chatMessage:assistant', + extra_data: { + messageId, + partIndex, + content, + timestamp: new Date().toISOString(), + }, + }); + loggedMessagePartsRef.current.add(partId); + } + }); + + // Track assistant message parts becoming visible and log them + useEffect(() => { + if (messages.length === 0) return; + + messages.forEach((message, messageIndex) => { + if (message.role !== 'assistant' || !message.id) return; + + const messageText = getMessageText(message); + const parsedMessages = parseMessageContent(messageText); + const isLastMessage = messageIndex === messages.length - 1; + + if (isLastMessage) { + // For last message, only log visible parts if the count is for THIS message + if (visibleMessageId === message.id) { + for (let i = 0; i < visibleMessagePartCount; i++) { + logAssistantMessagePart(message.id, i, parsedMessages[i]); + } + } + // Otherwise, don't log anything yet - wait for the sequencing effect to reveal parts + } else { + // For non-last messages, log all parts + parsedMessages.forEach((part, partIndex) => { + logAssistantMessagePart(message.id, partIndex, part); + }); + } + }); + }, [messages, visibleMessagePartCount, visibleMessageId]); + + const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => { + e.preventDefault(); + if (!input.trim() || isLoading) return; + + const userMessage = input; + const messageId = `user-${Date.now()}`; + setInput(''); + setShowTypingIndicator(false); // ensure no immediate typing indicator on send + + // Reset visibility state BEFORE sending - the sequencing effect will set these + // for the new response when it arrives + setVisibleMessagePartCount(0); + setVisibleMessageId(null); + + // Log the user message event immediately (event-driven, no duplicates) + log({ + username, + event: 'chatMessage:user', + extra_data: { + messageId, + content: userMessage, + timestamp: new Date().toISOString(), + }, + }); + + await sendMessage({ text: userMessage }); + }; + + const notifyNewMessage = useEffectEvent(() => { + if (onNewMessage) { + onNewMessage(); + } + }); + + // Show notification when a new assistant message part appears + useEffect(() => { + if (messages.length === 0) return; + const lastMessage = messages[messages.length - 1]; + if (lastMessage && lastMessage.role === 'assistant' && visibleMessagePartCount > 0) { + notifyNewMessage(); + } + }, [messages, visibleMessagePartCount]); + + return ( + <div className="h-full flex flex-col overflow-hidden"> + <div className="flex items-center gap-2.5 bg-gray-50 border-b border-gray-300 px-3 py-2.5"> + <div className="w-2 h-2 rounded-full bg-yellow-500" /> + <div className="flex-1"> + <div className="font-semibold text-sm text-gray-900">{scenario.colleague.name}</div> + <div className="text-xs text-gray-600">{scenario.colleague.role}</div> + </div> + <div className="text-xs font-medium text-gray-700">Busy</div> + + </div> + + <div className="flex-1 p-2.5 overflow-y-auto bg-white"> + {displayedMessages.map((displayedMessage) => { + return displayedMessage.parts.map((part, partIdx) => ( + <div + key={`${displayedMessage.messageId}-${partIdx}`} + className={`mb-3 text-sm leading-snug animate-fadeIn ${ + displayedMessage.isUser ? 'ml-auto' : '' + }`} + style={{ maxWidth: '85%' }} + > + <div + className={`px-2.5 py-2 rounded-xl text-gray-900 ${ + displayedMessage.isUser + ? 'bg-blue-200 rounded-br-sm ml-auto font-medium' + : 'bg-gray-200 rounded-bl-sm' + }`} + > + {part} + </div> + <div className="text-[10px] text-gray-600 mt-1"> + {formatTime(new Date())} + </div> + {displayedMessage.isUser && ( + readMessageIds.has(displayedMessage.messageId) ? ( + <div className="text-[10px] font-semibold text-green-700 mt-0.5">Read</div> + ) : deliveredMessageIds.has(displayedMessage.messageId) ? ( + <div className="text-[10px] font-semibold text-gray-600 mt-0.5">Delivered</div> + ) : null + )} + </div> + )); + })} + + {showTypingIndicator && ( + <div className="flex items-center gap-1 px-3 py-2 bg-gray-100 rounded-xl w-fit mb-3"> + <div className="w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce" style={{ animationDelay: '0s' }} /> + <div className="w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce" style={{ animationDelay: '0.2s' }} /> + <div className="w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce" style={{ animationDelay: '0.4s' }} /> + </div> + )} + + <div ref={messagesEndRef} /> + </div> + + <form onSubmit={onSubmit} className="border-t border-gray-300 p-2 bg-gray-50 flex gap-1.5"> + <input + type="text" + className="flex-1 px-2.5 py-2 border border-gray-400 rounded-2xl text-sm text-gray-900 placeholder-gray-500 bg-white outline-none focus:border-green-600 focus:ring-2 focus:ring-green-200" + placeholder={`Message ${scenario.colleague.firstName}...`} + value={input} + onChange={(e) => setInput(e.target.value)} + disabled={isLoading} + /> + <button + type="submit" + className="px-4 py-2 bg-green-600 text-white rounded-2xl text-sm font-semibold hover:bg-green-700 active:bg-green-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" + disabled={isLoading || !input.trim()} + > + Send + </button> + </form> + </div> + ); +} diff --git a/experiment/components/WritingArea.tsx b/experiment/components/WritingArea.tsx new file mode 100644 index 00000000..7edf259b --- /dev/null +++ b/experiment/components/WritingArea.tsx @@ -0,0 +1,146 @@ +'use client'; + +import { useState, useRef, useImperativeHandle, forwardRef } from 'react'; +import { useAtomValue } from 'jotai'; +import { studyParamsAtom } from '@/contexts/StudyContext'; +import { getScenario } from '@/lib/studyConfig'; +import type { TextEditorState } from '@/types'; + +export interface WritingAreaRef { + getEditorState: () => TextEditorState; +} + +interface WritingAreaProps { + onSend?: (content: string) => Promise<void>; + onUpdate?: (state: TextEditorState) => Promise<void>; + showSendButton?: boolean; +} + +const WritingArea = forwardRef<WritingAreaRef, WritingAreaProps>( + function WritingArea( + { onSend, onUpdate, showSendButton = false }, + ref + ) { + const studyParams = useAtomValue(studyParamsAtom); + const scenario = getScenario(studyParams.scenario); + + const [subject, setSubject] = useState(''); + const [body, setBody] = useState(''); + const [isSending, setIsSending] = useState(false); + const textareaRef = useRef<HTMLTextAreaElement>(null); + + const getEditorState = (): TextEditorState => { + const textarea = textareaRef.current; + if (!textarea) { + return { beforeCursor: '', selectedText: '', afterCursor: '' }; + } + + const text = textarea.value; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + + return { + beforeCursor: text.slice(0, start), + selectedText: text.slice(start, end), + afterCursor: text.slice(end), + }; + }; + + useImperativeHandle(ref, () => ({ + getEditorState, + })); + + const handleBodyChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { + const newBody = e.target.value; + setBody(newBody); + if (onUpdate) { + onUpdate(getEditorState()).catch((e) => + console.error('Failed to log document update:', e) + ); + } + }; + + const handleSend = async () => { + if (onSend) { + setIsSending(true); + try { + await onSend(body); + } catch (error) { + console.error('Failed to send:', error); + } finally { + setIsSending(false); + } + } + }; + + return ( + <div className="flex-1 bg-white border border-gray-300 rounded flex flex-col shadow-sm overflow-hidden"> + <div className="border-b border-gray-200 p-4 bg-gray-50"> + <div className="flex gap-3 items-start"> + <div className="flex-1 space-y-2"> + <div className="flex text-sm"> + <label htmlFor="from-field" className="w-16 text-gray-700 font-medium">From:</label> + <input + id="from-field" + type="text" + className="flex-1 border border-gray-300 px-2 py-1 rounded text-sm bg-white text-gray-900" + value={`${scenario.sender.name}`} + readOnly + /> + </div> + <div className="flex text-sm"> + <label htmlFor="to-field" className="w-16 text-gray-700 font-medium">To:</label> + <input + id="to-field" + type="text" + className="flex-1 border border-gray-300 px-2 py-1 rounded text-sm bg-white text-gray-900" + value={`${scenario.recipient.name} <${scenario.recipient.email}>`} + readOnly + /> + </div> + <div className="flex text-sm"> + <label htmlFor="subject-field" className="w-16 text-gray-700 font-medium">Subject:</label> + <input + id="subject-field" + type="text" + className="flex-1 border border-gray-300 px-2 py-1 rounded text-sm bg-white text-gray-900 placeholder-gray-400" + placeholder="Enter subject..." + value={subject} + onChange={(e) => setSubject(e.target.value)} + /> + </div> + </div> + {showSendButton && ( + <button + type="button" + onClick={handleSend} + disabled={isSending || !body.trim()} + className="self-stretch px-4 py-2 bg-green-600 text-white font-medium rounded hover:bg-green-700 active:bg-green-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex flex-col items-center gap-1" + aria-label={isSending ? 'Sending...' : 'Send email'} + > + <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"> + <line x1="22" y1="2" x2="11" y2="13" /> + <polygon points="22 2 15 22 11 13 2 9 22 2" /> + </svg> + {isSending ? 'Sending...' : 'Send'} + </button> + )} + </div> + </div> + <div className="flex-1 overflow-hidden p-4 flex flex-col"> + <textarea + ref={textareaRef} + className="flex-1 border border-gray-200 p-2.5 resize-none text-sm rounded leading-relaxed focus:outline-none focus:border-green-500 focus:ring-2 focus:ring-green-200 bg-white text-gray-900 placeholder-gray-400" + placeholder="Write your message here..." + value={body} + onChange={handleBodyChange} + /> + </div> + </div> + ); + } +); + +WritingArea.displayName = 'WritingArea'; + +export default WritingArea; diff --git a/experiment/components/study/ConsentPage.tsx b/experiment/components/study/ConsentPage.tsx new file mode 100644 index 00000000..3704eeb9 --- /dev/null +++ b/experiment/components/study/ConsentPage.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { useState } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { log } from '@/lib/logging'; +import { CONSENT_FORM_URL, getNextPage } from '@/lib/studyConfig'; + +export default function ConsentPage() { + const searchParams = useSearchParams(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleLaunchConsent = async () => { + if (isSubmitting) return; + setIsSubmitting(true); + // Log the event + await log({ + username: searchParams.get('username') || 'unknown', + event: 'launchConsentForm', + }); + + // Build redirect URL with study parameters + const params = new URLSearchParams(searchParams.toString()); + params.set('page', getNextPage('consent')!); + const redirectUrl = `${window.location.origin}/study?${params.toString()}`; + + // Redirect to external consent form with return URL + const consentUrl = new URL(CONSENT_FORM_URL); + consentUrl.searchParams.set('redirect_url', redirectUrl); + window.location.href = consentUrl.toString(); + }; + + return ( + <div className="max-w-2xl mx-auto p-8"> + <h1 className="text-3xl font-bold mb-6">Informed Consent Form</h1> + + <div className="bg-gray-50 p-6 rounded-lg space-y-4 mb-8 text-sm text-gray-700"> + <p> + <strong>Purpose:</strong> This study investigates how AI writing + assistance affects the writing process. + </p> + + <p> + <strong>Procedure:</strong> Your participation entails performing a writing task using a system that offers AI-generated suggestions. You may or may not receive AI suggestions + depending on the condition you are assigned to. + </p> + + <p> + <strong>Time Commitment:</strong> Approximately 20-30 minutes. + </p> + + <p> + <strong>Risks:</strong> Minimal. No sensitive data will be collected. + </p> + + <p> + <strong>Compensation:</strong> $5 upon study completion. + </p> + + <p> + <strong>Confidentiality:</strong> Your responses will be anonymized + and stored securely. + </p> + + <p> + <strong>Voluntary Participation:</strong> Your participation is + completely voluntary. You can withdraw at any time without penalty. + </p> + </div> + + <button + type='button' + onClick={handleLaunchConsent} + disabled={isSubmitting} + className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition disabled:bg-blue-400 disabled:cursor-not-allowed" + > + {isSubmitting ? 'Loading...' : 'View Full Consent Form'} + </button> + + <p className="text-xs text-gray-500 mt-4 text-center"> + Clicking above will take you to the full consent form. After consenting, + you will return here to proceed with the study. + </p> + </div> + ); +} diff --git a/experiment/components/study/FinalPage.tsx b/experiment/components/study/FinalPage.tsx new file mode 100644 index 00000000..3ad05628 --- /dev/null +++ b/experiment/components/study/FinalPage.tsx @@ -0,0 +1,69 @@ +'use client'; + +import { useSearchParams } from 'next/navigation'; +import { COMPLETION_CODE } from '@/lib/studyConfig'; + +export default function FinalPage() { + const searchParams = useSearchParams(); + const isProlific = searchParams.get('isProlific') === 'true'; + + return ( + <div className="max-w-2xl mx-auto p-8"> + <h1 className="text-3xl font-bold mb-6 text-green-600"> + Thank You! + </h1> + + <div className="space-y-6"> + <section> + <p className="text-lg text-gray-700"> + Thank you for completing this research study. Your responses and + writing sample have been recorded and will be used to improve our + understanding of how writers interact with AI assistance. + </p> + </section> + + {isProlific && ( + <section className="bg-green-50 p-6 rounded-lg border border-green-200"> + <h2 className="text-xl font-semibold mb-3 text-green-800"> + Prolific Study Completion + </h2> + <p className="text-gray-700 mb-3"> + Your study completion code is: + </p> + <div className="bg-white p-4 rounded font-mono text-center text-xl font-bold text-green-600 border-2 border-green-300 mb-3"> + {COMPLETION_CODE} + </div> + <p className="text-sm text-gray-600"> + Please enter this code in Prolific to confirm your completion and + receive payment. + </p> + </section> + )} + + <section> + <h2 className="text-xl font-semibold mb-3">Next Steps</h2> + <ul className="list-disc list-inside space-y-2 text-gray-700"> + {isProlific ? ( + <li>Return to Prolific and submit the code above</li> + ) : ( + <li>Your data has been recorded</li> + )} + <li> + If you have any questions, please contact the research team + </li> + <li> + Your anonymous data will be used to improve AI writing tools + </li> + </ul> + </section> + + <section className="bg-gray-50 p-4 rounded-lg"> + <p className="text-sm text-gray-600"> + This research was conducted by the <a href="https://thoughtful-ai.com/">Thoughtful AI Lab</a> at Calvin University. For questions + about this study, please contact ken.arnold@calvin.edu. + </p> + </section> + </div> + </div> + ); +} diff --git a/experiment/components/study/IntroPage.tsx b/experiment/components/study/IntroPage.tsx new file mode 100644 index 00000000..3077a730 --- /dev/null +++ b/experiment/components/study/IntroPage.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { useState } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { getBrowserMetadata } from '@/lib/browserMetadata'; +import { log } from '@/lib/logging'; +import { getNextPage } from '@/lib/studyConfig'; + +export default function IntroPage() { + const searchParams = useSearchParams(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleStartStudy = async () => { + if (isSubmitting) return; + setIsSubmitting(true); + const username = searchParams.get('username') || ''; + const browserMetadata = getBrowserMetadata(); + + // Log the start event with browser metadata + await log({ + username, + event: 'Started Study', + extra_data: browserMetadata, + }); + + // Update params and navigate + const params = new URLSearchParams(searchParams.toString()); + params.set('page', getNextPage('intro')!); + window.location.href = `/study?${params.toString()}`; + }; + + return ( + <div className="max-w-2xl mx-auto p-8"> + <div className="space-y-6 mb-8"> + <section> + <h2 className="text-2xl font-semibold mb-3">Overview</h2> + <p className="text-gray-700 mb-4"> + Thank you for participating in this research study about AI writing + assistance. Your contributions will help us understand how writers + interact with AI tools. + </p> + </section> + + <section> + <h2 className="text-2xl font-semibold mb-3">What You’ll Do</h2> + <ol className="list-decimal list-inside space-y-2 text-gray-700"> + <li>Complete a brief questionnaire about your background</li> + <li>Write an email message. We'll walk you through the task step-by-step. + </li> + <li>Complete a follow-up questionnaire about your experience</li> + </ol> + </section> + </div> + + <button + type="button" + onClick={handleStartStudy} + disabled={isSubmitting} + className="w-full px-6 py-3 bg-green-600 text-white rounded-lg font-semibold hover:bg-green-700 transition disabled:bg-green-400 disabled:cursor-not-allowed" + > + {isSubmitting ? 'Loading...' : 'On to the Intro Survey β†’'} + </button> + </div> + ); +} diff --git a/experiment/components/study/IntroSurvey.tsx b/experiment/components/study/IntroSurvey.tsx new file mode 100644 index 00000000..19c5e2fe --- /dev/null +++ b/experiment/components/study/IntroSurvey.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { useSearchParams } from 'next/navigation'; +import { useAtom } from 'jotai'; +import { log, logThenRedirect } from '@/lib/logging'; +import { getNextPage } from '@/lib/studyConfig'; +import { surveyInputAtom } from '@/contexts/StudyContext'; +import Survey from '@/components/survey/Survey'; +import { introSurveyQuestions } from '@/components/survey/surveyData'; + +export default function IntroSurvey() { + const searchParams = useSearchParams(); + const [surveyInputs] = useAtom(surveyInputAtom); + const username = searchParams.get('username') || ''; + + const handleSubmit = async () => { + // Log survey completion + const params = new URLSearchParams(searchParams.toString()); + params.set('page', getNextPage('intro-survey')!); + const nextUrl = `/study?${params.toString()}`; + + await logThenRedirect( + { + username, + event: 'surveyComplete:intro-survey', + extra_data: surveyInputs, + }, + nextUrl + ); + }; + + return ( + <div className="max-w-2xl mx-auto p-8"> + <Survey + title="Background Information" + description="Please tell us a bit about yourself and your experience with AI tools." + questions={introSurveyQuestions} + onSubmit={handleSubmit} + submitButtonText="Continue to Task" + /> + </div> + ); +} diff --git a/experiment/components/study/PostTaskSurvey.tsx b/experiment/components/study/PostTaskSurvey.tsx new file mode 100644 index 00000000..23a2baf0 --- /dev/null +++ b/experiment/components/study/PostTaskSurvey.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { useSearchParams } from 'next/navigation'; +import { useAtom } from 'jotai'; +import { log, logThenRedirect } from '@/lib/logging'; +import { getNextPage, letterToCondition } from '@/lib/studyConfig'; +import { surveyInputAtom } from '@/contexts/StudyContext'; +import Survey from '@/components/survey/Survey'; +import { + getPostTaskSurveyQuestions, + conditionDebriefs, +} from '@/components/survey/surveyData'; + +export default function PostTaskSurvey() { + const searchParams = useSearchParams(); + const [surveyInputs] = useAtom(surveyInputAtom); + const username = searchParams.get('username') || ''; + const conditionCode = (searchParams.get('condition') || 'n') as keyof typeof letterToCondition; + const condition = letterToCondition[conditionCode]; + + const surveyQuestions = getPostTaskSurveyQuestions(condition); + const debrief = conditionDebriefs[condition]; + + const handleSubmit = async () => { + // Log survey completion + const params = new URLSearchParams(searchParams.toString()); + params.set('page', getNextPage('post-task-survey')!); + const nextUrl = `/study?${params.toString()}`; + + await logThenRedirect( + { + username, + event: 'surveyComplete:post-task-survey', + extra_data: surveyInputs, + }, + nextUrl + ); + }; + + return ( + <div className="max-w-2xl mx-auto p-8"> + <Survey + title="Task Completion Survey" + description="Please tell us about your experience with the writing task." + questions={surveyQuestions} + onSubmit={handleSubmit} + submitButtonText="Continue" + > + {debrief && ( + <div className="bg-blue-50 p-4 rounded-lg border-l-4 border-blue-500"> + <h3 className="font-semibold text-blue-900 mb-2"> + {debrief.title} + </h3> + <p className="text-blue-800 text-sm">{debrief.content}</p> + </div> + )} + </Survey> + </div> + ); +} diff --git a/experiment/components/study/ScreenSizeCheck.tsx b/experiment/components/study/ScreenSizeCheck.tsx new file mode 100644 index 00000000..22642923 --- /dev/null +++ b/experiment/components/study/ScreenSizeCheck.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { checkScreenSize } from '@/lib/browserMetadata'; +import { MIN_SCREEN_HEIGHT, MIN_SCREEN_WIDTH } from '@/lib/studyConfig'; + +interface ScreenSizeCheckProps { + children: React.ReactNode; +} + +export default function ScreenSizeCheck({ children }: ScreenSizeCheckProps) { + const [isValid, setIsValid] = useState<boolean | null>(null); + const [errorMessage, setErrorMessage] = useState(''); + + useEffect(() => { + const check = checkScreenSize(); + setIsValid(check.valid); + if (!check.valid) { + setErrorMessage( + check.reason || + 'Your screen size is not compatible with this study.' + ); + } + }, []); + + if (isValid === null) { + return <div className="p-4">Checking system requirements...</div>; + } + + if (!isValid) { + return ( + <div className="flex items-center justify-center min-h-screen bg-gray-100"> + <div className="bg-white p-8 rounded-lg shadow-lg max-w-md"> + <h2 className="text-2xl font-bold text-red-600 mb-4"> + System Requirements Not Met + </h2> + <p className="text-gray-700 mb-6">{errorMessage}</p> + <div className="bg-blue-50 p-4 rounded mb-6"> + <p className="text-sm font-semibold mb-2"> + Minimum requirements: + </p> + <ul className="text-sm text-gray-700 space-y-1"> + <li>Screen resolution: {MIN_SCREEN_WIDTH}x{MIN_SCREEN_HEIGHT} or larger</li> + <li>Desktop or laptop computer (no mobile devices)</li> + <li>Modern web browser with JavaScript enabled</li> + </ul> + </div> + <p className="text-sm text-gray-600"> + Please try again with a compatible device or window size. + </p> + </div> + </div> + ); + } + + return <>{children}</>; +} diff --git a/experiment/components/study/StartTaskPage.tsx b/experiment/components/study/StartTaskPage.tsx new file mode 100644 index 00000000..dccfe384 --- /dev/null +++ b/experiment/components/study/StartTaskPage.tsx @@ -0,0 +1,91 @@ +'use client'; + +import { useState } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { logThenRedirect } from '@/lib/logging'; +import { getNextPage, getScenario } from '@/lib/studyConfig'; + +export default function StartTaskPage() { + const searchParams = useSearchParams(); + const username = searchParams.get('username') || ''; + const scenarioId = searchParams.get('scenario') || undefined; + const scenario = getScenario(scenarioId); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleStartTask = async () => { + if (isSubmitting) return; + setIsSubmitting(true); + const params = new URLSearchParams(searchParams.toString()); + params.set('page', getNextPage('start-task')!); + const nextUrl = `/study?${params.toString()}`; + + await logThenRedirect( + { + username, + event: 'taskStart', + }, + nextUrl + ); + }; + + return ( + <div className="max-w-2xl mx-auto p-8"> + <h1 className="text-3xl font-bold mb-6">{scenario.taskInstructions.title}</h1> + + <div className="bg-blue-50 p-6 rounded-lg mb-8 space-y-4 text-gray-800"> + <h2 className="text-xl font-semibold">Task Instructions</h2> + + <div> + <h3 className="font-semibold mb-2">Scenario:</h3> + <p> + {scenario.taskInstructions.description} + </p> + <p className="mt-2"> + {scenario.colleague.firstName} is available via chat to answer questions about the details of the situation. + </p> + </div> + + <div> + <h3 className="font-semibold mb-2">What to do:</h3> + <ol className="list-decimal list-inside space-y-1"> + <li>Review {scenario.colleague.firstName}'s messages. The initial message will not give you all the details you need, so <b>you will need to ask follow-up questions</b>.</li> + <li> + Compose your email response in the text area provided + </li> + <li> + Depending on your condition, you may see AI-generated suggestions that may or may not be helpful. Feel free to use helpful suggestions, edit them, or ignore them entirely. + </li> + <li> + When you're satisfied with your response, click the + “Send” button + </li> + </ol> + </div> + + <div> + <h3 className="font-semibold mb-2">Keep in mind:</h3> + <ul className="list-disc list-inside space-y-1"> + <li> + {scenario.taskInstructions.companyFraming} + </li> + <li>There's no “perfect” response - write naturally</li> + <li>Take as much time as you need</li> + <li> + If you encounter any issues, please note them in the final survey + </li> + <li>For practical purposes, an AI will be playing the role of {scenario.colleague.firstName}. But treat them as if they were from a real person.</li> + </ul> + </div> + </div> + + <button + type="button" + onClick={handleStartTask} + disabled={isSubmitting} + className="w-full px-6 py-3 bg-green-600 text-white rounded-lg font-semibold hover:bg-green-700 transition disabled:bg-green-400 disabled:cursor-not-allowed" + > + {isSubmitting ? 'Loading...' : 'Start Writing Task'} + </button> + </div> + ); +} diff --git a/experiment/components/study/TaskPage.tsx b/experiment/components/study/TaskPage.tsx new file mode 100644 index 00000000..bf168c60 --- /dev/null +++ b/experiment/components/study/TaskPage.tsx @@ -0,0 +1,156 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; +import type { WritingAreaRef } from '@/components/WritingArea'; +import type { TextEditorState } from '@/types'; +import AIPanel from '@/components/AIPanel'; +import ChatPanel from '@/components/ChatPanel'; +import WritingArea from '@/components/WritingArea'; +import { log } from '@/lib/logging'; +import { letterToCondition, getScenario } from '@/lib/studyConfig'; + +export default function TaskPage() { + const searchParams = useSearchParams(); + const writingAreaRef = useRef<WritingAreaRef>(null); + const username = searchParams.get('username') || ''; + const conditionCode = (searchParams.get('condition') || 'n') as keyof typeof letterToCondition; // TODO: don't default! + const condition = letterToCondition[conditionCode]; + const scenarioId = searchParams.get('scenario') || undefined; + const scenario = getScenario(scenarioId); + + // Collapsible chat state + const [isChatOpen, setIsChatOpen] = useState(false); + const [hasUnread, setHasUnread] = useState(false); + + // Auto-expand chat after a short delay so participants see the colleague's messages + useEffect(() => { + const delayTime = 1000; // 1 second + const timer = setTimeout(() => { + setIsChatOpen(true); + }, delayTime); + + return () => clearTimeout(timer); + }, []); + + const handleSendTask = async (content: string) => { + // Log task completion + await log({ + username, + event: 'taskComplete', + extra_data: { + finalText: content, + wordCount: content.split(/\s+/).length, + documentLength: content.length, + }, + }); + + // Navigate to post-task survey + const params = new URLSearchParams(searchParams.toString()); + params.set('page', 'post-task-survey'); + window.location.href = `/study?${params.toString()}`; + }; + + const handleDocumentUpdate = async (editorState: TextEditorState) => { + const fullContent = editorState.beforeCursor + editorState.selectedText + editorState.afterCursor; + await log({ + username, + event: 'documentUpdate', + extra_data: { + editorState, + wordCount: fullContent.split(/\s+/).length, + documentLength: fullContent.length, + }, + }); + }; + + return ( + <div className="flex h-screen gap-5 p-10 bg-gray-100 overflow-hidden relative"> + {/* Left side - Writing Area with floating chat */} + <div className="relative flex-1 flex flex-col min-w-0"> + <WritingArea + ref={writingAreaRef} + onSend={handleSendTask} + onUpdate={handleDocumentUpdate} + showSendButton={true} + /> + + {/* Collapsible Chat Window - floating over WritingArea */} + <div className="absolute bottom-0 right-8 z-50 flex flex-col items-end pointer-events-none"> + <div + className={`rounded-t-lg bg-white border border-gray-300 pointer-events-auto flex flex-col transition-all duration-300 ease-in-out ${ + isChatOpen ? 'w-[450px] h-[650px]' : 'w-[350px] h-12' + }`} + > + {/* Window Header (Tab) */} + <div + className={`h-12 flex items-center justify-between px-4 select-none transition-colors rounded-t-lg ${ + isChatOpen ? 'bg-gray-50 border-b border-gray-300' : 'bg-white' + }`} + > + {isChatOpen ? ( + <> + <div className="flex items-center gap-2 font-semibold text-gray-700 text-sm"> + <span>Chat with {scenario.colleague.firstName}</span> + </div> + <button + type="button" + onClick={() => setIsChatOpen(false)} + className="p-1 hover:bg-gray-200 rounded cursor-pointer text-gray-500" + aria-label="Minimize chat" + > + <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"> + <line x1="5" y1="12" x2="19" y2="12" /> + </svg> + </button> + </> + ) : ( + <button + type="button" + onClick={() => { + setHasUnread(false); + setIsChatOpen(true); + }} + className="flex-1 flex items-center justify-between hover:bg-gray-50 -mx-4 px-4 h-full cursor-pointer rounded-t-lg" + aria-label={`Open chat with ${scenario.colleague.firstName}`} + > + <div className="flex items-center gap-2 font-semibold text-gray-700 text-sm"> + <span>Chat with {scenario.colleague.firstName}</span> + {hasUnread && ( + <span className="flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-[10px] font-bold text-white shadow-sm"> + 1 + </span> + )} + </div> + <div className="flex items-center gap-1 text-gray-500"> + <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"> + <polyline points="18 15 12 9 6 15" /> + </svg> + </div> + </button> + )} + </div> + + {/* Window Body */} + <div className={`flex-1 overflow-hidden bg-white ${isChatOpen ? 'block' : 'hidden'}`}> + <ChatPanel + onNewMessage={() => { + if (!isChatOpen) { + setHasUnread(true); + } + }} + /> + </div> + </div> + </div> + </div> + + {/* Right side - AI Panel in sidebar (only for non-no_ai conditions) */} + {condition !== 'no_ai' && ( + <div className="flex flex-col gap-2.5 w-110 border border-gray-300 rounded overflow-hidden shadow-sm bg-white"> + <AIPanel writingAreaRef={writingAreaRef} isStudyMode={true} /> + </div> + )} + </div> + ); +} diff --git a/experiment/components/survey/ControlledInput.tsx b/experiment/components/survey/ControlledInput.tsx new file mode 100644 index 00000000..2bb0ba79 --- /dev/null +++ b/experiment/components/survey/ControlledInput.tsx @@ -0,0 +1,109 @@ +'use client'; + +import { useAtom } from 'jotai'; +import { surveyInputAtom } from '@/contexts/StudyContext'; +import { QuestionType } from './types'; + +interface ControlledInputProps { + questionId: string; + type: QuestionType; + placeholder?: string; + options?: string[]; + label?: string; + required?: boolean; + multiline?: boolean; +} + +export default function ControlledInput({ + questionId, + type, + placeholder, + options = [], + label, + required = false, + multiline = true, +}: ControlledInputProps) { + const [inputs, setInputs] = useAtom(surveyInputAtom); + const value = inputs[questionId] ?? ''; + + const handleChange = (newValue: unknown) => { + setInputs((prev) => ({ + ...prev, + [questionId]: newValue, + })); + }; + + if (type === 'text') { + const baseClassName = "w-full p-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"; + + if (multiline) { + return ( + <textarea + value={String(value)} + onChange={(e) => handleChange(e.target.value)} + placeholder={placeholder} + required={required} + rows={3} + className={baseClassName} + /> + ); + } + + return ( + <input + type="text" + value={String(value)} + onChange={(e) => handleChange(e.target.value)} + placeholder={placeholder} + required={required} + className={baseClassName} + /> + ); + } + + if (type === 'likert' || type === 'radio') { + return ( + <fieldset className="space-y-2"> + {options.map((option) => ( + <label key={option} className="flex items-center gap-2"> + <input + type="radio" + name={questionId} + value={option} + checked={value === option} + onChange={(e) => handleChange(e.target.value)} + required={required} + /> + {option} + </label> + ))} + </fieldset> + ); + } + + if (type === 'checkbox') { + const checked = Array.isArray(value) ? value : []; + return ( + <fieldset className="space-y-2"> + {options.map((option) => ( + <label key={option} className="flex items-center gap-2"> + <input + type="checkbox" + value={option} + checked={checked.includes(option)} + onChange={(e) => { + const newChecked = e.target.checked + ? [...checked, option] + : checked.filter((item) => item !== option); + handleChange(newChecked); + }} + /> + {option} + </label> + ))} + </fieldset> + ); + } + + return null; +} diff --git a/experiment/components/survey/Survey.tsx b/experiment/components/survey/Survey.tsx new file mode 100644 index 00000000..d19f6bee --- /dev/null +++ b/experiment/components/survey/Survey.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { type ReactNode, useState } from 'react'; +import { Question } from './types'; +import SurveyQuestion from './SurveyQuestion'; + +interface SurveyProps { + title?: string; + description?: string; + questions: Question[]; + onSubmit: () => Promise<void>; + submitButtonText?: string; + children?: ReactNode; +} + +export default function Survey({ + title, + description, + questions, + onSubmit, + submitButtonText = 'Submit', + children, +}: SurveyProps) { + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { + e.preventDefault(); + if (isSubmitting) return; + setIsSubmitting(true); + await onSubmit(); + }; + + return ( + <form onSubmit={handleSubmit} className="max-w-2xl"> + {title && <h2 className="text-2xl font-bold mb-4">{title}</h2>} + {description && <p className="text-gray-700 mb-6">{description}</p>} + + {children && <div className="mb-6">{children}</div>} + + <div className="space-y-6"> + {questions.map((question) => ( + <SurveyQuestion key={question.id} question={question} /> + ))} + </div> + + <button + type="submit" + disabled={isSubmitting} + className="mt-8 px-6 py-2 bg-blue-600 text-white rounded font-medium hover:bg-blue-700 transition disabled:bg-blue-400 disabled:cursor-not-allowed" + > + {isSubmitting ? 'Submitting...' : submitButtonText} + </button> + </form> + ); +} diff --git a/experiment/components/survey/SurveyQuestion.tsx b/experiment/components/survey/SurveyQuestion.tsx new file mode 100644 index 00000000..0ec7bea5 --- /dev/null +++ b/experiment/components/survey/SurveyQuestion.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { Question } from './types'; +import ControlledInput from './ControlledInput'; + +interface SurveyQuestionProps { + question: Question; +} + +export default function SurveyQuestion({ question }: SurveyQuestionProps) { + return ( + <div className="mb-6"> + <label className="block text-base font-medium mb-3"> + {question.text} + {question.required && <span className="text-red-500 ml-1">*</span>} + </label> + <ControlledInput + questionId={question.id} + type={question.type} + placeholder={question.placeholder} + options={question.options} + required={question.required} + multiline={question.multiline} + /> + </div> + ); +} diff --git a/experiment/components/survey/surveyData.tsx b/experiment/components/survey/surveyData.tsx new file mode 100644 index 00000000..87799c87 --- /dev/null +++ b/experiment/components/survey/surveyData.tsx @@ -0,0 +1,211 @@ +import { Question, likert, effortLikert } from './types'; +import { ConditionName } from '@/types/study'; + +/** + * Intro survey questions (demographics and experience) + */ +export const introSurveyQuestions: Question[] = [ + { + id: 'age', + text: 'What is your age?', + type: 'text', + placeholder: 'Enter your age', + required: true, + multiline: false, + }, + { + id: 'gender', + text: 'What is your gender?', + type: 'radio', + options: ['Male', 'Female', 'Non-binary', 'Prefer to self-describe', 'Prefer not to answer'], + required: true, + }, + { + id: 'english_proficiency', + text: 'English Proficiency', + type: 'radio', + options: ['Native', 'Fluent', 'Intermediate', 'Basic'], + required: true, + }, + { + id: 'chatbot_familiarity', + text: 'How familiar are you with chatbots or AI assistants (e.g., ChatGPT, Claude)?', + type: 'radio', + options: ['Very unfamiliar', 'Unfamiliar', 'Neutral', 'Familiar', 'Very familiar'], + required: true, + }, + { + id: 'ai_writing_tools', + text: 'Have you used AI writing tools before (e.g., ChatGPT for writing, Grammarly AI)?', + type: 'radio', + options: ['Never', 'Rarely', 'Sometimes', 'Often', 'Very often'], + required: true, + }, +]; + +/** + * Common post-task survey questions (used by all conditions) + */ +export const postTaskCommonQuestions: Question[] = [ + { + id: 'tlx_mental_demand', + text: 'How much mental effort was required to complete the task?', + type: 'radio', + options: effortLikert(), + required: true, + }, + { + id: 'tlx_temporal_demand', + text: 'How much time pressure did you feel while completing the task?', + type: 'radio', + options: effortLikert(), + required: true, + }, + { + id: 'tlx_performance', + text: 'How well do you think you performed on the task?', + type: 'radio', + options: ['Very poor', 'Poor', 'Fair', 'Good', 'Excellent'], + required: true, + }, + { + id: 'tlx_physical_demand', + text: 'How physically demanding was the task?', + type: 'radio', + options: effortLikert(), + required: true, + }, + { + id: 'tlx_effort', + text: 'How hard did you have to work to accomplish your level of performance?', + type: 'radio', + options: effortLikert(), + required: true, + }, + { + id: 'tlx_frustration', + text: 'How insecure, discouraged, irritated, stressed, and annoyed were you?', + type: 'radio', + options: effortLikert(), + required: true, + }, + { + id: 'other_tools_used', + text: 'What other tools did you use during the writing task? (Select all that apply)', + type: 'checkbox', + options: [ + 'Autocomplete (built-in to browser or OS)', + 'Grammarly or similar grammar checker', + 'ChatGPT or other AI', + 'Dictionary or thesaurus', + 'None', + 'Other (please specify in the next question)', + ], + required: false, + }, + { + id: 'technical_difficulties', + text: 'Did you experience any technical difficulties during the task?', + type: 'text', + placeholder: 'Describe any issues encountered', + required: false, + }, +]; + +/** + * AI-specific questions (for non-no_ai conditions) + */ +export const postTaskAIQuestions: Question[] = [ + { + id: 'ai_decision_timing', + text: <>When did you decide <b>not</b> to use an AI suggestion?</>, + type: 'text', + placeholder: 'Describe when and why you decided not to use a suggestion', + required: false, + }, + { + id: 'ai_ease_understand', + text: 'The AI suggestions were easy to understand', + type: 'radio', + options: likert(), + required: true, + }, + { + id: 'ai_helpful', + text: 'The AI suggestions were helpful', + type: 'radio', + options: likert(), + required: true, + }, + { + id: 'ai_felt_pressured', + text: 'I felt pressured to use the AI suggestions', + type: 'radio', + options: likert(), + required: true, + }, + { + id: 'ai_think_carefully', + text: 'I had to think carefully about when to use the AI suggestions', + type: 'radio', + options: likert(), + required: true, + }, + { + id: 'ai_describe', + text: 'In your own words, describe the AI-generated text and how you used it', + type: 'text', + placeholder: 'Describe your experience with the AI text', + required: false, + }, +]; + +/** + * Condition-specific debrief sections + */ +export const conditionDebriefs: Record< + string, + { title: string; content: string } +> = { + no_ai: { + title: 'Thank You', + content: + 'Thank you for completing the writing task without AI assistance. Your perspective on how humans approach writing is valuable.', + }, + complete_document: { + title: 'About the AI Draft', + content: + 'In this condition, the AI system provided complete draft emails. The information in these drafts may or may not have been consistent with the true context from the chat conversation. Please reflect on how you used these AI-generated drafts in your writing.', + }, + example_sentences: { + title: 'About the AI Suggestions', + content: + 'In this condition, the AI system provided example sentences as suggestions. The information in these suggestions may or may not have been consistent with the true context from the chat conversation. Please reflect on how you used these AI-generated suggestions in your writing.', + }, + analysis_readerPerspective: { + title: 'About the AI Analysis', + content: + 'In this condition, the AI system provided analysis from a reader perspective. Please reflect on how this feedback influenced your writing process.', + }, + proposal_advice: { + title: 'About the AI Advice', + content: + 'In this condition, the AI system provided writing advice and suggestions. Please reflect on how this advice influenced your writing process.', + }, +}; + +/** + * Get post-task survey questions for a condition + */ +export function getPostTaskSurveyQuestions( + condition: ConditionName +): Question[] { + const commonQuestions = [...postTaskCommonQuestions]; + + // Add AI-specific questions for all conditions except no_ai + if (condition !== 'no_ai') { + return [...commonQuestions, ...postTaskAIQuestions]; + } + + return commonQuestions; +} diff --git a/experiment/components/survey/types.ts b/experiment/components/survey/types.ts new file mode 100644 index 00000000..c22c47cb --- /dev/null +++ b/experiment/components/survey/types.ts @@ -0,0 +1,46 @@ +import type { JSX } from "react"; + +export type QuestionType = 'text' | 'likert' | 'radio' | 'checkbox'; + +export interface Question { + id: string; + text: string | JSX.Element; + type: QuestionType; + required?: boolean; + options?: string[]; // For likert, radio, checkbox + placeholder?: string; // For text inputs + multiline?: boolean; // For text inputs: false = single-line input, true/undefined = textarea +} + +/** + * Standard 5-point Likert scale + */ +export const likert = (): string[] => [ + 'Strongly Disagree', + 'Disagree', + 'Neutral', + 'Agree', + 'Strongly Agree', +]; + +/** + * Agreement scale (for questions phrased as "I agree that...") + */ +export const agreeLikert = (): string[] => [ + 'Strongly Disagree', + 'Disagree', + 'Neutral', + 'Agree', + 'Strongly Agree', +]; + +/** + * Effort scale (for Task Load Index questions) + */ +export const effortLikert = (): string[] => [ + 'Very Low', + 'Low', + 'Medium', + 'High', + 'Very High', +]; diff --git a/experiment/consent-form.md b/experiment/consent-form.md new file mode 100644 index 00000000..03f1d729 --- /dev/null +++ b/experiment/consent-form.md @@ -0,0 +1,57 @@ +**Study title**: AI for Supporting Independent Writing + +**Researchers**: Kenneth C. Arnold, Ph.D., Jooha (Hannah) Yoo, Alina Sainju, Jiho Kim, Jason Chew + +**1. Introduction** + +We invite you to participate in a study exploring the design of AI writing assistants. Participation in the study is entirely voluntary. If you have any questions or concerns, please feel free to discuss them with the primary investigator, Kenneth C. Arnold, Ph.D. (see Section 10). If you decide to take part, you will sign this form to signify your consent to participate in the study as outlined below. + +**2. Purpose of the Study** + +The purpose of this study is to investigate how AI systems might facilitate independent writing. + +**3. Procedures** + +Your participation entails performing a writing task using a system that offers AI-generated suggestions. The study itself will be conducted asynchronously via a website accessed through Prolific. Please see Section 7 for more detail on how your data will be handled. + +**4. Time Duration** + +Participating in this study will take 20-30 minutes. + +**5. Discomforts and Risks** + +We do not anticipate any discomforts or risks from participating in this study. + +**6. Potential Benefits** + +Your participation in this study may benefit future users of our software and contribute to scientific knowledge about how AI can influence the process of writing. + +**7. Statement of Confidentiality** + +We will be collecting several types of data from your participation in this study, including records of how you interact with the AI writing tool and samples of your writing. Selections from your writing, along with AI-generated outputs, will be logged and stored on a secure server accessible only to our research team. + +Please be aware that during your participation in this study, portions of your writing will be sent to third-party service providers such as OpenAI, Google, or Anthropic to request outputs from their AI services. We require that service providers guarantee confidentiality of the data sent to them and that they promise not to use that data to train their AI models or otherwise improve their services. + +The data you provide will be stored in a secure storage location on our servers, Microsoft cloud services including SharePoint, and on a qualitative analysis tool such as Delve. These platforms enable our research team to safely store, organize, share, and access the data in a private and collaborative environment, accessible only to the investigators. Your data will be stored in password-protected files and services accessible only to the researchers. In the unlikely event of a technical problem with the files, the researchers may grant software technical support staff temporary access to the files. In such cases, technical support staff will access the files solely to resolve the technical problem. Once issues are resolved, the researchers will revoke the technical support staff’s access to the files. + +Results of this study will be shared in academic publications and presentations. These results may include quotes from participants and samples of the system’s input and output, but we will ensure that quotes do not identify you. + +**8. Compensation for Participation** + +Participants will receive $5 for their time completing this study. + +**9. Voluntary Participation** + +Your participation is entirely voluntary. You may decline to participate without penalty, withdraw at any time without penalty, and request deletion of your data. + +**10. Contact Information of the Primary Investigator** + +If you have questions about this research or if you think you have been harmed by participating, you may contact Professor Kenneth C. Arnold, Department of Computer Science, Calvin University, by email at ken.arnold@calvin.edu, by phone at 616-526-8723, or by mail at 3201 Burton St SE, Grand Rapids, MI 49546. + +**11. Institutional Review Board Approval** + +The Calvin University Institutional Review Board has approved this study. If you have questions about your rights as a research participant, or wish to obtain information, ask questions or discuss any concerns about this study with someone other than the researcher(s), please contact the Calvin University Institutional Review Board Committee at Office of the Provost, Calvin University, 3201 Burton St SE, Grand Rapids, MI 49546, irb@calvin.edu. + +**12. Written Consent** + +By checking the box below, I am giving my informed consent to participate in the study described above. I understand that my participation is voluntary, and I am free to withdraw at any time, without any reason, and without any penalty or loss of benefits to which I am otherwise entitled. \ No newline at end of file diff --git a/experiment/contexts/StudyContext.tsx b/experiment/contexts/StudyContext.tsx new file mode 100644 index 00000000..87397621 --- /dev/null +++ b/experiment/contexts/StudyContext.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { atom } from 'jotai'; +import type { StudyParams, ConditionName } from '@/types/study'; +import { letterToCondition, DEFAULT_AUTO_REFRESH_INTERVAL } from '@/lib/studyConfig'; + +/** + * Study parameters from URL (username, condition, page, etc.) + */ +export const studyParamsAtom = atom<StudyParams>({ + username: '', + condition: 'n', + page: 'consent', + autoRefreshInterval: DEFAULT_AUTO_REFRESH_INTERVAL, + experiment: 'type', + isProlific: true, + scenario: 'roomDoubleBooking', +}); + +/** + * Derived condition name from condition code + */ +export const studyConditionAtom = atom<ConditionName>((get) => { + const params = get(studyParamsAtom); + return letterToCondition[params.condition]; +}); + +/** + * Survey form state - shared across all surveys + */ +export const surveyInputAtom = atom<Record<string, unknown>>({}); + +/** + * Helper to update study params + */ +export const updateStudyParamsAtom = atom( + null, + (get, set, params: Partial<StudyParams>) => { + const current = get(studyParamsAtom); + set(studyParamsAtom, { ...current, ...params }); + } +); diff --git a/experiment/docs/research-overview.md b/experiment/docs/research-overview.md new file mode 100644 index 00000000..dfd0f661 --- /dev/null +++ b/experiment/docs/research-overview.md @@ -0,0 +1,146 @@ +# Measuring Over-Reliance on AI Writing Assistants Through Information-Seeking Behavior + +## Abstract + +[TBD - summarize findings] + +## 1. Introduction + +AI writing assistants increasingly offer real-time suggestions as people compose text. While these tools can improve efficiency, they may also induce "premature closure"β€”users accept plausible-sounding output without fully thinking through what a situation requires. This is particularly concerning for consequential communications where understanding context, considering the recipient's perspective, and anticipating downstream effects are essential to producing effective text. + +We propose a novel experimental method for measuring over-reliance: observing information-seeking behavior during a realistic writing task. Participants compose workplace emails while having access to a colleague (simulated via LLM) who can provide relevant context. By measuring what questions participants askβ€”and don't askβ€”we gain a window into their cognitive engagement beyond what the final text reveals. + +### 1.1 The Problem of Premature Closure + +When an AI suggests text that "sounds right," users face reduced incentive to think through: + +- What information they actually need +- How the recipient will perceive the message +- What consequences might follow from different framings + +This is distinct from simple automation bias (accepting AI output as correct). Premature closure means the AI short-circuits the *thinking process itself*β€”users don't realize what they failed to consider. + +### 1.2 Research Questions + +1. **RQ1**: Do AI writing suggestions reduce information-seeking behavior compared to unassisted writing? +2. **RQ2**: Does reduced information-seeking correlate with lower-quality outcomes (emails that fail to address the situation appropriately)? +3. **RQ3**: Can AI suggestions be designed to support thinking rather than supplant it? + +## 2. Related Work + +### 2.1 AI Writing Assistants + +[Review of existing tools and their effects on writing] + +### 2.2 Automation Bias and Over-Reliance + +[Literature on over-reliance in AI-assisted decision making] + +### 2.3 Cognitive Offloading + +[Research on how external tools affect cognitive processes] + +### 2.4 Measuring Thought Processes + +[Methods for studying cognition during writing: think-aloud, keystroke logging, etc.] + +## 3. Method + +### 3.1 Experimental Design + +Participants complete a workplace email writing task with: + +- **Information source**: A colleague available via chat who has relevant context +- **Manipulation**: Presence/type of AI writing suggestions (Study 1: with/without; Study 2: suggestion type variations) + +### 3.2 The Colleague Chat as Measurement Instrument + +The colleague (simulated via LLM with specific behavioral constraints) serves dual purposes: + +1. **Ecological validity**: Real workplace writing often requires gathering information from others +2. **Process measurement**: Questions asked reveal what participants thought to think about + +Critical design constraint: The colleague must be *reactive*, not proactive. They answer questions when asked but do not volunteer information. This ensures that information gathered reflects participant initiative. + +#### Colleague Behavioral Constraints + +- Responds only to direct questions +- Provides factual information but refuses to draft email text +- Maintains realistic persona (busy, texting briefly) +- Does not anticipate user needs or "helpfully" dump relevant context + +### 3.3 Scenarios + +Two scenarios requiring participants to deliver unwelcome news to an external party: + +**Scenario A: Room Double-Booking** + +- Context: Panel event scheduling conflict +- Recipient: Client/influencer who must be moved to different room/time +- Key information to gather: new room, new time, reason for conflict, what to offer the client + +**Scenario B: Demo Rescheduling** + +- Context: Critical bug discovered before important product demo +- Recipient: VP at potential client company +- Key information to gather: reschedule options, nature of issue (how much to disclose), timeline + +### 3.4 Measures + +**Process measures:** + +- Number of questions asked to colleague +- Types of information sought (logistical, relational, strategic) +- Time spent in chat vs. composing + +**Outcome measures:** + +- Email completeness (does it contain necessary information?) +- Appropriateness (tone, acknowledgment of inconvenience, recipient consideration) +- Problem-solving quality (does it actually address the situation vs. just sound professional?) + +Outcome coding via rubric + independent raters (or LLM-assisted with human validation). + +### 3.5 Conditions + +**Study 1: AI Presence** + +- No-AI: No AI suggestions available +- Complete Document: the AI suggests a completed document (including both filling in missing text and correcting text as needed) +- Sentences only: The AI suggests sentence completions (or next sentences, if the current sentence is already complete) + +**Study 2: Suggestion Type** (if Study 1 shows effect) + +- Sentences only +- Advice / coaching +- Anticipated readers' reactions + +Hypothesis: Suggestions that don't "give the answer" may support thinking rather than supplanting it. + +## 4. Pilot Findings + +[Summarize pilot observations] + +Initial pilot revealed low engagement with colleague chat even when participants were explicitly told they'd need to ask questions. This led to redesigning: + +- Initial messages now end with a question to the participant (inverting the dynamic) +- Colleague persona emphasizes being busy/reactive rather than helpful +- Instructions reinforced but scenario structure now *requires* questions to get key info + +## 5. Results + +[TBD] + +## 6. Discussion + +### 6.1 Implications for AI Writing Tool Design + +### 6.2 Information-Seeking as a Measure of Cognitive Engagement + +### 6.3 Limitations + +### 6.4 Future Work + +## 7. Conclusion + +## References diff --git a/experiment/eslint.config.mjs b/experiment/eslint.config.mjs new file mode 100644 index 00000000..05e726d1 --- /dev/null +++ b/experiment/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/experiment/lib/browserMetadata.ts b/experiment/lib/browserMetadata.ts new file mode 100644 index 00000000..b218aaad --- /dev/null +++ b/experiment/lib/browserMetadata.ts @@ -0,0 +1,57 @@ +import { BrowserMetadata } from '@/types/study'; +import { MIN_SCREEN_HEIGHT, MIN_SCREEN_WIDTH } from './studyConfig'; + +/** + * Collect browser and device metadata + */ +export function getBrowserMetadata(): BrowserMetadata { + if (typeof window === 'undefined') { + throw new Error('getBrowserMetadata must be called in browser context'); + } + + return { + userAgent: navigator.userAgent, + screenWidth: window.screen.width, + screenHeight: window.screen.height, + windowWidth: window.innerWidth, + windowHeight: window.innerHeight, + colorDepth: window.screen.colorDepth, + pixelDepth: window.screen.pixelDepth, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + languages: Array.from(navigator.languages || []), + platform: navigator.platform, + cookieEnabled: navigator.cookieEnabled, + onLine: navigator.onLine, + }; +} + +/** + * Check if screen size is valid for the study + */ +export function checkScreenSize(): { valid: boolean; reason?: string } { + if (typeof window === 'undefined') { + return { valid: false, reason: 'Not in browser context' }; + } + + const width = window.innerWidth; + const height = window.innerHeight; + + if (width < MIN_SCREEN_WIDTH || height < MIN_SCREEN_HEIGHT) { + return { + valid: false, + reason: `Screen size ${width}x${height} is below minimum ${MIN_SCREEN_WIDTH}x${MIN_SCREEN_HEIGHT}`, + }; + } + + // Check for mobile user agents + const isMobile = + /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + navigator.userAgent + ); + + if (isMobile) { + return { valid: false, reason: 'Mobile devices are not supported' }; + } + + return { valid: true }; +} diff --git a/experiment/lib/eval/colleagueEval.ts b/experiment/lib/eval/colleagueEval.ts new file mode 100644 index 00000000..07509e61 --- /dev/null +++ b/experiment/lib/eval/colleagueEval.ts @@ -0,0 +1,130 @@ +import { openai } from '@ai-sdk/openai'; +import { generateText } from 'ai'; + +export interface EvalCriterion { + name: string; + prompt: string; + passCondition: 'yes' | 'no'; +} + +export interface EvalResult { + criterion: string; + pass: boolean; + answer: 'yes' | 'no'; + reasoning: string; +} + +export const EVAL_CRITERIA: Record<string, EvalCriterion> = { + noProactiveDump: { + name: 'No proactive info dump', + prompt: + 'Did the colleague volunteer specific logistical details (room numbers, times, dates, specific availability) without being directly asked for them?', + passCondition: 'no', + }, + refusesToDraft: { + name: 'Refuses to write email', + prompt: 'Did the colleague refuse or decline to write or draft the email for the user?', + passCondition: 'yes', + }, + answersWhenAsked: { + name: 'Answers direct questions', + prompt: 'Did the colleague provide the specific information that was directly asked for?', + passCondition: 'yes', + }, + noDraftLanguage: { + name: 'No draft language', + prompt: + "Did the colleague provide ready-to-use email text like 'Dear X, ...' or 'You could say: ...' or similar draft wording?", + passCondition: 'no', + }, + staysInCharacter: { + name: 'Stays in character', + prompt: + 'Did the colleague maintain a realistic workplace persona (brief messages, casual tone, appropriate for chat)?', + passCondition: 'yes', + }, +}; + +export async function evalColleagueResponse( + testInput: string, + colleagueResponse: string, + criterion: EvalCriterion, + model: string = 'gpt-4o-mini' +): Promise<EvalResult> { + const judgePrompt = `You are evaluating an AI colleague's response in a workplace chat scenario. + +User message: "${testInput}" +Colleague response: "${colleagueResponse}" + +Question: ${criterion.prompt} + +Think step by step, then answer with ONLY a JSON object (no markdown, no code blocks): +{"answer": "yes" or "no", "reasoning": "brief 1-sentence explanation"}`; + + const result = await generateText({ + model: openai(model), + prompt: judgePrompt, + maxOutputTokens: 150, + }); + + try { + const parsed = JSON.parse(result.text.trim()); + return { + criterion: criterion.name, + pass: parsed.answer === criterion.passCondition, + answer: parsed.answer, + reasoning: parsed.reasoning, + }; + } catch { + return { + criterion: criterion.name, + pass: false, + answer: 'no', + reasoning: `Failed to parse judge response: ${result.text}`, + }; + } +} + +export interface TestCase { + name: string; + input: string; + criteria: (keyof typeof EVAL_CRITERIA)[]; +} + +export const TEST_CASES: TestCase[] = [ + { + name: 'Passive acknowledgment', + input: 'Ok, got it', + criteria: ['noProactiveDump'], + }, + { + name: 'Simple acknowledgment', + input: 'Thanks', + criteria: ['noProactiveDump'], + }, + { + name: 'Direct room question', + input: 'What room should I tell him?', + criteria: ['answersWhenAsked', 'noDraftLanguage'], + }, + { + name: 'Direct time question', + input: 'What time works?', + criteria: ['answersWhenAsked', 'noDraftLanguage'], + }, + { + name: 'Request to write email', + input: 'Can you write the email for me?', + criteria: ['refusesToDraft'], + }, + { + name: 'Request for wording', + input: 'What should I say to him?', + criteria: ['noDraftLanguage', 'staysInCharacter'], + }, + { + name: 'Vague follow-up', + input: 'Anything else I should know?', + criteria: ['noProactiveDump'], + }, +]; diff --git a/experiment/lib/logging.ts b/experiment/lib/logging.ts new file mode 100644 index 00000000..10704135 --- /dev/null +++ b/experiment/lib/logging.ts @@ -0,0 +1,62 @@ +import { GIT_COMMIT, WAVE } from './studyConfig'; +import { LogPayload, LogEntry } from '@/types/study'; + +const MAX_RETRIES = 3; +const INITIAL_DELAY = 100; // ms + +/** + * Enrich payload with metadata and send to server + */ +export async function log(payload: LogPayload): Promise<void> { + const entry: LogEntry = { + ...payload, + timestamp: new Date().toISOString(), + wave: WAVE, + gitCommit: GIT_COMMIT, + }; + + await sendLogWithRetry(entry); +} + +/** + * Log an event and then navigate to a URL + */ +export async function logThenRedirect( + payload: LogPayload, + url: string +): Promise<void> { + try { + await log(payload); + } catch (error) { + console.error('Failed to log before redirect:', error); + // Continue with redirect even if logging fails + } + + window.location.href = url; +} + +/** + * Send log entry to server with retry logic + */ +async function sendLogWithRetry(entry: LogEntry, attempt = 0): Promise<void> { + try { + const response = await fetch('/api/log', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(entry), + keepalive: true, + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + } catch (error) { + if (attempt < MAX_RETRIES - 1) { + const delay = INITIAL_DELAY * Math.pow(2, attempt); + await new Promise((resolve) => setTimeout(resolve, delay)); + return sendLogWithRetry(entry, attempt + 1); + } + + console.error(`Failed to log after ${MAX_RETRIES} attempts:`, error); + } +} diff --git a/experiment/lib/messageTiming.ts b/experiment/lib/messageTiming.ts new file mode 100644 index 00000000..a02cb0a2 --- /dev/null +++ b/experiment/lib/messageTiming.ts @@ -0,0 +1,62 @@ +/** + * Calculate realistic timing for message delays based on message length. + * + * Thinking/Reading: colleague takes time proportional to what the user wrote (40-80 chars/sec) + * Typing: colleague types her response proportional to what she's typing (40-80 chars/sec) + * Both use Β±300ms variation + */ + +const MIN_READING_SPEED = 30; // chars per second +const MAX_READING_SPEED = 50; // chars per second +const READING_VARIATION = 300; // Β±ms + +/** + * Calculate thinking/reading delay based on received message length + * @param messageLength - Length of the received message in characters + * @returns Delay in milliseconds + */ +export function calculateThinkingDelay(messageLength: number): number { + if (messageLength === 0) return 600; // Minimum delay for empty messages + + // Random speed between MIN and MAX chars/sec + const speed = MIN_READING_SPEED + Math.random() * (MAX_READING_SPEED - MIN_READING_SPEED); + + // Calculate base delay: how long to read the message + const baseDelay = (messageLength / speed) * 1000; + + // Add variation: Β±300ms + const variation = (Math.random() - 0.5) * 2 * READING_VARIATION; + + return Math.max(600, baseDelay + variation); // Minimum 600ms +} + +/** + * Calculate typing duration based on response message length + * @param messageLength - Length of the message being typed in characters + * @returns Duration in milliseconds + */ +export function calculateTypingDuration(messageLength: number): number { + if (messageLength === 0) return 600; // Minimum duration + + // Random speed between MIN and MAX chars/sec + const speed = MIN_READING_SPEED + Math.random() * (MAX_READING_SPEED - MIN_READING_SPEED); + + // Calculate base duration: how long to type the message + const baseDuration = (messageLength / speed) * 1000; + + // Add variation: Β±300ms + const variation = (Math.random() - 0.5) * 2 * READING_VARIATION; + + return Math.max(600, baseDuration + variation); // Minimum 600ms +} + +/** + * Calculate delay between multiple messages + * Uses the typing duration of the previous message as the inter-message delay + * @param previousMessageLength - Length of the previous message + * @returns Delay in milliseconds + */ +export function calculateInterMessageDelay(previousMessageLength: number): number { + // Reuse typing duration calculation - messages sent one after another + return calculateTypingDuration(previousMessageLength); +} diff --git a/experiment/lib/scenarios.json b/experiment/lib/scenarios.json new file mode 100644 index 00000000..01fbdf3e --- /dev/null +++ b/experiment/lib/scenarios.json @@ -0,0 +1,78 @@ +{ + "roomDoubleBooking": { + "id": "roomDoubleBooking", + "sender": { + "name": "Alex Johnson" + }, + "colleague": { + "name": "Sarah Martinez", + "firstName": "Sarah", + "role": "Events Coordinator" + }, + "recipient": { + "name": "Jaden Thompson", + "email": "jaden.t@example.com" + }, + "taskInstructions": { + "title": "Writing Task", + "description": "You work as an event coordinator. Your colleague Sarah has messaged you about a scheduling conflict that needs to be resolved. You need to write an email to one of the panelists to address the situation.", + "companyFraming": "You're representing the company in this communication. Consider how your message will reflect on the team." + }, + "chat": { + "initialMessages": [ + "Problem with Jaden's panel tomorrow", + "Room got double-booked. Gotta move him. But gotta keep him happy!", + "I'm on a call, so need you to email him. What info do you need to sort this out?" + ], + "followUpMessage": "I got a sec between calls, what do you need to know?", + "systemPrompt": "You are Sarah Martinez, an Events Coordinator at a mid-sized company. You are currently dealing with a stressful room double-booking situation.\n\nSCENARIO CONTEXT:\n- Tomorrow there's a panel discussion with Jaden Thompson (a social media influencer)\n- The panel was originally scheduled for 1pm in Room 12\n- Room 12 was accidentally double-booked with Sophia Chen (a more famous influencer with 500K followers)\n- Sophia already publicly announced her panel at Room 12 at 1pm to her fans, so you can't move her\n- You need to move Jaden's panel to a different room/time\n- Room 14 is available, but the event before it ends at 1pm (so no setup time if scheduled at 1pm)\n- Room 14 would work fine at 1:30pm\n- Mike Chen handles facilities/room bookings\n- The user is a PR/communications person who needs to email Jaden about the change\n\nYOUR ROLE:\n- Answer questions about the facts of the situation\n- You're busy and stressed, typing quick messages on your phone\n- Keep responses SHORT - usually 1-2 sentences, sometimes just a few words\n- DON'T be proactive (you're busy and distracted); wait for the user to ask\n- You can send multiple short messages in a row if that feels natural\n- You CANNOT and WILL NOT write the email for them or tell them exactly what to say - that's their job\n- You can give them facts, but not draft communications\n- If asked to write/draft anything, politely refuse (you're too busy, or it's their expertise)\n- You can make up reasonable details if needed, but keep them consistent with the scenario\n- Be natural and conversational, use occasional emoji when appropriate\n- Sometimes you might need to check with Mike or look something up - you can say you'll get back to them\n\nRESPONSE FORMAT:\nRespond with a JSON array of messages. Each message is a string. If you want to send multiple messages in quick succession (like someone texting), put them in separate array elements.\n\nExample: [\"1pm same room πŸ˜…\", \"can you email him?\"]\nOr: [\"Room 14 is free\", \"but the event before ends at 1 so no setup time\"]\n\nJust return the JSON array, nothing else." + }, + "analysis": { + "context": "A marketing/events coordinator needs to email a panelist (Jaden Thompson, a social media influencer) about a room double-booking situation.\n\nKEY FACTS:\n- Jaden's panel was originally scheduled for 1pm in Room 12 tomorrow\n- Room 12 was accidentally double-booked with a more famous influencer (Sophia Chen)\n- Sophia already publicly announced her panel, so she can't be moved\n- Jaden's panel needs to be moved to Room 14 at 1:30pm\n- Room 14 would work fine at 1:30pm (event before ends at 1pm, so 1pm wouldn't allow setup time)\n- Mike Chen handles facilities/room bookings\n- The colleague (Sarah Martinez, Events Coordinator) provided these details via chat\n\nRECIPIENT CONTEXT:\n- Jaden Thompson is a paying client / social media influencer\n- He may be frustrated or upset by the last-minute change\n- The company wants to maintain good relations and not lose him as a client", + "keyFacts": [ + "The panel is being moved (not cancelled)", + "New location: Room 14", + "New time: 1:30pm (or acknowledgment of time change)", + "Reason relates to scheduling conflict / double-booking" + ] + } + }, + "demoRescheduling": { + "id": "demoRescheduling", + "sender": { + "name": "Alex Johnson" + }, + "colleague": { + "name": "Marcus Chen", + "firstName": "Marcus", + "role": "Solutions Engineer" + }, + "recipient": { + "name": "Dr. Lisa Patel", + "email": "l.patel@medicore.com" + }, + "taskInstructions": { + "title": "Writing Task", + "description": "You work as a customer success manager. Your colleague Marcus has messaged you about a technical issue that requires rescheduling an important product demo. You need to write an email to the client to address the situation.", + "companyFraming": "You're representing the company in this communication. Consider how your message will reflect on our professionalism and reliability." + }, + "chat": { + "initialMessages": [ + "Hey, we have a problem with tomorrow's MediCore demo πŸ˜“", + "Found a critical bug in the reporting module this morning. Can't show it like this to a VP.", + "Can you email Dr. Patel and reschedule? Need to keep her confident in us. What info do you need?" + ], + "followUpMessage": "still here if you need anything", + "systemPrompt": "You are Marcus Chen, a Solutions Engineer at a B2B SaaS company. You've discovered a critical bug right before an important product demo.\n\nSCENARIO CONTEXT:\n- Tomorrow (Tuesday) at 2pm you have a scheduled product demo with Dr. Lisa Patel, VP of IT at MediCore Health (a potential major client)\n- This morning you discovered a critical bug in the reporting module that causes incorrect data aggregation\n- The bug makes the product look unreliable and unprofessional - you absolutely cannot demo it in this state\n- Your engineering team needs 3-4 business days to fix and test it properly\n- Thursday afternoon and Friday morning next week are your available slots (you can check your calendar for exact times if asked)\n- This is the second meeting with MediCore - the first was an intro call last week where Dr. Patel expressed strong interest\n- The user is a customer success manager who handles client communications\n- Dr. Patel seems professional but busy - she mentioned having a tight timeline for vendor selection\n\nYOUR ROLE:\n- Answer questions about the technical issue and rescheduling options\n- You're concerned about maintaining client confidence but honest about technical issues\n- Keep responses SHORT - usually 1-2 sentences, sometimes just a few words\n- DON'T be proactive (you're juggling this with other fires); wait for the user to ask\n- You can send multiple short messages in a row if that feels natural\n- You CANNOT and WILL NOT write the email for them or tell them exactly what to say - that's their job\n- You can give them facts about the bug, timeline, and available slots, but not draft communications\n- If asked to write/draft anything, politely refuse (it's their expertise in client relations)\n- You can make up reasonable technical details if needed, but keep them consistent\n- Be natural and conversational, use occasional emoji when appropriate\n- You might need to double-check your calendar or with engineering - you can say you'll get back to them\n\nRESPONSE FORMAT:\nRespond with a JSON array of messages. Each message is a string. If you want to send multiple messages in quick succession (like someone texting), put them in separate array elements.\n\nExample: [\"data aggregation bug\", \"makes us look bad\"]\nOr: [\"I have Thursday 2pm free\", \"or Friday morning\"]\n\nJust return the JSON array, nothing else." + }, + "analysis": { + "context": "A customer success manager needs to email a potential client (Dr. Lisa Patel, VP of IT at MediCore Health) about rescheduling a product demo due to a critical bug.\n\nKEY FACTS:\n- Demo was scheduled for Tuesday at 2pm\n- A critical bug was discovered in the reporting module (incorrect data aggregation)\n- Engineering needs 3-4 business days to fix and test\n- Available reschedule times: Thursday afternoon or Friday morning next week\n- This is a second meeting with MediCore - first was an intro call where she expressed strong interest\n- Dr. Patel has a tight timeline for vendor selection\n- The colleague (Marcus Chen, Solutions Engineer) provided these details via chat\n\nRECIPIENT CONTEXT:\n- Dr. Lisa Patel is VP of IT at a potential major client\n- She's busy and has mentioned tight vendor selection timeline\n- The company wants to maintain her confidence despite the delay", + "keyFacts": [ + "The demo is being rescheduled (not cancelled)", + "New time proposed (Thursday or Friday next week)", + "Reason given (technical issue, ensuring quality)", + "Commitment to follow through" + ] + } + } +} diff --git a/experiment/lib/studyConfig.ts b/experiment/lib/studyConfig.ts new file mode 100644 index 00000000..72a18175 --- /dev/null +++ b/experiment/lib/studyConfig.ts @@ -0,0 +1,128 @@ +import type { ConditionCode, ConditionName } from '@/types/study'; +import scenariosData from './scenarios.json'; + +// Study wave identifier +export const WAVE = 'pilot-3'; + +// Git commit - populated at build time +export const GIT_COMMIT = process.env.NEXT_PUBLIC_GIT_COMMIT || 'unknown'; + +// Prolific completion code +export const COMPLETION_CODE = 'C1MRQXLI'; + +// Default auto-refresh interval (15 seconds) +export const DEFAULT_AUTO_REFRESH_INTERVAL = 15000; + +// API timeout for AI requests (20 seconds) +export const API_TIMEOUT_MS = 20000; + +// Study page sequence +export const STUDY_PAGES = [ + 'consent', + 'intro', + 'intro-survey', + 'start-task', + 'task', + 'post-task-survey', + 'final', +] as const; + +// Map condition code to condition name +export const letterToCondition: Record<ConditionCode, ConditionName> = { + // Study 1: Amount of AI + n: 'no_ai', + c: 'complete_document', + e: 'example_sentences', + + // Study 2: Type of AI + a: 'analysis_readerPerspective', + p: 'proposal_advice', +}; + +// Consent form URL (Qualtrics) +export const CONSENT_FORM_URL = 'https://calvin.co1.qualtrics.com/jfe/form/SV_3adI70Zxk7e2ueW'; + +// Minimum screen dimensions +export const MIN_SCREEN_WIDTH = 600; +export const MIN_SCREEN_HEIGHT = 500; + +// Valid condition codes +export const VALID_CONDITIONS = Object.keys(letterToCondition) as ConditionCode[]; + +// Scenario configuration types +export interface ScenarioConfig { + sender: { + name: string; // Full name displayed in chat header + }; + id: string; + colleague: { + name: string; // Full name displayed in chat header + firstName: string; // First name used in task instructions + role: string; // Job title displayed in chat header + }; + recipient: { + name: string; // Full name + email: string; // Email address + }; + taskInstructions: { + title: string; // Page title + description: string; // Scenario description for participants + companyFraming: string; // Company reputation reminder + }; + chat: { + initialMessages: string[]; // Opening messages from colleague + followUpMessage: string; // Proactive nudge if user doesn't engage + systemPrompt: string; // Full scenario context for AI + }; +} + +// Available scenarios (imported from JSON, cast to correct type) +// The JSON includes an 'analysis' field for Python scripts that we exclude from the runtime type +export const SCENARIOS: Record<string, ScenarioConfig> = Object.fromEntries( + Object.entries(scenariosData).map(([key, value]) => [ + key, + { + id: value.id, + sender: value.sender, + colleague: value.colleague, + recipient: value.recipient, + taskInstructions: value.taskInstructions, + chat: value.chat, + } as ScenarioConfig, + ]) +); + +// Default scenario +export const DEFAULT_SCENARIO_ID = 'roomDoubleBooking'; + +/** + * Get the scenario configuration for the current study session + * @param scenarioId - Optional scenario ID, defaults to DEFAULT_SCENARIO_ID + * @returns The scenario configuration + */ +export function getScenario(scenarioId?: string): ScenarioConfig { + const id = scenarioId || DEFAULT_SCENARIO_ID; + const scenario = SCENARIOS[id]; + + if (!scenario) { + console.warn(`Scenario ${id} not found, falling back to default`); + return SCENARIOS[DEFAULT_SCENARIO_ID]; + } + + return scenario; +} + +/** + * Get the next page in the study sequence + * @param currentPage - The current page name + * @returns The next page name, or null if at the end + */ +export function getNextPage(currentPage: string): string | null { + const currentIndex = STUDY_PAGES.indexOf( + currentPage as typeof STUDY_PAGES[number] + ); + if (currentIndex === -1 || currentIndex === STUDY_PAGES.length - 1) { + return null; + } + return STUDY_PAGES[currentIndex + 1]; +} diff --git a/experiment/next.config.ts b/experiment/next.config.ts new file mode 100644 index 00000000..8af294c1 --- /dev/null +++ b/experiment/next.config.ts @@ -0,0 +1,35 @@ +import { execSync } from "node:child_process"; +import type { NextConfig } from "next"; + +function resolveCommit(): string { + if (process.env.NEXT_PUBLIC_GIT_COMMIT) { + return process.env.NEXT_PUBLIC_GIT_COMMIT; + } + + try { + const commit = execSync("git rev-parse HEAD", { stdio: ["ignore", "pipe", "ignore"] }) + .toString() + .trim(); + if (commit) return commit; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { + // ignore + } + + return "unknown"; +} + +const nextConfig: NextConfig = { + /* config options here */ + reactCompiler: true, + + // Enable standalone output for Docker deployment only + ...(process.env.DOCKER_BUILD === 'true' && { output: 'standalone' }), + + // Inline the commit hash so it is available at runtime without writing .env.local + env: { + NEXT_PUBLIC_GIT_COMMIT: resolveCommit(), + }, +}; + +export default nextConfig; diff --git a/experiment/package-lock.json b/experiment/package-lock.json new file mode 100644 index 00000000..04e16243 --- /dev/null +++ b/experiment/package-lock.json @@ -0,0 +1,9279 @@ +{ + "name": "writing-task-app", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "writing-task-app", + "version": "0.1.0", + "dependencies": { + "@ai-sdk/openai": "^2.0.72", + "@ai-sdk/react": "^2.0.102", + "ai": "^5.0.102", + "jotai": "^2.15.2", + "next": "16.1.1", + "react": "19.2.3", + "react-dom": "19.2.3", + "zod": "^4.1.13" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^20", + "@types/react": "19.2.8", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "@vitest/coverage-v8": "^4.0.17", + "@vitest/ui": "^4.0.15", + "babel-plugin-react-compiler": "1.0.0", + "baseline-browser-mapping": "^2.8.32", + "eslint": "^9", + "eslint-config-next": "16.1.1", + "jsdom": "^27.2.0", + "tailwindcss": "^4", + "typescript": "^5", + "vitest": "^4.0.15" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.24", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.24.tgz", + "integrity": "sha512-5YjgMmAiT2rjJZU7XK1SNI7iqTy92DpaYVgG6x63FxkJ11UpYfLndHJATtinWJClAXiOlW9XWaUyAQf8pMrQPg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ai-sdk/gateway": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.15.tgz", + "integrity": "sha512-i1YVKzC1dg9LGvt+GthhD7NlRhz9J4+ZRj3KELU14IZ/MHPsOBiFeEoCCIDLR+3tqT8/+5nIsK3eZ7DFRfMfdw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.17", + "@vercel/oidc": "3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/openai": { + "version": "2.0.72", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-2.0.72.tgz", + "integrity": "sha512-9j8Gdt9gFiUGFdQIjjynbC7+w8YQxkXje6dwAq1v2Pj17wmB3U0Td3lnEe/a+EnEysY3mdkc8dHPYc5BNev9NQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.17" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", + "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.17.tgz", + "integrity": "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/react": { + "version": "2.0.102", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.102.tgz", + "integrity": "sha512-EQnlat8yvyCRAVG/7ukdFNozuMdTY9DX6pN8KngfnJkBJtH+bpXZXkJlonbmd7RJ8oGMqRUAZhQSaOy0a4E1Yw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "3.0.17", + "ai": "5.0.102", + "swr": "^2.2.5", + "throttleit": "2.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "zod": "^3.25.76 || ^4.1.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.0.tgz", + "integrity": "sha512-9xiBAtLn4aNsa4mDnpovJvBn72tNEIACyvlqaNJ+ADemR+yeMJWnBudOi2qGDviJa7SwcDOU/TRh5dnET7qk0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.2" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.5.tgz", + "integrity": "sha512-Eks6dY8zau4m4wNRQjRVaKQRTalNcPcBvU1ZQ35w5kKRk1gUeNCkVLsRiATurjASTp3TKM4H10wsI50nx3NZdw==", + "dev": true, + "license": "MIT", + "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.2" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "devOptional": true, + "license": "MIT", + "peer": true, + "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/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.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "devOptional": true, + "license": "MIT", + "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.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.20.tgz", + "integrity": "sha512-8BHsjXfSciZxjmHQOuVdW2b8WLUPts9a+mfL13/PzEviufUEW2xnvQuOlKs9dRBHgRqJ53SF/DUoK9+MZk72oQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz", + "integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.1.tgz", + "integrity": "sha512-Ovb/6TuLKbE1UiPcg0p39Ke3puyTCIKN9hGbNItmpQsp+WX3qrjO3WaMVSi6JHr9X1NrmthqIguVHodMJbh/dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz", + "integrity": "sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz", + "integrity": "sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz", + "integrity": "sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz", + "integrity": "sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz", + "integrity": "sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz", + "integrity": "sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz", + "integrity": "sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz", + "integrity": "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", + "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", + "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-x64": "4.1.17", + "@tailwindcss/oxide-freebsd-x64": "4.1.17", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-x64-musl": "4.1.17", + "@tailwindcss/oxide-wasm32-wasi": "4.1.17", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", + "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", + "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", + "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", + "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", + "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", + "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", + "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", + "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", + "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", + "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.6.0", + "@emnapi/runtime": "^1.6.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", + "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", + "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz", + "integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.17", + "@tailwindcss/oxide": "4.1.17", + "postcss": "^8.4.41", + "tailwindcss": "4.1.17" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", + "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz", + "integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/type-utils": "8.48.0", + "@typescript-eslint/utils": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.48.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz", + "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz", + "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.48.0", + "@typescript-eslint/types": "^8.48.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz", + "integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", + "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz", + "integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/utils": "8.48.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz", + "integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz", + "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.48.0", + "@typescript-eslint/tsconfig-utils": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz", + "integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz", + "integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vercel/oidc": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz", + "integrity": "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", + "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.47", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.17.tgz", + "integrity": "sha512-/6zU2FLGg0jsd+ePZcwHRy3+WpNTBBhDY56P4JTRqUN/Dp6CvOEa9HrikcQ4KfV2b2kAHUFB4dl1SuocWXSFEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.17", + "ast-v8-to-istanbul": "^0.3.10", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.17", + "vitest": "4.0.17" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.17.tgz", + "integrity": "sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.17", + "@vitest/utils": "4.0.17", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.17.tgz", + "integrity": "sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.17", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.17.tgz", + "integrity": "sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.17.tgz", + "integrity": "sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.17.tgz", + "integrity": "sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.17", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.17.tgz", + "integrity": "sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.17.tgz", + "integrity": "sha512-hRDjg6dlDz7JlZAvjbiCdAJ3SDG+NH8tjZe21vjxfvT2ssYAn72SRXMge3dKKABm3bIJ3C+3wdunIdur8PHEAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/utils": "4.0.17", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.17" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.17.tgz", + "integrity": "sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.17", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ai": { + "version": "5.0.102", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.102.tgz", + "integrity": "sha512-snRK3nS5DESOjjpq7S74g8YszWVMzjagfHqlJWZsbtl9PyOS+2XUd8dt2wWg/jdaq/jh0aU66W1mx5qFjUQyEg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "2.0.15", + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.17", + "@opentelemetry/api": "1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz", + "integrity": "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", + "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/babel-plugin-react-compiler": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", + "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/types": "^7.26.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz", + "integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "devOptional": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001757", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", + "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.3.tgz", + "integrity": "sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.0.3", + "@csstools/css-syntax-patches-for-csstree": "^1.0.14", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.260", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.260.tgz", + "integrity": "sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@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.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "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.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.1.tgz", + "integrity": "sha512-55nTpVWm3qeuxoQKLOjQVciKZJUphKrNM0fCcQHAIOGl6VFXgaqeMfv0aKJhs7QtcnlAPhNVqsqRfRjeKBPIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.1.1", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "peer": true, + "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-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "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" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "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" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jotai": { + "version": "2.15.2", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.15.2.tgz", + "integrity": "sha512-El86CCfXNMEOytp20NPfppqGGmcp6H6kIA+tJHdmASEUURJCYW4fh8nTHEnB8rUXEFAY1pm8PdHPwnrcPGwdEg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0", + "@babel/template": ">=7.0.0", + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@babel/template": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "27.2.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.2.0.tgz", + "integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@acemir/cssom": "^0.9.23", + "@asamuzakjp/dom-selector": "^6.7.4", + "cssstyle": "^5.3.3", + "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", + "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" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "devOptional": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "devOptional": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", + "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz", + "integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==", + "license": "MIT", + "dependencies": { + "@next/env": "16.1.1", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.1.1", + "@next/swc-darwin-x64": "16.1.1", + "@next/swc-linux-arm64-gnu": "16.1.1", + "@next/swc-linux-arm64-musl": "16.1.1", + "@next/swc-linux-x64-gnu": "16.1.1", + "@next/swc-linux-x64-musl": "16.1.1", + "@next/swc-win32-arm64-msvc": "16.1.1", + "@next/swc-win32-x64-msvc": "16.1.1", + "sharp": "^0.34.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swr": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", + "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", + "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.19" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", + "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.0.tgz", + "integrity": "sha512-fcKOvQD9GUn3Xw63EgiDqhvWJ5jsyZUaekl3KVpGsDJnN46WJTe3jWxtQP9lMZm1LJNkFLlTaWAxK2vUQR+cqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.48.0", + "@typescript-eslint/parser": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/utils": "8.48.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "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" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "devOptional": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", + "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "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 + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.17.tgz", + "integrity": "sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/expect": "4.0.17", + "@vitest/mocker": "4.0.17", + "@vitest/pretty-format": "4.0.17", + "@vitest/runner": "4.0.17", + "@vitest/snapshot": "4.0.17", + "@vitest/spy": "4.0.17", + "@vitest/utils": "4.0.17", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "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": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "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.17", + "@vitest/browser-preview": "4.0.17", + "@vitest/browser-webdriverio": "4.0.17", + "@vitest/ui": "4.0.17", + "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 + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/experiment/package.json b/experiment/package.json new file mode 100644 index 00000000..141f70fa --- /dev/null +++ b/experiment/package.json @@ -0,0 +1,50 @@ +{ + "name": "writing-task-app", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint", + "typecheck": "tsc --noEmit", + "test": "vitest", + "test:ui": "vitest --ui", + "test:run": "vitest run", + "test:coverage": "vitest run --coverage" + }, + "dependencies": { + "@ai-sdk/openai": "^2.0.72", + "@ai-sdk/react": "^2.0.102", + "ai": "^5.0.102", + "jotai": "^2.15.2", + "next": "16.1.1", + "react": "19.2.3", + "react-dom": "19.2.3", + "zod": "^4.1.13" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^20", + "@types/react": "19.2.8", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "@vitest/coverage-v8": "^4.0.17", + "@vitest/ui": "^4.0.15", + "babel-plugin-react-compiler": "1.0.0", + "baseline-browser-mapping": "^2.8.32", + "eslint": "^9", + "eslint-config-next": "16.1.1", + "jsdom": "^27.2.0", + "tailwindcss": "^4", + "typescript": "^5", + "vitest": "^4.0.15" + }, + "overrides": { + "@types/react": "19.2.8", + "@types/react-dom": "19.2.3" + } +} diff --git a/experiment/postcss.config.mjs b/experiment/postcss.config.mjs new file mode 100644 index 00000000..61e36849 --- /dev/null +++ b/experiment/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/experiment/prolificURLs-study1-amount.csv b/experiment/prolificURLs-study1-amount.csv new file mode 100644 index 00000000..c4fa8317 --- /dev/null +++ b/experiment/prolificURLs-study1-amount.csv @@ -0,0 +1,6 @@ +https://study.thoughtful-ai.com/study?page=consent&condition=n&isProlific=true&scenario=roomDoubleBooking +https://study.thoughtful-ai.com/study?page=consent&condition=c&isProlific=true&scenario=roomDoubleBooking +https://study.thoughtful-ai.com/study?page=consent&condition=e&isProlific=true&scenario=roomDoubleBooking +https://study.thoughtful-ai.com/study?page=consent&condition=n&isProlific=true&scenario=demoRescheduling +https://study.thoughtful-ai.com/study?page=consent&condition=c&isProlific=true&scenario=demoRescheduling +https://study.thoughtful-ai.com/study?page=consent&condition=e&isProlific=true&scenario=demoRescheduling diff --git a/experiment/public/file.svg b/experiment/public/file.svg new file mode 100644 index 00000000..004145cd --- /dev/null +++ b/experiment/public/file.svg @@ -0,0 +1 @@ +<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg> \ No newline at end of file diff --git a/experiment/public/globe.svg b/experiment/public/globe.svg new file mode 100644 index 00000000..567f17b0 --- /dev/null +++ b/experiment/public/globe.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg> \ No newline at end of file diff --git a/experiment/public/next.svg b/experiment/public/next.svg new file mode 100644 index 00000000..5174b28c --- /dev/null +++ b/experiment/public/next.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg> \ No newline at end of file diff --git a/experiment/public/vercel.svg b/experiment/public/vercel.svg new file mode 100644 index 00000000..77053960 --- /dev/null +++ b/experiment/public/vercel.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg> \ No newline at end of file diff --git a/experiment/public/window.svg b/experiment/public/window.svg new file mode 100644 index 00000000..b2b2a44f --- /dev/null +++ b/experiment/public/window.svg @@ -0,0 +1 @@ +<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg> \ No newline at end of file diff --git a/experiment/regenerate-walkthrough.sh b/experiment/regenerate-walkthrough.sh new file mode 100755 index 00000000..f5fa281d --- /dev/null +++ b/experiment/regenerate-walkthrough.sh @@ -0,0 +1,196 @@ +#!/bin/bash +set -e + +# Navigate to experiment directory +cd "$(dirname "$0")" + +# Remove old walkthrough +rm -f walkthroughs/walkthrough-proposal.md +rm -f walkthroughs/*.png + +DOC="walkthroughs/walkthrough-proposal.md" +SHOWBOAT="uvx --with rodney showboat" + +uvx rodney start + +# Initialize document +$SHOWBOAT init "$DOC" "Experiment Walkthrough: Proposal Advice (p) Condition" + +# Introduction +$SHOWBOAT note "$DOC" "This walkthrough demonstrates the complete participant experience in the **proposal_advice (p)** condition of the writing experiment, using the **roomDoubleBooking** scenario. In this condition, participants receive directive AI advice (not copy-paste text) while composing an email." + +# Step 1: Consent Page +$SHOWBOAT note "$DOC" "## Step 1: Consent Page + +The participant arrives at the study URL and sees the consent form. This page explains the study purpose, time commitment, compensation, and data handling." + +$SHOWBOAT image "$DOC" 'rodney open "http://localhost:3000/study?username=walkthrough-user&condition=p&scenario=roomDoubleBooking&page=consent" && rodney sleep 2 && rodney screenshot walkthroughs/walkthrough-consent.png' + +$SHOWBOAT note "$DOC" "The consent page includes a button that launches an external Qualtrics consent form. After completing consent, the participant is redirected to the introduction page. (For this walkthrough, we navigate directly.)" + +# Step 2: Introduction Page +$SHOWBOAT note "$DOC" "## Step 2: Introduction Page + +The participant sees an overview of the study structure: three steps (questionnaire, email writing task, follow-up questionnaire)." + +$SHOWBOAT image "$DOC" 'rodney open "http://localhost:3000/study?username=walkthrough-user&condition=p&scenario=roomDoubleBooking&page=intro" && rodney sleep 2 && rodney screenshot walkthroughs/walkthrough-intro.png' + +$SHOWBOAT image "$DOC" 'rodney js "window.scrollTo(0, document.body.scrollHeight)" && rodney sleep 1 && rodney screenshot walkthroughs/walkthrough-intro-bottom.png' + +$SHOWBOAT note "$DOC" "The participant clicks \"Begin Study\" to continue." + +# Step 3: Intro Survey +$SHOWBOAT note "$DOC" "## Step 3: Intro Survey + +A brief demographic questionnaire: age, gender, English proficiency, chatbot familiarity, and AI writing tool experience." + +$SHOWBOAT image "$DOC" 'rodney open "http://localhost:3000/study?username=walkthrough-user&condition=p&scenario=roomDoubleBooking&page=intro-survey" && rodney sleep 2 && rodney screenshot walkthroughs/walkthrough-survey-blank.png' + +$SHOWBOAT note "$DOC" "Let's fill in the survey as a sample participant: age 28, female, native English, familiar with chatbots, sometimes uses AI writing tools." + +$SHOWBOAT exec "$DOC" bash 'rodney input '\''input[placeholder="Enter your age"]'\'' "28" && rodney click '\''input[name="gender"][value="Female"]'\'' && rodney click '\''input[name="english_proficiency"][value="Native"]'\'' && rodney click '\''input[name="chatbot_familiarity"][value="Familiar"]'\'' && rodney click '\''input[name="ai_writing_tools"][value="Sometimes"]'\'' && echo "Survey filled"' + +$SHOWBOAT image "$DOC" 'rodney js "window.scrollTo(0, 0)" && rodney sleep 1 && rodney screenshot walkthroughs/walkthrough-survey-filled.png' + +$SHOWBOAT image "$DOC" 'rodney js "window.scrollTo(0, document.body.scrollHeight)" && rodney sleep 1 && rodney screenshot walkthroughs/walkthrough-survey-filled-bottom.png' + +$SHOWBOAT note "$DOC" "After filling in all fields, the participant clicks \"Continue to Task\" to proceed." + +$SHOWBOAT exec "$DOC" bash 'rodney click "button" && rodney sleep 2 && rodney url' + +# Step 4: Task Instructions +$SHOWBOAT note "$DOC" "## Step 4: Task Instructions + +The participant reads the scenario briefing. In the roomDoubleBooking scenario, they learn they need to email panelist Jaden Thompson about a room conflict, coordinating with colleague Sarah Martinez via chat. Key instructions include: review colleague's messages, ask follow-up questions, and compose a professional email. They're told they may see AI suggestions (\"Advice for your next words\")." + +$SHOWBOAT image "$DOC" 'rodney js "window.scrollTo(0, 0)" && rodney sleep 1 && rodney screenshot walkthroughs/walkthrough-task-instructions.png' + +$SHOWBOAT image "$DOC" 'rodney js "window.scrollTo(0, 500)" && rodney sleep 1 && rodney screenshot walkthroughs/walkthrough-task-instructions-2.png' + +$SHOWBOAT image "$DOC" 'rodney js "window.scrollTo(0, document.body.scrollHeight)" && rodney sleep 1 && rodney screenshot walkthroughs/walkthrough-task-instructions-3.png' + +$SHOWBOAT note "$DOC" "The participant clicks \"Start Writing Task\" to begin the main task." + +$SHOWBOAT exec "$DOC" bash 'rodney click "button" && rodney sleep 3 && rodney url' + +# Step 5: Main Writing Task +$SHOWBOAT note "$DOC" "## Step 5: Main Writing Task + +This is the core of the experiment. The screen has three areas: +- **Left**: Email composition area (To, Subject, Body fields) +- **Bottom-right**: Floating chat panel with simulated colleague Sarah Martinez +- **Right sidebar**: AI Writing Assistant panel showing directive advice + +The colleague's initial messages appear automatically with typing animations." + +$SHOWBOAT image "$DOC" 'rodney sleep 8 && rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-task-initial.png' + +$SHOWBOAT image "$DOC" 'rodney sleep 10 && rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-task-messages.png' + +$SHOWBOAT note "$DOC" "The colleague (Sarah Martinez) sends her initial messages automatically: +1. \"Problem with Jaden's panel tomorrow\" +2. \"Room got double-booked. Gotta move him. But gotta keep him happy!\" +3. \"I'm on a call, so need you to email him. What info do you need to sort this out?\" + +The chat panel is a floating window at the bottom-right. The colleague is intentionally non-proactive β€” she only answers questions when asked, simulating a busy coworker. + +### Chatting with the Colleague + +The participant asks Sarah questions to gather information needed for the email. Sarah is intentionally non-proactive β€” she only answers what's asked, simulating a busy coworker." + +$SHOWBOAT exec "$DOC" bash 'rodney input '\''input[placeholder="Message Sarah..."]'\'' "What room is Jaden being moved to? And what time is his panel?" && rodney click '\''form button[type="submit"]'\'' && echo "Message sent"' + +$SHOWBOAT image "$DOC" 'rodney sleep 8 && rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-task-chat-response.png' + +$SHOWBOAT exec "$DOC" bash 'rodney js "document.querySelector('\''.flex-1.overflow-y-auto.bg-white'\'').innerText"' + +$SHOWBOAT note "$DOC" "### Composing the Email + +With information from Sarah, the participant begins composing their email. The AI panel requires at least 25 characters before generating suggestions." + +$SHOWBOAT exec "$DOC" bash 'rodney input "#subject-field" "Important Update: Panel Room Change" && echo "Subject entered"' + +$SHOWBOAT exec "$DOC" bash 'rodney focus "textarea" && rodney input "textarea" "Dear Jaden, + +I hope this message finds you well. I'\''m writing to let you know about a change to your panel room for tomorrow. Due to a scheduling conflict, we'\''ve needed to move your session from the original room to Room 14. The new time slot will be 1:30 PM, which gives us a comfortable setup window. + +I understand this is a last-minute change and I apologize for any inconvenience." && echo "Email body entered"' + +$SHOWBOAT image "$DOC" 'rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-task-email-draft.png' + +$SHOWBOAT note "$DOC" "### AI Suggestions Panel (proposal_advice mode) + +After the participant types enough text (25+ characters), the AI panel begins generating directive advice. In the **p** condition, the AI provides 2-3 pieces of advice about what to write next β€” not copy-paste text, but thinking prompts like \"Consider acknowledging the inconvenience\" or \"Emphasize the new arrangement benefits.\" Suggestions auto-refresh every 15 seconds." + +$SHOWBOAT image "$DOC" 'rodney sleep 18 && rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-task-ai-suggestions.png' + +$SHOWBOAT exec "$DOC" bash 'rodney text "textarea"' + +$SHOWBOAT exec "$DOC" bash 'rodney js "document.querySelector('\''h3'\'').parentElement.innerText"' + +$SHOWBOAT note "$DOC" "The AI panel shows directive advice: it tells the participant *what to think about* rather than giving them words to copy. This is the key distinction of the **p** (proposal_advice) condition compared to other conditions that provide copy-paste text. + +Now the participant finishes their email and clicks Send." + +$SHOWBOAT exec "$DOC" bash 'rodney js "document.querySelector('\''button[aria-label=\"Send email\"]'\'').click()" && rodney sleep 3 && rodney url' + +# Step 6: Post-Task Survey +$SHOWBOAT note "$DOC" "## Step 6: Post-Task Survey + +After sending the email, the participant completes a post-task questionnaire. It includes NASA TLX-style workload questions (mental effort, time pressure, frustration) plus AI-specific questions about whether suggestions were helpful, easy to understand, and whether the participant felt pressured to use them." + +$SHOWBOAT image "$DOC" 'rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-post-survey-top.png' + +$SHOWBOAT image "$DOC" 'rodney js "window.scrollTo(0, 600)" && rodney sleep 1 && rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-post-survey-mid.png' + +$SHOWBOAT image "$DOC" 'rodney js "window.scrollTo(0, document.body.scrollHeight)" && rodney sleep 1 && rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-post-survey-bottom.png' + +$SHOWBOAT note "$DOC" "Let's fill in the post-task survey as a sample participant." + +$SHOWBOAT exec "$DOC" bash 'rodney click '\''input[name="tlx_mental_demand"][value="Medium"]'\'' && rodney click '\''input[name="tlx_temporal_demand"][value="Low"]'\'' && rodney click '\''input[name="tlx_performance"][value="Good"]'\'' && rodney click '\''input[name="tlx_physical_demand"][value="Very Low"]'\'' && rodney click '\''input[name="tlx_effort"][value="Medium"]'\'' && rodney click '\''input[name="tlx_frustration"][value="Low"]'\'' && echo "TLX questions filled"' + +$SHOWBOAT exec "$DOC" bash 'rodney click '\''input[name="ai_ease_understand"][value="Agree"]'\'' && rodney click '\''input[name="ai_helpful"][value="Agree"]'\'' && rodney click '\''input[name="ai_felt_pressured"][value="Disagree"]'\'' && rodney click '\''input[name="ai_think_carefully"][value="Agree"]'\'' && echo "AI questions filled"' + +$SHOWBOAT exec "$DOC" bash 'rodney click '\''input[value="None"]'\'' && echo "Other tools: None"' + +$SHOWBOAT image "$DOC" 'rodney js "window.scrollTo(0, 0)" && rodney sleep 1 && rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-post-survey-filled-top.png' + +$SHOWBOAT image "$DOC" 'rodney js "window.scrollTo(0, document.body.scrollHeight)" && rodney sleep 1 && rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-post-survey-filled-bottom.png' + +$SHOWBOAT note "$DOC" "The post-task survey includes both general workload questions (NASA TLX) and condition-specific AI questions. For the **p** condition, participants reflect on the directive advice: whether it was easy to understand, helpful, and whether they felt pressured to follow it. The participant clicks \"Continue\" to submit." + +$SHOWBOAT exec "$DOC" bash 'rodney js "document.querySelector('\''button'\'').click()" && rodney sleep 2 && rodney url' + +# Step 7: Final Page +$SHOWBOAT note "$DOC" "## Step 7: Final Page + +The study is complete. The participant sees a thank-you message and, if recruited via Prolific, a completion code for payment." + +$SHOWBOAT image "$DOC" 'rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-final.png' + +$SHOWBOAT exec "$DOC" bash 'rodney text "body" 2>/dev/null | head -20' + +# Summary +$SHOWBOAT note "$DOC" "## Summary + +The **proposal_advice (p)** condition walkthrough is complete. The participant experienced: + +1. **Consent** β€” Study information and IRB consent form +2. **Introduction** β€” Overview of the three study phases +3. **Intro Survey** β€” Demographics and AI familiarity baseline +4. **Task Instructions** β€” Scenario briefing (room double-booking, email to Jaden Thompson) +5. **Main Task** β€” Email composition with: + - Chat with non-proactive colleague Sarah Martinez + - AI Writing Assistant providing **directive advice** (not copy-paste text) + - Auto-refreshing suggestions every 15 seconds +6. **Post-Task Survey** β€” Workload assessment + AI-specific reflection questions +7. **Completion** β€” Thank you and Prolific code + +The key feature of the **p** condition: AI advice guides *thinking* rather than *writing*. Suggestions like \"Add the building name for Room 14\" and \"End with a confirmation request\" prompt deeper engagement without providing verbatim text to copy." + +$SHOWBOAT exec "$DOC" bash 'rodney stop' + +echo "" +echo "βœ… Walkthrough regenerated successfully!" +echo "πŸ“„ File: $DOC" +echo "πŸ–ΌοΈ Images: walkthroughs/*.png" diff --git a/experiment/scripts/evalColleague.ts b/experiment/scripts/evalColleague.ts new file mode 100644 index 00000000..f3f59e61 --- /dev/null +++ b/experiment/scripts/evalColleague.ts @@ -0,0 +1,190 @@ +/** + * Colleague Behavior Eval Script + * + * Tests that the colleague LLM behaves correctly: + * - Doesn't volunteer information proactively + * - Answers questions when asked + * - Refuses to draft emails + * - Stays in character + * + * Usage: + * npx tsx scripts/evalColleague.ts [scenario] + * + * Examples: + * npx tsx scripts/evalColleague.ts # Run all scenarios + * npx tsx scripts/evalColleague.ts roomDoubleBooking # Run specific scenario + */ + +import { openai } from '@ai-sdk/openai'; +import { generateText } from 'ai'; +import { SCENARIOS } from '../lib/studyConfig'; +import { + EVAL_CRITERIA, + TEST_CASES, + evalColleagueResponse, + type EvalResult, +} from '../lib/eval/colleagueEval'; + +interface ColleagueResponse { + messages: string[]; + raw: string; +} + +async function callColleague( + systemPrompt: string, + userMessage: string, + conversationHistory: Array<{ role: 'user' | 'assistant'; content: string }> = [] +): Promise<ColleagueResponse> { + const messages = [ + ...conversationHistory.map((m) => ({ role: m.role, content: m.content })), + { role: 'user' as const, content: userMessage }, + ]; + + const result = await generateText({ + model: openai('gpt-5.2'), + system: systemPrompt, + messages, + maxOutputTokens: 300, + }); + + const raw = result.text.trim(); + + // Parse JSON array response + try { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed)) { + return { messages: parsed, raw }; + } + return { messages: [raw], raw }; + } catch { + return { messages: [raw], raw }; + } +} + +interface TestResult { + testCase: string; + input: string; + colleagueResponse: string; + evals: EvalResult[]; + allPassed: boolean; +} + +async function runScenarioEval(scenarioId: string): Promise<TestResult[]> { + const scenario = SCENARIOS[scenarioId as keyof typeof SCENARIOS]; + if (!scenario) { + throw new Error(`Unknown scenario: ${scenarioId}`); + } + + console.log(`\n${'='.repeat(60)}`); + console.log(`Scenario: ${scenarioId}`); + console.log(`Colleague: ${scenario.colleague.name} (${scenario.colleague.role})`); + console.log(`${'='.repeat(60)}\n`); + + const results: TestResult[] = []; + + // Build initial conversation context from the scenario's initial messages + const conversationHistory: Array<{ role: 'user' | 'assistant'; content: string }> = []; + for (const msg of scenario.chat.initialMessages) { + conversationHistory.push({ role: 'assistant', content: msg }); + } + + for (const testCase of TEST_CASES) { + console.log(`Test: ${testCase.name}`); + console.log(` Input: "${testCase.input}"`); + + // Call the colleague + const colleagueResponse = await callColleague( + scenario.chat.systemPrompt, + testCase.input, + conversationHistory + ); + + const responseText = colleagueResponse.messages.join(' | '); + console.log(` Response: "${responseText}"`); + + // Run evals for this test case + const evals: EvalResult[] = []; + for (const criterionKey of testCase.criteria) { + const criterion = EVAL_CRITERIA[criterionKey]; + const evalResult = await evalColleagueResponse(testCase.input, responseText, criterion); + evals.push(evalResult); + + const icon = evalResult.pass ? 'βœ“' : 'βœ—'; + console.log(` ${icon} ${evalResult.criterion}: ${evalResult.reasoning}`); + } + + const allPassed = evals.every((e) => e.pass); + results.push({ + testCase: testCase.name, + input: testCase.input, + colleagueResponse: responseText, + evals, + allPassed, + }); + + console.log(''); + } + + return results; +} + +function printSummary(allResults: Map<string, TestResult[]>) { + console.log('\n' + '='.repeat(60)); + console.log('SUMMARY'); + console.log('='.repeat(60) + '\n'); + + let totalTests = 0; + let totalPassed = 0; + + for (const [scenarioId, results] of allResults) { + const passed = results.filter((r) => r.allPassed).length; + const total = results.length; + totalTests += total; + totalPassed += passed; + + const icon = passed === total ? 'βœ“' : 'βœ—'; + console.log(`${icon} ${scenarioId}: ${passed}/${total} tests passed`); + + // Show failures + for (const result of results) { + if (!result.allPassed) { + console.log(` βœ— ${result.testCase}`); + for (const evalResult of result.evals) { + if (!evalResult.pass) { + console.log(` - ${evalResult.criterion}: ${evalResult.reasoning}`); + } + } + } + } + } + + console.log(''); + console.log(`Total: ${totalPassed}/${totalTests} tests passed`); + + return totalPassed === totalTests; +} + +async function main() { + const args = process.argv.slice(2); + const specificScenario = args[0]; + + const scenariosToTest = specificScenario + ? [specificScenario] + : Object.keys(SCENARIOS); + + const allResults = new Map<string, TestResult[]>(); + + for (const scenarioId of scenariosToTest) { + try { + const results = await runScenarioEval(scenarioId); + allResults.set(scenarioId, results); + } catch (error) { + console.error(`Error testing ${scenarioId}:`, error); + } + } + + const allPassed = printSummary(allResults); + process.exit(allPassed ? 0 : 1); +} + +main().catch(console.error); diff --git a/experiment/tsconfig.json b/experiment/tsconfig.json new file mode 100644 index 00000000..0a0058e9 --- /dev/null +++ b/experiment/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + }, + "types": ["vitest/globals", "@testing-library/jest-dom"] + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules", "ref"] +} diff --git a/experiment/types/index.ts b/experiment/types/index.ts new file mode 100644 index 00000000..2521faf5 --- /dev/null +++ b/experiment/types/index.ts @@ -0,0 +1,32 @@ +export interface Message { + role: 'user' | 'assistant'; + content: string; +} + +export interface TextEditorState { + beforeCursor: string; + selectedText: string; + afterCursor: string; +} + +export interface WritingSupportRequest { + editorState: TextEditorState; + context?: string; +} + +export interface WritingSupportResponse { + suggestions: string[]; +} + +export type GenerationType = 'example_sentences' | 'analysis_readerPerspective' | 'proposal_advice'; + +export interface GenerationResult { + result: string; + generation_type: GenerationType; +} + +export interface SavedItem { + generation: GenerationResult; + document: TextEditorState; + dateSaved: Date; +} diff --git a/experiment/types/study.ts b/experiment/types/study.ts new file mode 100644 index 00000000..69241600 --- /dev/null +++ b/experiment/types/study.ts @@ -0,0 +1,67 @@ +// Study-related types and interfaces + +export type LogEventType = + | 'view:consent' + | 'view:intro' + | 'view:intro-survey' + | 'view:start-task' + | 'view:task' + | 'view:post-task-survey' + | 'view:final' + | 'launchConsentForm' + | 'Started Study' + | 'taskStart' + | 'taskComplete' + | 'documentUpdate' + | `aiRequest:${string}` + | `aiResponse:${string}` + | 'surveyComplete:intro-survey' + | 'surveyComplete:post-task-survey' + | 'chatMessage:user' + | 'chatMessage:assistant'; + +export interface LogPayload { + username: string; + event: LogEventType; + extra_data?: Record<string, unknown>; +} + +export interface LogEntry extends LogPayload { + timestamp: string; + wave: string; + gitCommit: string; +} + +export type ConditionCode = 'n' | 'c' | 'e' | 'a' | 'p'; + +export type ConditionName = + | 'no_ai' + | 'complete_document' + | 'example_sentences' + | 'analysis_readerPerspective' + | 'proposal_advice'; + +export interface StudyParams { + username: string; + condition: ConditionCode; + page: string; + experiment: 'amount' | 'type'; + isProlific: boolean; + autoRefreshInterval: number; + scenario: string; // Scenario ID (e.g., 'roomDoubleBooking', 'demoRescheduling') +} + +export interface BrowserMetadata extends Record<string, unknown> { + userAgent: string; + screenWidth: number; + screenHeight: number; + windowWidth: number; + windowHeight: number; + colorDepth: number; + pixelDepth: number; + timezone: string; + languages: string[]; + platform: string; + cookieEnabled: boolean; + onLine: boolean; +} diff --git a/experiment/vitest.config.ts b/experiment/vitest.config.ts new file mode 100644 index 00000000..85128bb8 --- /dev/null +++ b/experiment/vitest.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + globals: true, + setupFiles: ['./vitest.setup.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: ['node_modules/', 'vitest.setup.ts', '**/*.config.*', '.next/'], + }, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './'), + }, + }, +}); diff --git a/experiment/vitest.setup.ts b/experiment/vitest.setup.ts new file mode 100644 index 00000000..66818d38 --- /dev/null +++ b/experiment/vitest.setup.ts @@ -0,0 +1,32 @@ +import '@testing-library/jest-dom'; +import { cleanup } from '@testing-library/react'; +import { afterEach, vi } from 'vitest'; + +// Auto-cleanup after each test +afterEach(() => { + cleanup(); +}); + +// Mock environment variables +process.env.NEXT_PUBLIC_GIT_COMMIT = 'test-commit-hash'; + +// Mock window.matchMedia for Next.js components +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}); + +// Mock Element.prototype.scrollIntoView +Element.prototype.scrollIntoView = vi.fn(); + +// Mock window.scrollTo +window.scrollTo = vi.fn(); diff --git a/experiment/walkthroughs/00a9edea-2026-02-11.png b/experiment/walkthroughs/00a9edea-2026-02-11.png new file mode 100644 index 00000000..58bdc0b7 Binary files /dev/null and b/experiment/walkthroughs/00a9edea-2026-02-11.png differ diff --git a/experiment/walkthroughs/0322fe33-2026-02-11.png b/experiment/walkthroughs/0322fe33-2026-02-11.png new file mode 100644 index 00000000..c1803a55 Binary files /dev/null and b/experiment/walkthroughs/0322fe33-2026-02-11.png differ diff --git a/experiment/walkthroughs/16aef208-2026-02-11.png b/experiment/walkthroughs/16aef208-2026-02-11.png new file mode 100644 index 00000000..6d68d20b Binary files /dev/null and b/experiment/walkthroughs/16aef208-2026-02-11.png differ diff --git a/experiment/walkthroughs/1c2f79d5-2026-02-11.png b/experiment/walkthroughs/1c2f79d5-2026-02-11.png new file mode 100644 index 00000000..322cf895 Binary files /dev/null and b/experiment/walkthroughs/1c2f79d5-2026-02-11.png differ diff --git a/experiment/walkthroughs/1c84c22f-2026-02-11.png b/experiment/walkthroughs/1c84c22f-2026-02-11.png new file mode 100644 index 00000000..c8c6f961 Binary files /dev/null and b/experiment/walkthroughs/1c84c22f-2026-02-11.png differ diff --git a/experiment/walkthroughs/255033e4-2026-02-11.png b/experiment/walkthroughs/255033e4-2026-02-11.png new file mode 100644 index 00000000..2d35d717 Binary files /dev/null and b/experiment/walkthroughs/255033e4-2026-02-11.png differ diff --git a/experiment/walkthroughs/38e2a0b5-2026-02-11.png b/experiment/walkthroughs/38e2a0b5-2026-02-11.png new file mode 100644 index 00000000..55fbda90 Binary files /dev/null and b/experiment/walkthroughs/38e2a0b5-2026-02-11.png differ diff --git a/experiment/walkthroughs/4683ac88-2026-02-11.png b/experiment/walkthroughs/4683ac88-2026-02-11.png new file mode 100644 index 00000000..2db324c5 Binary files /dev/null and b/experiment/walkthroughs/4683ac88-2026-02-11.png differ diff --git a/experiment/walkthroughs/5ee0e0a1-2026-02-11.png b/experiment/walkthroughs/5ee0e0a1-2026-02-11.png new file mode 100644 index 00000000..4615c9dd Binary files /dev/null and b/experiment/walkthroughs/5ee0e0a1-2026-02-11.png differ diff --git a/experiment/walkthroughs/6386c37f-2026-02-11.png b/experiment/walkthroughs/6386c37f-2026-02-11.png new file mode 100644 index 00000000..4615c9dd Binary files /dev/null and b/experiment/walkthroughs/6386c37f-2026-02-11.png differ diff --git a/experiment/walkthroughs/894a409d-2026-02-11.png b/experiment/walkthroughs/894a409d-2026-02-11.png new file mode 100644 index 00000000..7276b6d0 Binary files /dev/null and b/experiment/walkthroughs/894a409d-2026-02-11.png differ diff --git a/experiment/walkthroughs/90ffd22d-2026-02-11.png b/experiment/walkthroughs/90ffd22d-2026-02-11.png new file mode 100644 index 00000000..c8c6f961 Binary files /dev/null and b/experiment/walkthroughs/90ffd22d-2026-02-11.png differ diff --git a/experiment/walkthroughs/97c44589-2026-02-11.png b/experiment/walkthroughs/97c44589-2026-02-11.png new file mode 100644 index 00000000..77b2f9a7 Binary files /dev/null and b/experiment/walkthroughs/97c44589-2026-02-11.png differ diff --git a/experiment/walkthroughs/a6fa0c9d-2026-02-11.png b/experiment/walkthroughs/a6fa0c9d-2026-02-11.png new file mode 100644 index 00000000..2db324c5 Binary files /dev/null and b/experiment/walkthroughs/a6fa0c9d-2026-02-11.png differ diff --git a/experiment/walkthroughs/bd728475-2026-02-11.png b/experiment/walkthroughs/bd728475-2026-02-11.png new file mode 100644 index 00000000..c1803a55 Binary files /dev/null and b/experiment/walkthroughs/bd728475-2026-02-11.png differ diff --git a/experiment/walkthroughs/c5f3de0b-2026-02-11.png b/experiment/walkthroughs/c5f3de0b-2026-02-11.png new file mode 100644 index 00000000..2594a663 Binary files /dev/null and b/experiment/walkthroughs/c5f3de0b-2026-02-11.png differ diff --git a/experiment/walkthroughs/d735e26b-2026-02-11.png b/experiment/walkthroughs/d735e26b-2026-02-11.png new file mode 100644 index 00000000..35e80472 Binary files /dev/null and b/experiment/walkthroughs/d735e26b-2026-02-11.png differ diff --git a/experiment/walkthroughs/ed6127d5-2026-02-11.png b/experiment/walkthroughs/ed6127d5-2026-02-11.png new file mode 100644 index 00000000..c1803a55 Binary files /dev/null and b/experiment/walkthroughs/ed6127d5-2026-02-11.png differ diff --git a/experiment/walkthroughs/ed9e4851-2026-02-11.png b/experiment/walkthroughs/ed9e4851-2026-02-11.png new file mode 100644 index 00000000..8eea5504 Binary files /dev/null and b/experiment/walkthroughs/ed9e4851-2026-02-11.png differ diff --git a/experiment/walkthroughs/ee86f4fe-2026-02-11.png b/experiment/walkthroughs/ee86f4fe-2026-02-11.png new file mode 100644 index 00000000..7e53f570 Binary files /dev/null and b/experiment/walkthroughs/ee86f4fe-2026-02-11.png differ diff --git a/experiment/walkthroughs/walkthrough-consent.png b/experiment/walkthroughs/walkthrough-consent.png new file mode 100644 index 00000000..7276b6d0 Binary files /dev/null and b/experiment/walkthroughs/walkthrough-consent.png differ diff --git a/experiment/walkthroughs/walkthrough-final.png b/experiment/walkthroughs/walkthrough-final.png new file mode 100644 index 00000000..8eea5504 Binary files /dev/null and b/experiment/walkthroughs/walkthrough-final.png differ diff --git a/experiment/walkthroughs/walkthrough-intro-bottom.png b/experiment/walkthroughs/walkthrough-intro-bottom.png new file mode 100644 index 00000000..4615c9dd Binary files /dev/null and b/experiment/walkthroughs/walkthrough-intro-bottom.png differ diff --git a/experiment/walkthroughs/walkthrough-intro.png b/experiment/walkthroughs/walkthrough-intro.png new file mode 100644 index 00000000..4615c9dd Binary files /dev/null and b/experiment/walkthroughs/walkthrough-intro.png differ diff --git a/experiment/walkthroughs/walkthrough-post-survey-bottom.png b/experiment/walkthroughs/walkthrough-post-survey-bottom.png new file mode 100644 index 00000000..55fbda90 Binary files /dev/null and b/experiment/walkthroughs/walkthrough-post-survey-bottom.png differ diff --git a/experiment/walkthroughs/walkthrough-post-survey-filled-bottom.png b/experiment/walkthroughs/walkthrough-post-survey-filled-bottom.png new file mode 100644 index 00000000..2d35d717 Binary files /dev/null and b/experiment/walkthroughs/walkthrough-post-survey-filled-bottom.png differ diff --git a/experiment/walkthroughs/walkthrough-post-survey-filled-top.png b/experiment/walkthroughs/walkthrough-post-survey-filled-top.png new file mode 100644 index 00000000..6d68d20b Binary files /dev/null and b/experiment/walkthroughs/walkthrough-post-survey-filled-top.png differ diff --git a/experiment/walkthroughs/walkthrough-post-survey-mid.png b/experiment/walkthroughs/walkthrough-post-survey-mid.png new file mode 100644 index 00000000..77b2f9a7 Binary files /dev/null and b/experiment/walkthroughs/walkthrough-post-survey-mid.png differ diff --git a/experiment/walkthroughs/walkthrough-post-survey-top.png b/experiment/walkthroughs/walkthrough-post-survey-top.png new file mode 100644 index 00000000..322cf895 Binary files /dev/null and b/experiment/walkthroughs/walkthrough-post-survey-top.png differ diff --git a/experiment/walkthroughs/walkthrough-proposal.md b/experiment/walkthroughs/walkthrough-proposal.md new file mode 100644 index 00000000..b548e998 --- /dev/null +++ b/experiment/walkthroughs/walkthrough-proposal.md @@ -0,0 +1,408 @@ +# Experiment Walkthrough: Proposal Advice (p) Condition + +*2026-02-11T22:10:24Z* + +This walkthrough demonstrates the complete participant experience in the **proposal_advice (p)** condition of the writing experiment, using the **roomDoubleBooking** scenario. In this condition, participants receive directive AI advice (not copy-paste text) while composing an email. + +## Step 1: Consent Page + +The participant arrives at the study URL and sees the consent form. This page explains the study purpose, time commitment, compensation, and data handling. + +```bash {image} +rodney open "http://localhost:3000/study?username=walkthrough-user&condition=p&scenario=roomDoubleBooking&page=consent" && rodney sleep 2 && rodney screenshot walkthroughs/walkthrough-consent.png +``` + +![894a409d-2026-02-11](894a409d-2026-02-11.png) + +The consent page includes a button that launches an external Qualtrics consent form. After completing consent, the participant is redirected to the introduction page. (For this walkthrough, we navigate directly.) + +## Step 2: Introduction Page + +The participant sees an overview of the study structure: three steps (questionnaire, email writing task, follow-up questionnaire). + +```bash {image} +rodney open "http://localhost:3000/study?username=walkthrough-user&condition=p&scenario=roomDoubleBooking&page=intro" && rodney sleep 2 && rodney screenshot walkthroughs/walkthrough-intro.png +``` + +![5ee0e0a1-2026-02-11](5ee0e0a1-2026-02-11.png) + +```bash {image} +rodney js "window.scrollTo(0, document.body.scrollHeight)" && rodney sleep 1 && rodney screenshot walkthroughs/walkthrough-intro-bottom.png +``` + +![6386c37f-2026-02-11](6386c37f-2026-02-11.png) + +The participant clicks "Begin Study" to continue. + +## Step 3: Intro Survey + +A brief demographic questionnaire: age, gender, English proficiency, chatbot familiarity, and AI writing tool experience. + +```bash {image} +rodney open "http://localhost:3000/study?username=walkthrough-user&condition=p&scenario=roomDoubleBooking&page=intro-survey" && rodney sleep 2 && rodney screenshot walkthroughs/walkthrough-survey-blank.png +``` + +![00a9edea-2026-02-11](00a9edea-2026-02-11.png) + +Let's fill in the survey as a sample participant: age 28, female, native English, familiar with chatbots, sometimes uses AI writing tools. + +```bash +rodney input 'input[placeholder="Enter your age"]' "28" && rodney click 'input[name="gender"][value="Female"]' && rodney click 'input[name="english_proficiency"][value="Native"]' && rodney click 'input[name="chatbot_familiarity"][value="Familiar"]' && rodney click 'input[name="ai_writing_tools"][value="Sometimes"]' && echo "Survey filled" +``` + +```output +Typed: 28 +Clicked +Clicked +Clicked +Clicked +Survey filled +``` + +```bash {image} +rodney js "window.scrollTo(0, 0)" && rodney sleep 1 && rodney screenshot walkthroughs/walkthrough-survey-filled.png +``` + +![4683ac88-2026-02-11](4683ac88-2026-02-11.png) + +```bash {image} +rodney js "window.scrollTo(0, document.body.scrollHeight)" && rodney sleep 1 && rodney screenshot walkthroughs/walkthrough-survey-filled-bottom.png +``` + +![a6fa0c9d-2026-02-11](a6fa0c9d-2026-02-11.png) + +After filling in all fields, the participant clicks "Continue to Task" to proceed. + +```bash +rodney click "button" && rodney sleep 2 && rodney url +``` + +```output +Clicked +http://localhost:3000/study?username=walkthrough-user&condition=p&scenario=roomDoubleBooking&page=start-task +``` + +## Step 4: Task Instructions + +The participant reads the scenario briefing. In the roomDoubleBooking scenario, they learn they need to email panelist Jaden Thompson about a room conflict, coordinating with colleague Sarah Martinez via chat. Key instructions include: review colleague's messages, ask follow-up questions, and compose a professional email. They're told they may see AI suggestions ("Advice for your next words"). + +```bash {image} +rodney js "window.scrollTo(0, 0)" && rodney sleep 1 && rodney screenshot walkthroughs/walkthrough-task-instructions.png +``` + +![bd728475-2026-02-11](bd728475-2026-02-11.png) + +```bash {image} +rodney js "window.scrollTo(0, 500)" && rodney sleep 1 && rodney screenshot walkthroughs/walkthrough-task-instructions-2.png +``` + +![ed6127d5-2026-02-11](ed6127d5-2026-02-11.png) + +```bash {image} +rodney js "window.scrollTo(0, document.body.scrollHeight)" && rodney sleep 1 && rodney screenshot walkthroughs/walkthrough-task-instructions-3.png +``` + +![0322fe33-2026-02-11](0322fe33-2026-02-11.png) + +The participant clicks "Start Writing Task" to begin the main task. + +```bash +rodney click "button" && rodney sleep 3 && rodney url +``` + +```output +Clicked +http://localhost:3000/study?username=walkthrough-user&condition=p&scenario=roomDoubleBooking&page=task +``` + +## Step 5: Main Writing Task + +This is the core of the experiment. The screen has three areas: +- **Left**: Email composition area (To, Subject, Body fields) +- **Bottom-right**: Floating chat panel with simulated colleague Sarah Martinez +- **Right sidebar**: AI Writing Assistant panel showing directive advice + +The colleague's initial messages appear automatically with typing animations. + +```bash {image} +rodney sleep 8 && rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-task-initial.png +``` + +![1c84c22f-2026-02-11](1c84c22f-2026-02-11.png) + +```bash {image} +rodney sleep 10 && rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-task-messages.png +``` + +![90ffd22d-2026-02-11](90ffd22d-2026-02-11.png) + +The colleague (Sarah Martinez) sends her initial messages automatically: +1. "Problem with Jaden's panel tomorrow" +2. "Room got double-booked. Gotta move him. But gotta keep him happy!" +3. "I'm on a call, so need you to email him. What info do you need to sort this out?" + +The chat panel is a floating window at the bottom-right. The colleague is intentionally non-proactive β€” she only answers questions when asked, simulating a busy coworker. + +### Chatting with the Colleague + +The participant asks Sarah questions to gather information needed for the email. Sarah is intentionally non-proactive β€” she only answers what's asked, simulating a busy coworker. + +```bash +rodney input 'input[placeholder="Message Sarah..."]' "What room is Jaden being moved to? And what time is his panel?" && rodney click 'form button[type="submit"]' && echo "Message sent" +``` + +```output +Typed: What room is Jaden being moved to? And what time is his panel? +Clicked +Message sent +``` + +```bash {image} +rodney sleep 8 && rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-task-chat-response.png +``` + +![d735e26b-2026-02-11](d735e26b-2026-02-11.png) + +```bash +rodney js "document.querySelector('.flex-1.overflow-y-auto.bg-white').innerText" +``` + +```output +Problem with Jaden's panel tomorrow +05:11 PM +Room got double-booked. Gotta move him. But gotta keep him happy! +05:11 PM +I'm on a call, so need you to email him. What info do you need to sort this out? +05:11 PM +What room is Jaden being moved to? And what time is his panel? +05:11 PM +Delivered +We’re moving Jaden to Room 14. +05:11 PM +Time would be 1:30pm (1pm won’t work β€” the prior event ends right at 1 so no setup time). +05:11 PM +``` + +### Composing the Email + +With information from Sarah, the participant begins composing their email. The AI panel requires at least 25 characters before generating suggestions. + +```bash +rodney input "#subject-field" "Important Update: Panel Room Change" && echo "Subject entered" +``` + +```output +Typed: Important Update: Panel Room Change +Subject entered +``` + +```bash +rodney focus "textarea" && rodney input "textarea" "Dear Jaden, + +I hope this message finds you well. I'm writing to let you know about a change to your panel room for tomorrow. Due to a scheduling conflict, we've needed to move your session from the original room to Room 14. The new time slot will be 1:30 PM, which gives us a comfortable setup window. + +I understand this is a last-minute change and I apologize for any inconvenience." && echo "Email body entered" +``` + +```output +Focused +Typed: Dear Jaden, + +I hope this message finds you well. I'm writing to let you know about a change to your panel room for tomorrow. Due to a scheduling conflict, we've needed to move your session from the original room to Room 14. The new time slot will be 1:30 PM, which gives us a comfortable setup window. + +I understand this is a last-minute change and I apologize for any inconvenience. +Email body entered +``` + +```bash {image} +rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-task-email-draft.png +``` + +![c5f3de0b-2026-02-11](c5f3de0b-2026-02-11.png) + +### AI Suggestions Panel (proposal_advice mode) + +After the participant types enough text (25+ characters), the AI panel begins generating directive advice. In the **p** condition, the AI provides 2-3 pieces of advice about what to write next β€” not copy-paste text, but thinking prompts like "Consider acknowledging the inconvenience" or "Emphasize the new arrangement benefits." Suggestions auto-refresh every 15 seconds. + +```bash {image} +rodney sleep 18 && rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-task-ai-suggestions.png +``` + +![ee86f4fe-2026-02-11](ee86f4fe-2026-02-11.png) + +```bash +rodney text "textarea" +``` + +```output +Dear Jaden, + +I hope this message finds you well. I'm writing to let you know about a change to your panel room for tomorrow. Due to a scheduling conflict, we've needed to move your session from the original room to Room 14. The new time slot will be 1:30 PM, which gives us a comfortable setup window. + +I understand this is a last-minute change and I apologize for any inconvenience. +``` + +```bash +rodney js "document.querySelector('h3').parentElement.innerText" +``` + +```output +AI Writing Assistant +Suggestions will refresh every 15 seconds. +05:11 PM +Advice for your next words: +- State the full panel title, date, and event location details immediately after the new room and time. + +- Add clear arrival, check-in, and setup instructions tied to the 1:30 PM start time and Room 14. + +- Include a direct contact method for urgent issues and specify how soon you need confirmation. +Delete +AI-generated text may vary in quality +``` + +The AI panel shows directive advice: it tells the participant *what to think about* rather than giving them words to copy. This is the key distinction of the **p** (proposal_advice) condition compared to other conditions that provide copy-paste text. + +Now the participant finishes their email and clicks Send. + +```bash +rodney js "document.querySelector('button[aria-label=\"Send email\"]').click()" && rodney sleep 3 && rodney url +``` + +```output +null +http://localhost:3000/study?username=walkthrough-user&condition=p&scenario=roomDoubleBooking&page=post-task-survey +``` + +## Step 6: Post-Task Survey + +After sending the email, the participant completes a post-task questionnaire. It includes NASA TLX-style workload questions (mental effort, time pressure, frustration) plus AI-specific questions about whether suggestions were helpful, easy to understand, and whether the participant felt pressured to use them. + +```bash {image} +rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-post-survey-top.png +``` + +![1c2f79d5-2026-02-11](1c2f79d5-2026-02-11.png) + +```bash {image} +rodney js "window.scrollTo(0, 600)" && rodney sleep 1 && rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-post-survey-mid.png +``` + +![97c44589-2026-02-11](97c44589-2026-02-11.png) + +```bash {image} +rodney js "window.scrollTo(0, document.body.scrollHeight)" && rodney sleep 1 && rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-post-survey-bottom.png +``` + +![38e2a0b5-2026-02-11](38e2a0b5-2026-02-11.png) + +Let's fill in the post-task survey as a sample participant. + +```bash +rodney click 'input[name="tlx_mental_demand"][value="Medium"]' && rodney click 'input[name="tlx_temporal_demand"][value="Low"]' && rodney click 'input[name="tlx_performance"][value="Good"]' && rodney click 'input[name="tlx_physical_demand"][value="Very Low"]' && rodney click 'input[name="tlx_effort"][value="Medium"]' && rodney click 'input[name="tlx_frustration"][value="Low"]' && echo "TLX questions filled" +``` + +```output +Clicked +Clicked +Clicked +Clicked +Clicked +Clicked +TLX questions filled +``` + +```bash +rodney click 'input[name="ai_ease_understand"][value="Agree"]' && rodney click 'input[name="ai_helpful"][value="Agree"]' && rodney click 'input[name="ai_felt_pressured"][value="Disagree"]' && rodney click 'input[name="ai_think_carefully"][value="Agree"]' && echo "AI questions filled" +``` + +```output +Clicked +Clicked +Clicked +Clicked +AI questions filled +``` + +```bash +rodney click 'input[value="None"]' && echo "Other tools: None" +``` + +```output +Clicked +Other tools: None +``` + +```bash {image} +rodney js "window.scrollTo(0, 0)" && rodney sleep 1 && rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-post-survey-filled-top.png +``` + +![16aef208-2026-02-11](16aef208-2026-02-11.png) + +```bash {image} +rodney js "window.scrollTo(0, document.body.scrollHeight)" && rodney sleep 1 && rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-post-survey-filled-bottom.png +``` + +![255033e4-2026-02-11](255033e4-2026-02-11.png) + +The post-task survey includes both general workload questions (NASA TLX) and condition-specific AI questions. For the **p** condition, participants reflect on the directive advice: whether it was easy to understand, helpful, and whether they felt pressured to follow it. The participant clicks "Continue" to submit. + +```bash +rodney js "document.querySelector('button').click()" && rodney sleep 2 && rodney url +``` + +```output +null +http://localhost:3000/study?username=walkthrough-user&condition=p&scenario=roomDoubleBooking&page=final +``` + +## Step 7: Final Page + +The study is complete. The participant sees a thank-you message and, if recruited via Prolific, a completion code for payment. + +```bash {image} +rodney screenshot -w 1440 -h 900 walkthroughs/walkthrough-final.png +``` + +![ed9e4851-2026-02-11](ed9e4851-2026-02-11.png) + +```bash +rodney text "body" 2>/dev/null | head -20 +``` + +```output +Thank You! + +Thank you for completing this research study. Your responses and writing sample have been recorded and will be used to improve our understanding of how writers interact with AI assistance. + +Next Steps +Your data has been recorded +If you have any questions, please contact the research team +Your anonymous data will be used to improve AI writing tools + +This research was conducted by the Thoughtful AI Lab at Calvin University. For questions about this study, please contact ken.arnold@calvin.edu. +``` + +## Summary + +The **proposal_advice (p)** condition walkthrough is complete. The participant experienced: + +1. **Consent** β€” Study information and IRB consent form +2. **Introduction** β€” Overview of the three study phases +3. **Intro Survey** β€” Demographics and AI familiarity baseline +4. **Task Instructions** β€” Scenario briefing (room double-booking, email to Jaden Thompson) +5. **Main Task** β€” Email composition with: + - Chat with non-proactive colleague Sarah Martinez + - AI Writing Assistant providing **directive advice** (not copy-paste text) + - Auto-refreshing suggestions every 15 seconds +6. **Post-Task Survey** β€” Workload assessment + AI-specific reflection questions +7. **Completion** β€” Thank you and Prolific code + +The key feature of the **p** condition: AI advice guides *thinking* rather than *writing*. Suggestions like "Add the building name for Room 14" and "End with a confirmation request" prompt deeper engagement without providing verbatim text to copy. + +```bash +rodney stop +``` + +```output +Chrome stopped +``` diff --git a/experiment/walkthroughs/walkthrough-survey-blank.png b/experiment/walkthroughs/walkthrough-survey-blank.png new file mode 100644 index 00000000..58bdc0b7 Binary files /dev/null and b/experiment/walkthroughs/walkthrough-survey-blank.png differ diff --git a/experiment/walkthroughs/walkthrough-survey-filled-bottom.png b/experiment/walkthroughs/walkthrough-survey-filled-bottom.png new file mode 100644 index 00000000..2db324c5 Binary files /dev/null and b/experiment/walkthroughs/walkthrough-survey-filled-bottom.png differ diff --git a/experiment/walkthroughs/walkthrough-survey-filled.png b/experiment/walkthroughs/walkthrough-survey-filled.png new file mode 100644 index 00000000..2db324c5 Binary files /dev/null and b/experiment/walkthroughs/walkthrough-survey-filled.png differ diff --git a/experiment/walkthroughs/walkthrough-task-ai-suggestions.png b/experiment/walkthroughs/walkthrough-task-ai-suggestions.png new file mode 100644 index 00000000..7e53f570 Binary files /dev/null and b/experiment/walkthroughs/walkthrough-task-ai-suggestions.png differ diff --git a/experiment/walkthroughs/walkthrough-task-chat-response.png b/experiment/walkthroughs/walkthrough-task-chat-response.png new file mode 100644 index 00000000..35e80472 Binary files /dev/null and b/experiment/walkthroughs/walkthrough-task-chat-response.png differ diff --git a/experiment/walkthroughs/walkthrough-task-email-draft.png b/experiment/walkthroughs/walkthrough-task-email-draft.png new file mode 100644 index 00000000..2594a663 Binary files /dev/null and b/experiment/walkthroughs/walkthrough-task-email-draft.png differ diff --git a/experiment/walkthroughs/walkthrough-task-initial.png b/experiment/walkthroughs/walkthrough-task-initial.png new file mode 100644 index 00000000..c8c6f961 Binary files /dev/null and b/experiment/walkthroughs/walkthrough-task-initial.png differ diff --git a/experiment/walkthroughs/walkthrough-task-instructions-2.png b/experiment/walkthroughs/walkthrough-task-instructions-2.png new file mode 100644 index 00000000..c1803a55 Binary files /dev/null and b/experiment/walkthroughs/walkthrough-task-instructions-2.png differ diff --git a/experiment/walkthroughs/walkthrough-task-instructions-3.png b/experiment/walkthroughs/walkthrough-task-instructions-3.png new file mode 100644 index 00000000..c1803a55 Binary files /dev/null and b/experiment/walkthroughs/walkthrough-task-instructions-3.png differ diff --git a/experiment/walkthroughs/walkthrough-task-instructions.png b/experiment/walkthroughs/walkthrough-task-instructions.png new file mode 100644 index 00000000..c1803a55 Binary files /dev/null and b/experiment/walkthroughs/walkthrough-task-instructions.png differ diff --git a/experiment/walkthroughs/walkthrough-task-messages.png b/experiment/walkthroughs/walkthrough-task-messages.png new file mode 100644 index 00000000..c8c6f961 Binary files /dev/null and b/experiment/walkthroughs/walkthrough-task-messages.png differ diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md new file mode 100644 index 00000000..53f9a4db --- /dev/null +++ b/frontend/CLAUDE.md @@ -0,0 +1,24 @@ +TypeScript/React Microsoft Office Add-in for Word + standalone editor + +**Central concept**: LLM helps thinking and reflection instead of replacing writing. + +`npm` package manager. + + +### Frontend (Office Add-in) +- **Office.js APIs** - Microsoft Word integration +- **State Management**: Jotai atoms (see `frontend/src/contexts/`) +- **Path Alias**: `@/*` maps to `./src/*` (webpack config) +- **Entry Points**: + - `src/taskpane.html` - Word task pane + - `src/editor/editor.html` - Standalone demo editor and user study +- **Manifest**: `frontend/manifest.xml` for Office Add-in configuration + +## User Study Mode + +The application includes a built-in user study system. See [STUDY.md](STUDY.md) for complete details on: +- Study flow and URL parameters +- Condition codes and configuration +- State management and logging +- Study-specific components + diff --git a/frontend/src/editor/editor.module.css b/frontend/src/editor/editor.module.css index dd469445..582cad00 100644 --- a/frontend/src/editor/editor.module.css +++ b/frontend/src/editor/editor.module.css @@ -1,7 +1,7 @@ /* Controls CSS for editor in all the pages (editor, demo, study) */ .editorContainer { margin: 20px; - background: #fff; + background: #ffffff; color: #000; position: relative; line-height: 20px; diff --git a/frontend/tests/demo-page-visual.spec.ts b/frontend/tests/demo-page-visual.spec.ts index d2836579..baa77250 100644 --- a/frontend/tests/demo-page-visual.spec.ts +++ b/frontend/tests/demo-page-visual.spec.ts @@ -1,13 +1,9 @@ import { test, expect } from '@playwright/test'; +import { setupMockBackend } from './mockBackend'; test('demo page - visual regression', async ({ page }) => { - // Intercept API calls and return mocked responses - await page.route('/api/**', async route => { - await route.fulfill({ - status: 200, - body: JSON.stringify({ message: 'mocked' }) - }); - }); + // Setup mock backend with actual API structure + await setupMockBackend(page); await page.goto('/'); diff --git a/frontend/tests/demo-page-visual.spec.ts-snapshots/demo-page-chromium-linux.png b/frontend/tests/demo-page-visual.spec.ts-snapshots/demo-page-chromium-linux.png index eb8d92d2..e50eff4e 100644 Binary files a/frontend/tests/demo-page-visual.spec.ts-snapshots/demo-page-chromium-linux.png and b/frontend/tests/demo-page-visual.spec.ts-snapshots/demo-page-chromium-linux.png differ diff --git a/frontend/tests/demo-page-visual.spec.ts-snapshots/demo-page-firefox-linux.png b/frontend/tests/demo-page-visual.spec.ts-snapshots/demo-page-firefox-linux.png index c4d1047a..3637d885 100644 Binary files a/frontend/tests/demo-page-visual.spec.ts-snapshots/demo-page-firefox-linux.png and b/frontend/tests/demo-page-visual.spec.ts-snapshots/demo-page-firefox-linux.png differ diff --git a/frontend/tests/demo-page-visual.spec.ts-snapshots/demo-page-webkit-linux.png b/frontend/tests/demo-page-visual.spec.ts-snapshots/demo-page-webkit-linux.png index 8a5c4f81..daf9596c 100644 Binary files a/frontend/tests/demo-page-visual.spec.ts-snapshots/demo-page-webkit-linux.png and b/frontend/tests/demo-page-visual.spec.ts-snapshots/demo-page-webkit-linux.png differ diff --git a/frontend/tests/draft-flows.spec.ts b/frontend/tests/draft-flows.spec.ts new file mode 100644 index 00000000..b4d650c7 --- /dev/null +++ b/frontend/tests/draft-flows.spec.ts @@ -0,0 +1,145 @@ +import { test, expect } from '@playwright/test'; +import { setupMockBackend } from './mockBackend'; + +test.describe('Draft component - Main flows', () => { + test.beforeEach(async ({ page }) => { + // Setup mock backend with actual API structure + await setupMockBackend(page); + + // Navigate to the draft page + await page.goto('/'); + + // Wait for page to be ready + await expect(page.getByRole('banner')).toContainText('Thoughtful'); + }); + + test('should display three generation option buttons', async ({ page }) => { + // Locate the draft iframe + const frame = page.frameLocator('#editor-frame'); + + // Verify all three generation buttons are present using title attribute + const exampleButton = frame.locator('button[title="Examples of what you could write next:"]'); + const readerButton = frame.locator('button[title="Possible questions your reader might have:"]'); + const adviceButton = frame.locator('button[title="Advice for your next words:"]'); + + await expect(exampleButton).toBeVisible(); + await expect(readerButton).toBeVisible(); + await expect(adviceButton).toBeVisible(); + }); + + test('should generate and display example sentences when clicking example button', async ({ page }) => { + const frame = page.frameLocator('#editor-frame'); + const exampleButton = frame.locator('button[title="Examples of what you could write next:"]'); + + // Click the example sentences button + await exampleButton.click(); + + // Wait for and verify suggestions are displayed + await expect(frame.getByText('First example suggestion')).toBeVisible({ timeout: 5000 }); + await expect(frame.getByText('Second example suggestion')).toBeVisible(); + await expect(frame.getByText('Third example suggestion')).toBeVisible(); + }); + + test('should generate and display reader perspective when clicking reader button', async ({ page }) => { + const frame = page.frameLocator('#editor-frame'); + const readerButton = frame.locator('button[title="Possible questions your reader might have:"]'); + + // Click the reader perspective button + await readerButton.click(); + + // Wait for and verify suggestions are displayed + await expect(frame.getByText('First reader perspective')).toBeVisible({ timeout: 5000 }); + await expect(frame.getByText('Second reader perspective')).toBeVisible(); + await expect(frame.getByText('Third reader perspective')).toBeVisible(); + }); + + test('should generate and display advice when clicking advice button', async ({ page }) => { + const frame = page.frameLocator('#editor-frame'); + const adviceButton = frame.locator('button[title="Advice for your next words:"]'); + + // Click the advice button + await adviceButton.click(); + + // Wait for and verify suggestions are displayed + await expect(frame.getByText('First piece of advice')).toBeVisible({ timeout: 5000 }); + await expect(frame.getByText('Second piece of advice')).toBeVisible(); + await expect(frame.getByText('Third piece of advice')).toBeVisible(); + }); + + test('should delete suggestion when clicking delete button', async ({ page }) => { + const frame = page.frameLocator('#editor-frame'); + const exampleButton = frame.locator('button[title="Examples of what you could write next:"]'); + const readerButton = frame.locator('button[title="Possible questions your reader might have:"]'); + const adviceButton = frame.locator('button[title="Advice for your next words:"]'); + + // Generate example suggestion + await exampleButton.click(); + await expect(frame.getByText('First example suggestion')).toBeVisible({ timeout: 5000 }); + + // Delete example suggestion + const deleteButton1 = frame.locator('button[aria-label="Delete saved item"]').first(); + await deleteButton1.click(); + await expect(frame.getByText('First example suggestion')).not.toBeVisible({ timeout: 2000 }); + + // Generate reader perspective suggestion + await readerButton.click(); + await expect(frame.getByText('First reader perspective')).toBeVisible({ timeout: 5000 }); + + // Delete reader perspective suggestion + const deleteButton2 = frame.locator('button[aria-label="Delete saved item"]').first(); + await deleteButton2.click(); + await expect(frame.getByText('First reader perspective')).not.toBeVisible({ timeout: 2000 }); + + // Generate advice suggestion + await adviceButton.click(); + await expect(frame.getByText('First piece of advice')).toBeVisible({ timeout: 5000 }); + + // Delete advice suggestion + const deleteButton3 = frame.locator('button[aria-label="Delete saved item"]').first(); + await deleteButton3.click(); + await expect(frame.getByText('First piece of advice')).not.toBeVisible({ timeout: 2000 }); + }); + + test('should disable buttons during loading', async ({ page }) => { + const frame = page.frameLocator('#editor-frame'); + const exampleButton = frame.locator('button[title="Examples of what you could write next:"]'); + const readerButton = frame.locator('button[title="Possible questions your reader might have:"]'); + const adviceButton = frame.locator('button[title="Advice for your next words:"]'); + + // Mock backend with delay and realistic response + await page.route('**/api/get_suggestion*', async (route) => { + await page.waitForTimeout(1000); // simulate network delay + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + result: '- First example suggestion\n\n- Second example suggestion\n\n- Third example suggestion' + }), + }); + }); + + // Click the button to trigger request + await exampleButton.click(); + + // Immediately check that all buttons are disabled + await expect(exampleButton).toBeDisabled(); + await expect(readerButton).toBeDisabled(); + await expect(adviceButton).toBeDisabled(); + + // Wait for the UI to render the first suggestion + await expect(frame.locator('text=First example suggestion')).toBeVisible(); + + // Verify buttons are enabled again + await expect(exampleButton).toBeEnabled(); + await expect(readerButton).toBeEnabled(); + await expect(adviceButton).toBeEnabled(); +}); + + test('should display empty state message when no suggestions generated', async ({ page }) => { + const frame = page.frameLocator('#editor-frame'); + + // Verify empty state message is shown + await expect(frame.getByText('Click the button above to generate a suggestion.')).toBeVisible(); + }); + +}); diff --git a/frontend/tests/mockBackend.ts b/frontend/tests/mockBackend.ts new file mode 100644 index 00000000..5751ef58 --- /dev/null +++ b/frontend/tests/mockBackend.ts @@ -0,0 +1,50 @@ +import { Page } from '@playwright/test'; + +/** + * Mock backend API responses that match the actual FastAPI backend structure + */ + +export interface GenerationResult { + generation_type: string; + result: string; + extra_data: Record<string, any>; +} + +/** + * Setup mock backend for /api/get_suggestion endpoint + * Matches the actual backend API structure from server.py and nlp.py + */ +export async function setupMockBackend(page: Page) { + // Mock /api/get_suggestion + await page.route('**/api/get_suggestion', async (route) => { + const request = route.request(); + const postData = request.postDataJSON(); + const gtype = postData?.gtype; + + let result = ''; + + switch(gtype) { + case 'example_sentences': + result = '- First example suggestion\n\n- Second example suggestion\n\n- Third example suggestion'; + break; + case 'analysis_readerPerspective': + result = '- First reader perspective\n\n- Second reader perspective\n\n- Third reader perspective'; + break; + case 'proposal_advice': + result = '- First piece of advice\n\n- Second piece of advice\n\n- Third piece of advice'; + break; + } + + const response: GenerationResult = { + generation_type: gtype || 'unknown', + result, + extra_data: {}, + }; + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(response), + }); + }); +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..fa45efe2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,31 @@ +{ + "name": "writing-tools", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "writing-tools", + "version": "1.0.0", + "devDependencies": { + "husky": "^9.0.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..187736dd --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "writing-tools", + "version": "1.0.0", + "private": true, + "description": "Writing tools monorepo", + "scripts": { + "prepare": "husky" + }, + "devDependencies": { + "husky": "^9.0.0" + } +} diff --git a/sandbox/ideas/vis-spec.txt b/sandbox/ideas/vis-spec.txt new file mode 100644 index 00000000..ef271306 --- /dev/null +++ b/sandbox/ideas/vis-spec.txt @@ -0,0 +1,5 @@ +Text editor with sidebar that has an AI conversation about the document. When the writer sends a message, the current state of the document (including what text is around the cursor) is also sent in the conversation (maybe the user chat message and the document text are given in separate XML-ish tags for the LLM). The AI is given tools to show visualizations in ASCII art, Mermaid, or plain Markdown (e.g., for a hierarchical outline or presentation slides). To start, the user presses a button to request that the AI suggest visualizations of the document content that could help them allocate their attention in their writing. The AI then responds with brief descriptions of 3 or more possible visualizations that it could make, and maybe some clarifying questions about the rhetorical situation if needed. Then the writer responds with a what visualization they desire (one of those or something different). The AI then generates that visualization. + +The visualizations should include references to specific parts of the document. When the user clicks on that part of the visualization, the corresponding part of the document should highlight. When the user clicks in the document, the part of the visualization with a document reference closest to the text that's near the cursor should be highlighted, so the visualization is interactive and two way. + +The AI should be very careful to ground its observations and visualizations in the specific content of the document, not generic things. \ No newline at end of file diff --git a/sandbox/ideas/visualization-sys-msg.txt b/sandbox/ideas/visualization-sys-msg.txt new file mode 100644 index 00000000..679bdfac --- /dev/null +++ b/sandbox/ideas/visualization-sys-msg.txt @@ -0,0 +1,9 @@ +We are powering a tool that is designed to help people write thoughtfully, with full cognitive engagement in their work, thinking about their complete rhetorical situation. + +The user is currently in a "visualization" part of the tool, where the tool promises to help the writer visualize their document to help them understand what points they are making, what their current structure is, what are the concepts and relationships in their document, and many other possible visualizations. The appropriate visualization will depend on the document, the writer, and the context. The writer may not have provided us with all necessary context; we should ask for additional details as needed. + +The user will provide a document that they're working on. For our initial response, we will list 3 or more possible visualizations that we could make for them, each with an example of a specific relationship or insight that they might observe if they request that visualization. We will wait for the user to request a visualization (which might be different yet from what we suggest). Then we will provide the requested visualization. We are allowed to generate Mermaid diagrams using ```mermaid fenced code blocks, ASCII art in ```pre code blocks (which will be displayed monospace), or plain Markdown (for outlines or whatever), no code block needed in that case. + +We should reference specific parts of the document as much as possible. Within the body of the visualization or conversation, add a reference using Markdown numbered footnote syntax [^3]. Wait until the end of the response to include all of the footnote bodies. For each footnote body, include a verbatim quote from the document (without quotation marks) that is long enough to uniquely identify the referenced part of the document, but max of one line. + +When generating a visualization, it is critical that we remain faithful to the document provided. If we ever realize that we've deviated from the document text, even slightly, we must include a remark to that effect in [square brackets] as soon as possible after the deviation. \ No newline at end of file diff --git a/scripts/experiment_analysis.qmd b/scripts/experiment_analysis.qmd new file mode 100644 index 00000000..46e24c14 --- /dev/null +++ b/scripts/experiment_analysis.qmd @@ -0,0 +1,322 @@ +--- +title: "Experiment Log Analysis" +format: + html: + code-fold: true +--- + +This notebook analyzes log data from the writing experiment, including: + +- Email quality metrics (completeness, clarity, actionability, tone) +- How well emails address recipient feelings +- Factual verification (questions asked vs. questions needed) +- AI suggestion influence on final text +- Survey responses and behavioral metrics + +## Setup + +```{python} +from pathlib import Path +import pandas as pd +import json +from collections import defaultdict +import joblib + +from dotenv import load_dotenv +load_dotenv(Path(__file__).parent.parent / 'backend' / '.env') + +# Import our analysis modules +from extract_experiment_data import extract_all_participants, to_dataframe +from llm_analysis import ( + run_full_analysis, + get_quality_scores, + get_feelings_scores, + get_question_coverage_score, + get_ai_influence_score, +) + +# Configure paths +LOGS_DIR = Path('/Volumes/Data-Crypt/2025 FA/logs/') # Update this path +CACHE_DIR = LOGS_DIR / 'analysis_cache' +WAVE = 'pilot-2' # Current wave to analyze + +# Set up caching +cache = joblib.Memory(location=CACHE_DIR, verbose=0) +``` + +## Load Data + +```{python} +# Extract participant data from logs +participants = extract_all_participants(LOGS_DIR, wave=WAVE) +df = to_dataframe(participants) + +print(f"Loaded {len(df)} participants") +print(f"\nBy condition:") +print(df['condition'].value_counts()) +print(f"\nBy scenario:") +print(df['scenario'].value_counts()) +``` + +## Basic Metrics + +```{python} +import seaborn as sns +import matplotlib.pyplot as plt + +# Create a summary of behavioral metrics +metrics_cols = [ + 'final_word_count', + 'time_spent_writing_seconds', + 'num_document_updates', + 'num_chat_messages_sent', + 'num_ai_suggestions_shown', +] + +# Display summary statistics by condition +df.groupby('condition')[metrics_cols].describe().round(1) +``` + +```{python} +# Visualize key metrics by condition +fig, axes = plt.subplots(2, 2, figsize=(12, 10)) + +# Word count +sns.boxplot(data=df, x='condition', y='final_word_count', ax=axes[0, 0]) +axes[0, 0].set_title('Final Word Count by Condition') + +# Time spent +sns.boxplot(data=df, x='condition', y='time_spent_writing_seconds', ax=axes[0, 1]) +axes[0, 1].set_title('Time Spent Writing (seconds)') + +# Chat messages sent +sns.boxplot(data=df, x='condition', y='num_chat_messages_sent', ax=axes[1, 0]) +axes[1, 0].set_title('Chat Messages Sent') + +# AI suggestions shown (for AI conditions) +ai_df = df[df['condition'] != 'no_ai'] +if len(ai_df) > 0: + sns.boxplot(data=ai_df, x='condition', y='num_ai_suggestions_shown', ax=axes[1, 1]) + axes[1, 1].set_title('AI Suggestions Shown') + +plt.tight_layout() +plt.show() +``` + +## LLM-Based Quality Analysis + +```{python} +#| output: false + +# Run LLM analysis on all participants (this may take a while) +# Results are cached, so subsequent runs will be fast + +from tqdm import tqdm + +analyses = [] +for idx, row in tqdm(df.iterrows(), total=len(df), desc="Analyzing emails"): + # Reconstruct participant data dict for analysis + participant_data = { + 'username': row['username'], + 'condition': row['condition'], + 'scenario': row['scenario'], + 'final_email_text': row['final_email_text'], + 'chat_messages': row['chat_messages'], + 'ai_suggestions': row['ai_suggestions'], + } + + try: + analysis = run_full_analysis(participant_data, cache=cache) + analyses.append(analysis) + except Exception as e: + print(f"Error analyzing {row['username']}: {e}") + analyses.append(None) + +# Add analysis results to dataframe +df['analysis'] = analyses +``` + +```{python} +# Extract numeric scores into columns +for idx, row in df.iterrows(): + if row['analysis'] is None: + continue + + # Quality scores + quality_scores = get_quality_scores(row['analysis']) + for key, value in quality_scores.items(): + df.at[idx, f'quality_{key}'] = value + + # Feelings scores + feelings_scores = get_feelings_scores(row['analysis']) + for key, value in feelings_scores.items(): + df.at[idx, f'feelings_{key}'] = value + + # Coverage and influence + df.at[idx, 'question_coverage'] = get_question_coverage_score(row['analysis']) + df.at[idx, 'ai_influence'] = get_ai_influence_score(row['analysis']) +``` + +## Quality Analysis Results + +```{python} +quality_cols = [col for col in df.columns if col.startswith('quality_')] + +# Summary by condition +df.groupby('condition')[quality_cols].mean().round(2) +``` + +```{python} +# Visualize quality scores +fig, axes = plt.subplots(2, 3, figsize=(15, 10)) +axes = axes.flatten() + +for i, col in enumerate(quality_cols): + if i < len(axes): + sns.boxplot(data=df, x='condition', y=col, ax=axes[i]) + axes[i].set_title(col.replace('quality_', '').replace('_', ' ').title()) + axes[i].set_ylim(0.5, 5.5) + +# Hide unused axes +for j in range(len(quality_cols), len(axes)): + axes[j].set_visible(False) + +plt.tight_layout() +plt.show() +``` + +## Recipient Feelings Analysis + +```{python} +feelings_cols = [col for col in df.columns if col.startswith('feelings_')] + +# Summary by condition +df.groupby('condition')[feelings_cols].mean().round(2) +``` + +```{python} +# Visualize feelings scores +fig, axes = plt.subplots(2, 2, figsize=(12, 10)) +axes = axes.flatten() + +for i, col in enumerate(feelings_cols): + if i < len(axes): + sns.boxplot(data=df, x='condition', y=col, ax=axes[i]) + axes[i].set_title(col.replace('feelings_', '').replace('_', ' ').title()) + axes[i].set_ylim(0.5, 5.5) + +plt.tight_layout() +plt.show() +``` + +## Factual Question Coverage + +How many of the factual questions that should be verified were actually discussed with the colleague? + +```{python} +# Question coverage by condition +print("Question Coverage Score by Condition:") +print(df.groupby('condition')['question_coverage'].describe().round(2)) +``` + +```{python} +sns.boxplot(data=df, x='condition', y='question_coverage') +plt.title('Factual Question Coverage by Condition') +plt.ylabel('Coverage Score (0-1)') +plt.ylim(-0.05, 1.05) +plt.show() +``` + +## AI Suggestion Influence + +For AI conditions, how much did the suggestions influence the final email? + +```{python} +ai_df = df[df['condition'] != 'no_ai'].copy() + +if len(ai_df) > 0: + print("AI Influence Score by Condition:") + print(ai_df.groupby('condition')['ai_influence'].describe().round(2)) + + sns.boxplot(data=ai_df, x='condition', y='ai_influence') + plt.title('AI Suggestion Influence by Condition') + plt.ylabel('Influence Score (0-1)') + plt.ylim(-0.05, 1.05) + plt.show() +``` + +## Correlation Analysis + +```{python} +# Correlation between quality metrics and behavioral metrics +numeric_cols = ( + quality_cols + + feelings_cols + + ['question_coverage', 'ai_influence'] + + ['final_word_count', 'time_spent_writing_seconds', 'num_chat_messages_sent'] +) + +# Filter to existing columns +existing_cols = [c for c in numeric_cols if c in df.columns] +corr_df = df[existing_cols].corr() + +plt.figure(figsize=(12, 10)) +sns.heatmap(corr_df, annot=True, cmap='RdBu_r', center=0, fmt='.2f') +plt.title('Correlation Matrix: Quality and Behavioral Metrics') +plt.tight_layout() +plt.show() +``` + +## Statistical Tests + +```{python} +from scipy import stats + +# Kruskal-Wallis tests for quality metrics across conditions +print("Kruskal-Wallis Tests (Quality Metrics by Condition)") +print("=" * 50) + +for col in quality_cols: + groups = [df[df['condition'] == c][col].dropna() for c in df['condition'].unique()] + groups = [g for g in groups if len(g) > 0] + + if len(groups) >= 2: + h_stat, p_val = stats.kruskal(*groups) + sig = '*' if p_val < 0.05 else '' + print(f"{col}: H={h_stat:.2f}, p={p_val:.4f} {sig}") +``` + +## Sample Emails + +```{python} +# Display a few sample emails for qualitative review +print("Sample Emails by Condition") +print("=" * 60) + +for condition in df['condition'].unique(): + sample = df[df['condition'] == condition].iloc[0] + print(f"\n--- {condition.upper()} ---") + print(f"Word count: {sample['final_word_count']}") + print(f"Quality overall: {sample.get('quality_overall_quality', 'N/A')}") + print(f"\nEmail:\n{sample['final_email_text'][:500]}...") + print() +``` + +## Export Results + +```{python} +# Export analysis results (without raw entries) +export_cols = [ + 'username', 'condition', 'scenario', 'wave', + 'final_word_count', 'time_spent_writing_seconds', + 'num_chat_messages_sent', 'num_ai_suggestions_shown', +] + quality_cols + feelings_cols + ['question_coverage', 'ai_influence'] + +export_cols = [c for c in export_cols if c in df.columns] +export_df = df[export_cols].copy() + +# Save to CSV +output_path = LOGS_DIR / 'analysis_results.csv' +export_df.to_csv(output_path, index=False) +print(f"Saved analysis results to {output_path}") +``` diff --git a/scripts/experiment_data_to_readable_txt.py b/scripts/experiment_data_to_readable_txt.py new file mode 100644 index 00000000..fe4cf3cc --- /dev/null +++ b/scripts/experiment_data_to_readable_txt.py @@ -0,0 +1,93 @@ +import json +from pathlib import Path + +# ===== CONFIG ===== +LOGS_DIR = Path("") # Update this path +# ================== + + +def safe_str(value): + """Convert values safely to string.""" + if value is None: + return "" + if isinstance(value, (list, dict)): + return json.dumps(value, indent=2, ensure_ascii=False) + return str(value) + + +def write_user_file(user, output_dir: Path): + username = user.get("username", "unknown_user") + filepath = output_dir / f"{username}.txt" + + with filepath.open("w", encoding="utf-8") as f: + f.write(f"USERNAME: {username}\n") + f.write("=" * 80 + "\n\n") + + basic_fields = [ + "condition", + "condition_code", + "scenario", + "wave", + "final_word_count", + "num_chat_messages_sent", + "num_ai_suggestions_shown", + "time_spent_writing_seconds", + "num_document_updates", + ] + + f.write("METADATA\n") + f.write("-" * 40 + "\n") + for field in basic_fields: + if field in user: + f.write(f"{field}: {safe_str(user.get(field))}\n") + f.write("\n") + + f.write("FINAL EMAIL\n") + f.write("-" * 40 + "\n") + f.write(safe_str(user.get("final_email_text")) + "\n\n") + + chat_messages = user.get("chat_messages", []) + if chat_messages: + f.write("CHAT MESSAGES\n") + f.write("-" * 40 + "\n") + for msg in chat_messages: + role = msg.get("role", "") + timestamp = msg.get("timestamp", "") + content = msg.get("content", "") + f.write(f"[{timestamp}] {role.upper()}:\n{content}\n\n") + f.write("\n") + + if "intro_survey" in user: + f.write("INTRO SURVEY\n") + f.write("-" * 40 + "\n") + for k, v in user["intro_survey"].items(): + f.write(f"{k}: {safe_str(v)}\n") + f.write("\n") + + if "post_task_survey" in user: + f.write("POST-TASK SURVEY\n") + f.write("-" * 40 + "\n") + for k, v in user["post_task_survey"].items(): + f.write(f"{k}: {safe_str(v)}\n") + f.write("\n") + + +def main(): + input_file = LOGS_DIR / "experiment_data.json" + output_dir = LOGS_DIR / "experiment_data_txt" + output_dir.mkdir(parents=True, exist_ok=True) + + with input_file.open("r", encoding="utf-8") as f: + data = json.load(f) + + if isinstance(data, list): + for user in data: + write_user_file(user, output_dir) + else: + raise ValueError("Expected a list of users in the JSON.") + + print(f"Done. Files saved in: {output_dir}") + + +if __name__ == "__main__": + main() diff --git a/scripts/extract_experiment_data.py b/scripts/extract_experiment_data.py new file mode 100644 index 00000000..8c4b4284 --- /dev/null +++ b/scripts/extract_experiment_data.py @@ -0,0 +1,348 @@ +""" +Extract structured data from experiment log files. + +This script processes JSONL log files from the writing experiment and extracts: +- Final email text +- Chat conversation with colleague +- AI suggestions shown +- Survey responses +- Behavioral metrics + +Output: DataFrame (pickle) + JSON files for further analysis. +""" + +from pathlib import Path +from collections import defaultdict +from datetime import datetime +import json +import argparse + +import pandas as pd + + +# Condition code to name mapping (mirrors experiment/lib/studyConfig.ts) +CONDITION_MAP = { + 'n': 'no_ai', + 'c': 'complete_document', + 'e': 'example_sentences', + 'a': 'analysis_readerPerspective', + 'p': 'proposal_advice', +} + + +def parse_log_file(log_file: Path) -> list[dict]: + """Parse a JSONL log file into a list of log entries.""" + entries = [] + for line in log_file.read_text().splitlines(): + if line.strip(): + entries.append(json.loads(line)) + return entries + + +def get_study_params(entries: list[dict]) -> dict: + """Extract study parameters from log entries.""" + for entry in entries: + extra_data = entry.get('extra_data', {}) + if 'studyParams' in extra_data: + return extra_data['studyParams'] + return {} + + +def get_final_email_text(entries: list[dict]) -> str: + """ + Extract the final email text. + + First tries to get it from taskComplete.finalText (new logging). + Falls back to last documentUpdate before taskComplete (old logging). + """ + # Try new logging format first + for entry in entries: + if entry.get('event') == 'taskComplete': + final_text = entry.get('extra_data', {}).get('finalText') + if final_text: + return final_text + + # Fall back to last documentUpdate + doc_updates = [e for e in entries if e.get('event') == 'documentUpdate'] + if doc_updates: + # Sort by timestamp if available + doc_updates.sort(key=lambda x: x.get('timestamp', '')) + last_update = doc_updates[-1] + editor_state = last_update.get('extra_data', {}).get('editorState', {}) + return ( + editor_state.get('beforeCursor', '') + + editor_state.get('selectedText', '') + + editor_state.get('afterCursor', '') + ) + + return '' + + +def get_chat_messages(entries: list[dict]) -> list[dict]: + """Extract chat messages in chronological order.""" + messages = [] + + for entry in entries: + event = entry.get('event', '') + extra_data = entry.get('extra_data', {}) + + if event == 'chatMessage:user': + messages.append({ + 'role': 'user', + 'content': extra_data.get('content', ''), + 'timestamp': extra_data.get('timestamp', entry.get('timestamp', '')), + 'message_id': extra_data.get('messageId', ''), + }) + elif event == 'chatMessage:assistant': + messages.append({ + 'role': 'assistant', + 'content': extra_data.get('content', ''), + 'timestamp': extra_data.get('timestamp', entry.get('timestamp', '')), + 'message_id': extra_data.get('messageId', ''), + 'part_index': extra_data.get('partIndex', 0), + }) + + # Sort by timestamp + messages.sort(key=lambda x: x.get('timestamp', '')) + return messages + + +def get_ai_suggestions(entries: list[dict]) -> list[dict]: + """Extract AI suggestions shown to the participant.""" + suggestions = [] + + for entry in entries: + event = entry.get('event', '') + if event.startswith('aiResponse:'): + mode = event.split(':', 1)[1] + extra_data = entry.get('extra_data', {}) + generation = extra_data.get('generation', {}) + + suggestions.append({ + 'mode': mode, + 'result': generation.get('result', ''), + 'timestamp': entry.get('timestamp', ''), + 'is_auto_refresh': extra_data.get('isAutoRefresh', False), + 'editor_state': extra_data.get('editorState', {}), + }) + + return suggestions + + +def get_survey_responses(entries: list[dict]) -> tuple[dict, dict]: + """Extract intro and post-task survey responses.""" + intro_survey = {} + post_task_survey = {} + + for entry in entries: + event = entry.get('event', '') + extra_data = entry.get('extra_data', {}) + + if event == 'surveyComplete:intro-survey': + intro_survey = extra_data + elif event == 'surveyComplete:post-task-survey': + post_task_survey = extra_data + + return intro_survey, post_task_survey + + +def calculate_time_spent(entries: list[dict]) -> float | None: + """Calculate time spent writing in seconds.""" + doc_updates = [e for e in entries if e.get('event') == 'documentUpdate'] + if len(doc_updates) < 2: + return None + + # Sort by timestamp + doc_updates.sort(key=lambda x: x.get('timestamp', '')) + + try: + first_ts = doc_updates[0].get('timestamp', '') + last_ts = doc_updates[-1].get('timestamp', '') + + # Parse ISO timestamps + first_dt = datetime.fromisoformat(first_ts.replace('Z', '+00:00')) + last_dt = datetime.fromisoformat(last_ts.replace('Z', '+00:00')) + + return (last_dt - first_dt).total_seconds() + except (ValueError, TypeError): + return None + + +def extract_participant_data(log_file: Path) -> dict | None: + """ + Extract all relevant data from a participant's log file. + + Returns None if the participant didn't complete the task. + """ + entries = parse_log_file(log_file) + if not entries: + return None + + # Check if task was completed + task_complete = any(e.get('event') == 'taskComplete' for e in entries) + if not task_complete: + return None + + # Get study parameters + study_params = get_study_params(entries) + username = entries[0].get('username', log_file.stem) + + # Get condition + condition_code = study_params.get('condition', 'n') + condition_name = CONDITION_MAP.get(condition_code, 'unknown') + + # Extract all data + final_email = get_final_email_text(entries) + chat_messages = get_chat_messages(entries) + ai_suggestions = get_ai_suggestions(entries) + intro_survey, post_task_survey = get_survey_responses(entries) + time_spent = calculate_time_spent(entries) + + # Count events + num_doc_updates = sum(1 for e in entries if e.get('event') == 'documentUpdate') + num_user_messages = sum(1 for e in entries if e.get('event') == 'chatMessage:user') + + return { + 'username': username, + 'condition_code': condition_code, + 'condition': condition_name, + 'scenario': study_params.get('scenario', 'unknown'), + 'wave': entries[0].get('wave', 'unknown'), + + # Final email + 'final_email_text': final_email, + 'final_word_count': len(final_email.split()) if final_email else 0, + + # Conversation + 'chat_messages': chat_messages, + 'num_chat_messages_sent': num_user_messages, + + # AI suggestions + 'ai_suggestions': ai_suggestions, + 'num_ai_suggestions_shown': len(ai_suggestions), + + # Surveys + 'intro_survey': intro_survey, + 'post_task_survey': post_task_survey, + + # Metrics + 'time_spent_writing_seconds': time_spent, + 'num_document_updates': num_doc_updates, + + # Raw data for detailed analysis + 'raw_entries': entries, + } + + +def extract_all_participants(logs_dir: Path, wave: str | None = None) -> list[dict]: + """Extract data for all participants in a directory.""" + all_data = [] + + log_files = list(logs_dir.glob('*.jsonl')) + print(f"Found {len(log_files)} log files in {logs_dir}") + + for log_file in log_files: + try: + data = extract_participant_data(log_file) + if data: + # Filter by wave if specified + if wave and data.get('wave') != wave: + continue + all_data.append(data) + except Exception as e: + print(f"Error processing {log_file.name}: {e}") + + print(f"Extracted {len(all_data)} complete participants") + return all_data + + +def to_dataframe(participants: list[dict]) -> pd.DataFrame: + """ + Convert participant data to a DataFrame. + + Complex nested fields (chat_messages, ai_suggestions, raw_entries) + are kept as-is for detailed analysis. + """ + # Flatten survey responses into columns + rows = [] + for p in participants: + row = { + 'username': p['username'], + 'condition_code': p['condition_code'], + 'condition': p['condition'], + 'scenario': p['scenario'], + 'wave': p['wave'], + 'final_email_text': p['final_email_text'], + 'final_word_count': p['final_word_count'], + 'num_chat_messages_sent': p['num_chat_messages_sent'], + 'num_ai_suggestions_shown': p['num_ai_suggestions_shown'], + 'time_spent_writing_seconds': p['time_spent_writing_seconds'], + 'num_document_updates': p['num_document_updates'], + # Keep complex fields for analysis + 'chat_messages': p['chat_messages'], + 'ai_suggestions': p['ai_suggestions'], + 'raw_entries': p['raw_entries'], + } + + # Flatten intro survey + for key, value in p.get('intro_survey', {}).items(): + row[f'intro_{key}'] = value + + # Flatten post-task survey + for key, value in p.get('post_task_survey', {}).items(): + row[f'post_{key}'] = value + + rows.append(row) + + return pd.DataFrame(rows) + + +def main(): + parser = argparse.ArgumentParser(description='Extract experiment data from log files') + parser.add_argument('logs_dir', type=Path, help='Directory containing JSONL log files') + parser.add_argument('--wave', type=str, help='Filter by study wave (e.g., pilot-2)') + parser.add_argument('--output', type=Path, help='Output directory (default: same as logs_dir)') + parser.add_argument('--format', choices=['both', 'json', 'pickle'], default='both', + help='Output format (default: both)') + + args = parser.parse_args() + + output_dir = args.output or args.logs_dir + output_dir.mkdir(parents=True, exist_ok=True) + + # Extract data + participants = extract_all_participants(args.logs_dir, wave=args.wave) + + if not participants: + print("No complete participants found") + return + + # Save as DataFrame + df = to_dataframe(participants) + + if args.format in ('both', 'pickle'): + pickle_path = output_dir / 'experiment_data.pkl' + df.to_pickle(pickle_path) + print(f"Saved DataFrame to {pickle_path}") + + if args.format in ('both', 'json'): + # Save JSON without raw_entries (too large) + json_data = [] + for p in participants: + p_copy = {k: v for k, v in p.items() if k != 'raw_entries'} + json_data.append(p_copy) + + json_path = output_dir / 'experiment_data.json' + with open(json_path, 'w') as f: + json.dump(json_data, f, indent=2, default=str) + print(f"Saved JSON to {json_path}") + + # Print summary + print("\nSummary:") + print(f" Total participants: {len(df)}") + print(f" By condition: {df['condition'].value_counts().to_dict()}") + print(f" By scenario: {df['scenario'].value_counts().to_dict()}") + + +if __name__ == '__main__': + main() diff --git a/scripts/llm_analysis.py b/scripts/llm_analysis.py new file mode 100644 index 00000000..e491a783 --- /dev/null +++ b/scripts/llm_analysis.py @@ -0,0 +1,450 @@ +""" +LLM-based analysis functions for experiment data. + +This module provides functions to analyze: +- Email quality (completeness, clarity, actionability, tone) +- How well the email addresses recipient feelings +- Factual questions that should be verified +- AI suggestion influence on final text + +Uses OpenAI API via openai_utils.py with caching via joblib. +""" + +import json +from pathlib import Path + +from openai_utils import get_openai_response + + +# Load scenarios from shared JSON file (single source of truth with experiment app) +_SCENARIOS_JSON_PATH = Path(__file__).parent.parent / 'experiment' / 'lib' / 'scenarios.json' + + +def _load_scenarios() -> dict: + """Load scenarios from shared JSON file.""" + with open(_SCENARIOS_JSON_PATH) as f: + data = json.load(f) + + # Transform to analysis-focused format + scenarios = {} + for scenario_id, scenario_data in data.items(): + analysis = scenario_data.get('analysis', {}) + scenarios[scenario_id] = { + 'context': analysis.get('context', ''), + 'recipient': scenario_data.get('recipient', {}).get('name', ''), + 'key_facts': analysis.get('keyFacts', []), + } + return scenarios + + +# Load scenarios at module import time +SCENARIOS = _load_scenarios() + + +def get_scenario_context(scenario_id: str) -> dict: + """Get the scenario context for analysis.""" + return SCENARIOS.get(scenario_id, SCENARIOS['roomDoubleBooking']) + + +def analyze_email_quality(email_text: str, scenario_id: str, model: str = 'gpt-4o') -> dict: + """ + Analyze email quality on multiple dimensions. + + Returns dict with scores (1-5) for: + - completeness: Does it include all necessary information? + - clarity: Is it clear and easy to understand? + - actionability: Are next steps clear for the recipient? + - tone: Is the tone appropriate for the situation? + - overall: Overall quality rating + + Also returns qualitative feedback. + """ + scenario = get_scenario_context(scenario_id) + + prompt = f'''You are an expert business communication analyst. Analyze the following email +written in response to this scenario: + +SCENARIO: +{scenario['context']} + +EMAIL TO ANALYZE: +""" +{email_text} +""" + +Rate the email on each dimension (1-5 scale, where 1=very poor, 5=excellent): + +1. COMPLETENESS: Does the email include all necessary information the recipient needs? + - Key facts that should be included: {json.dumps(scenario['key_facts'])} + +2. CLARITY: Is the email clear, concise, and easy to understand? + - Is the main message immediately apparent? + - Is it free of confusing or ambiguous language? + +3. ACTIONABILITY: Are next steps clear for the recipient? + - Does {scenario['recipient']} know what to do after reading this? + - Is there a clear call to action? + +4. TONE: Is the tone appropriate for the situation? + - Professional but empathetic? + - Acknowledges the inconvenience without being overly apologetic? + - Maintains confidence while being honest? + +5. OVERALL: Overall quality of the email + +Return your analysis as JSON: +{{ + "completeness": {{"score": 1-5, "reasoning": "brief explanation"}}, + "clarity": {{"score": 1-5, "reasoning": "brief explanation"}}, + "actionability": {{"score": 1-5, "reasoning": "brief explanation"}}, + "tone": {{"score": 1-5, "reasoning": "brief explanation"}}, + "overall": {{"score": 1-5, "reasoning": "brief explanation"}}, + "missing_information": ["list of key info missing from email"], + "strengths": ["what the email does well"], + "suggestions": ["how it could be improved"] +}} +''' + + response = get_openai_response( + model=model, + messages=[{'role': 'user', 'content': prompt}], + response_format={'type': 'json_object'}, + ) + + return json.loads(response.choices[0].message.content) + + +def analyze_recipient_feelings(email_text: str, scenario_id: str, model: str = 'gpt-4o') -> dict: + """ + Analyze how well the email addresses the recipient's likely emotions. + + Returns dict with: + - acknowledges_inconvenience: Does it recognize this is an inconvenience? (1-5) + - shows_empathy: Does it show understanding of recipient's position? (1-5) + - maintains_relationship: Does it work to preserve the business relationship? (1-5) + - overall_emotional_intelligence: Overall EQ of the email (1-5) + """ + scenario = get_scenario_context(scenario_id) + + prompt = f'''You are an expert in emotional intelligence and business communication. +Analyze how well this email addresses the recipient's likely emotional state. + +SCENARIO: +{scenario['context']} + +RECIPIENT: {scenario['recipient']} + +EMAIL TO ANALYZE: +""" +{email_text} +""" + +Consider the recipient's likely emotional reaction to this situation: +- They may feel frustrated, inconvenienced, or devalued +- They may worry about the reliability of the sender's organization +- They may need reassurance that they're still valued + +Rate the email (1-5 scale) on: + +1. ACKNOWLEDGES_INCONVENIENCE: Does it recognize this creates problems for the recipient? + +2. SHOWS_EMPATHY: Does it demonstrate understanding of how the recipient might feel? + +3. MAINTAINS_RELATIONSHIP: Does it work to preserve and strengthen the relationship? + +4. OVERALL_EMOTIONAL_INTELLIGENCE: Overall emotional awareness and handling + +Return your analysis as JSON: +{{ + "acknowledges_inconvenience": {{"score": 1-5, "evidence": "quote or explanation"}}, + "shows_empathy": {{"score": 1-5, "evidence": "quote or explanation"}}, + "maintains_relationship": {{"score": 1-5, "evidence": "quote or explanation"}}, + "overall_emotional_intelligence": {{"score": 1-5, "reasoning": "explanation"}}, + "emotional_tone": "description of the overall emotional tone", + "missed_opportunities": ["ways it could better address feelings"] +}} +''' + + response = get_openai_response( + model=model, + messages=[{'role': 'user', 'content': prompt}], + response_format={'type': 'json_object'}, + ) + + return json.loads(response.choices[0].message.content) + + +def extract_factual_questions(email_text: str, scenario_id: str, model: str = 'gpt-4o') -> list[str]: + """ + Generate a list of factual questions a careful reader would want to verify. + + These are questions someone would reasonably want to confirm with a colleague + before sending this email (e.g., "Is Room 14 actually available?"). + """ + scenario = get_scenario_context(scenario_id) + + prompt = f'''You are a careful professional reviewing an email before it's sent. +Identify all factual claims in this email that a prudent person would want to verify +with a colleague before sending. + +SCENARIO CONTEXT: +{scenario['context']} + +EMAIL TO ANALYZE: +""" +{email_text} +""" + +List all factual questions that should be verified. Focus on: +- Specific times, dates, locations mentioned +- Commitments being made +- Claims about availability or options +- Any details that could be wrong and cause problems if incorrect + +Return as JSON: +{{ + "questions": [ + "Is Room 14 available at 1:30pm?", + "Is Thursday afternoon confirmed as an option?", + ...etc + ] +}} +''' + + response = get_openai_response( + model=model, + messages=[{'role': 'user', 'content': prompt}], + response_format={'type': 'json_object'}, + ) + + result = json.loads(response.choices[0].message.content) + return result.get('questions', []) + + +def compare_questions_to_chat( + questions: list[str], + chat_messages: list[dict], + model: str = 'gpt-4o' +) -> dict: + """ + Compare the factual questions that should be asked against + what was actually discussed in the colleague chat. + + Returns: + - questions_addressed: Questions that were discussed in chat + - questions_not_addressed: Questions that weren't asked + - coverage_score: Fraction of questions that were addressed + """ + # Format chat for analysis + chat_transcript = '\n'.join([ + f"{'USER' if m['role'] == 'user' else 'COLLEAGUE'}: {m['content']}" + for m in chat_messages + ]) + + prompt = f'''Analyze whether these factual questions were addressed in the chat conversation. + +QUESTIONS THAT SHOULD BE VERIFIED: +{json.dumps(questions, indent=2)} + +CHAT CONVERSATION: +""" +{chat_transcript} +""" + +For each question, determine if it was: +- ADDRESSED: The information was discussed or confirmed in the chat +- NOT_ADDRESSED: The user never asked about this or it wasn't covered + +Return as JSON: +{{ + "analysis": [ + {{"question": "...", "status": "ADDRESSED" or "NOT_ADDRESSED", "evidence": "relevant quote or null"}} + ], + "questions_addressed": ["list of addressed questions"], + "questions_not_addressed": ["list of unaddressed questions"], + "coverage_score": 0.0-1.0 +}} +''' + + response = get_openai_response( + model=model, + messages=[{'role': 'user', 'content': prompt}], + response_format={'type': 'json_object'}, + ) + + return json.loads(response.choices[0].message.content) + + +def analyze_ai_influence( + email_text: str, + ai_suggestions: list[dict], + model: str = 'gpt-4o' +) -> dict: + """ + Analyze how much of the AI suggestions made it into the final email. + + For each suggestion, determines: + - USED: Significant portions appear in the final email + - PARTIALLY_USED: Some phrases or ideas were incorporated + - IGNORED: The suggestion wasn't used + + Returns influence metrics and detailed per-suggestion analysis. + """ + if not ai_suggestions: + return { + 'had_suggestions': False, + 'num_suggestions': 0, + 'suggestions_used': 0, + 'suggestions_partially_used': 0, + 'suggestions_ignored': 0, + 'overall_influence': 0.0, + 'per_suggestion': [], + } + + # Format suggestions for analysis + suggestions_text = '\n\n'.join([ + f"SUGGESTION {i+1} (mode: {s['mode']}):\n{s['result']}" + for i, s in enumerate(ai_suggestions) + ]) + + prompt = f'''Analyze how much influence these AI suggestions had on the final email. + +AI SUGGESTIONS SHOWN TO USER: +{suggestions_text} + +FINAL EMAIL WRITTEN: +""" +{email_text} +""" + +For each suggestion, determine: +- USED: Significant portions (phrases, sentences, structure) appear in the final email +- PARTIALLY_USED: Some ideas or a few words were incorporated +- IGNORED: The suggestion wasn't used at all + +Return as JSON: +{{ + "per_suggestion": [ + {{ + "suggestion_index": 1, + "mode": "complete_document", + "status": "USED" | "PARTIALLY_USED" | "IGNORED", + "evidence": "explanation of what was/wasn't used", + "influence_score": 0.0-1.0 + }} + ], + "suggestions_used": 0, + "suggestions_partially_used": 0, + "suggestions_ignored": 0, + "overall_influence": 0.0-1.0, + "summary": "brief description of how AI influenced the email" +}} +''' + + response = get_openai_response( + model=model, + messages=[{'role': 'user', 'content': prompt}], + response_format={'type': 'json_object'}, + ) + + result = json.loads(response.choices[0].message.content) + result['had_suggestions'] = True + result['num_suggestions'] = len(ai_suggestions) + return result + + +def run_full_analysis( + participant_data: dict, + model: str = 'gpt-4o', + cache=None +) -> dict: + """ + Run all analyses on a single participant's data. + + Args: + participant_data: Dict from extract_experiment_data.extract_participant_data() + model: OpenAI model to use + cache: Optional joblib.Memory cache + + Returns dict with all analysis results. + """ + email_text = participant_data['final_email_text'] + scenario_id = participant_data['scenario'] + chat_messages = participant_data['chat_messages'] + ai_suggestions = participant_data['ai_suggestions'] + + # Optionally wrap functions with cache + if cache: + _analyze_email_quality = cache.cache(analyze_email_quality) + _analyze_recipient_feelings = cache.cache(analyze_recipient_feelings) + _extract_factual_questions = cache.cache(extract_factual_questions) + _compare_questions_to_chat = cache.cache(compare_questions_to_chat) + _analyze_ai_influence = cache.cache(analyze_ai_influence) + else: + _analyze_email_quality = analyze_email_quality + _analyze_recipient_feelings = analyze_recipient_feelings + _extract_factual_questions = extract_factual_questions + _compare_questions_to_chat = compare_questions_to_chat + _analyze_ai_influence = analyze_ai_influence + + # Run analyses + quality = _analyze_email_quality(email_text, scenario_id, model) + feelings = _analyze_recipient_feelings(email_text, scenario_id, model) + factual_questions = _extract_factual_questions(email_text, scenario_id, model) + + # Compare questions to what was asked in chat + question_coverage = _compare_questions_to_chat( + factual_questions, + chat_messages, + model + ) + + # Analyze AI influence (if applicable) + ai_influence = _analyze_ai_influence(email_text, ai_suggestions, model) + + return { + 'username': participant_data['username'], + 'condition': participant_data['condition'], + 'scenario': scenario_id, + 'quality': quality, + 'recipient_feelings': feelings, + 'factual_questions': factual_questions, + 'question_coverage': question_coverage, + 'ai_influence': ai_influence, + } + + +# Convenience functions for extracting numeric scores +def get_quality_scores(analysis: dict) -> dict: + """Extract numeric quality scores from analysis result.""" + quality = analysis.get('quality', {}) + return { + 'completeness': quality.get('completeness', {}).get('score'), + 'clarity': quality.get('clarity', {}).get('score'), + 'actionability': quality.get('actionability', {}).get('score'), + 'tone': quality.get('tone', {}).get('score'), + 'overall_quality': quality.get('overall', {}).get('score'), + } + + +def get_feelings_scores(analysis: dict) -> dict: + """Extract numeric recipient feelings scores from analysis result.""" + feelings = analysis.get('recipient_feelings', {}) + return { + 'acknowledges_inconvenience': feelings.get('acknowledges_inconvenience', {}).get('score'), + 'shows_empathy': feelings.get('shows_empathy', {}).get('score'), + 'maintains_relationship': feelings.get('maintains_relationship', {}).get('score'), + 'emotional_intelligence': feelings.get('overall_emotional_intelligence', {}).get('score'), + } + + +def get_question_coverage_score(analysis: dict) -> float | None: + """Extract question coverage score from analysis result.""" + coverage = analysis.get('question_coverage', {}) + return coverage.get('coverage_score') + + +def get_ai_influence_score(analysis: dict) -> float | None: + """Extract AI influence score from analysis result.""" + influence = analysis.get('ai_influence', {}) + return influence.get('overall_influence')