diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2aeb8c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,62 @@ +# Dependencies +node_modules/ + +# Next.js +.next/ +out/ +.swc/ + +# Build outputs +build/ +dist/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Debug logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Vercel +.vercel + +# TypeScript +*.tsbuildinfo +next-env.d.ts + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +.venv/ +ENV/ +env/ + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +.tox/ +.nox/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store + +# Logs +logs/ +*.log + +# Debug audio (project specific) +debug_audio/ diff --git a/.hypothesis/constants/04f2136ccc19ab10 b/.hypothesis/constants/04f2136ccc19ab10 new file mode 100644 index 0000000..6cccbc7 --- /dev/null +++ b/.hypothesis/constants/04f2136ccc19ab10 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/order/__init__.py +# hypothesis_version: 6.151.4 + +['TrackOrderTool'] \ No newline at end of file diff --git a/.hypothesis/constants/0618c790d33baeca b/.hypothesis/constants/0618c790d33baeca new file mode 100644 index 0000000..a2320bc --- /dev/null +++ b/.hypothesis/constants/0618c790d33baeca @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/media/sample_image_tool.py +# hypothesis_version: 6.151.4 + +['Sample Image', 'content', 'description', 'image', 'name', 'object', 'properties', 'required', 'schema', 'shortDescription', 'showSampleImageTool', 'title', 'type', 'url'] \ No newline at end of file diff --git a/.hypothesis/constants/0a00cf04224d1343 b/.hypothesis/constants/0a00cf04224d1343 new file mode 100644 index 0000000..5fb69a4 --- /dev/null +++ b/.hypothesis/constants/0a00cf04224d1343 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/order/track_order_tool.py +# hypothesis_version: 6.151.4 + +[10000, '#', '%Y-%m-%d', 'Current Status', 'Delayed', 'Delivered', 'Distribution Center', 'Enabled', 'Estimated Delivery', 'Front Door', 'In transit', 'Notifications', 'Order received', 'Out for delivery', 'Processing', 'Shipped', 'Track Another Order', 'Tracking an order', 'action', 'additionalInfo', 'boolean', 'card', 'content', 'currentLocation', 'deliveryLocation', 'description', 'details', 'estimatedDelivery', 'footer', 'name', 'notificationStatus', 'object', 'orderId', 'orderNumber', 'orderStatus', 'properties', 'requestNotifications', 'required', 'schema', 'shortDescription', 'string', 'text', 'title', 'trackOrderTool', 'type', 'url'] \ No newline at end of file diff --git a/.hypothesis/constants/0a746ffa7128899a b/.hypothesis/constants/0a746ffa7128899a new file mode 100644 index 0000000..2ec1838 --- /dev/null +++ b/.hypothesis/constants/0a746ffa7128899a @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/banking/state.py +# hypothesis_version: 6.151.4 + +[-120.0, -85.23, -45.8, -14.99, -4.5, 29.99, 500.0, 3000.0, 5000.0, 'Alice', 'BankingState', 'Bob', 'Charlie', 'Coffee Shop', 'Diana', 'Electric Bill', 'Freelance Payment', 'Grocery Store', 'Power Company', 'Restaurant', 'Salary Deposit', 'Subscription Service', 'action', 'actionHistory', 'amount', 'avatarColor', 'balance', 'blue', 'contacts', 'date', 'description', 'green', 'id', 'name', 'orange', 'purple', 'recipient', 'timestamp', 'transactions'] \ No newline at end of file diff --git a/.hypothesis/constants/33d8627ad75175ce b/.hypothesis/constants/33d8627ad75175ce new file mode 100644 index 0000000..51830de --- /dev/null +++ b/.hypothesis/constants/33d8627ad75175ce @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/banking/__init__.py +# hypothesis_version: 6.151.4 + +['ActionHistoryEntry', 'BankingState', 'CheckBalanceTool', 'Contact', 'GetTransactionsTool', 'SendMoneyTool', 'Transaction'] \ No newline at end of file diff --git a/.hypothesis/constants/4c4ee5bb1d98c4c9 b/.hypothesis/constants/4c4ee5bb1d98c4c9 new file mode 100644 index 0000000..6c166df --- /dev/null +++ b/.hypothesis/constants/4c4ee5bb1d98c4c9 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/utility/__init__.py +# hypothesis_version: 6.151.4 + +['DateAndTimeTool'] \ No newline at end of file diff --git a/.hypothesis/constants/605aa399669e812f b/.hypothesis/constants/605aa399669e812f new file mode 100644 index 0000000..cc93c26 --- /dev/null +++ b/.hypothesis/constants/605aa399669e812f @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/utility/datetime_tool.py +# hypothesis_version: 6.151.4 + +['#', '%A', '%A, %B %d, %Y', '%B', '%I:%M %p', '%Y-%m-%d', 'America/Los_Angeles', 'Current Date & Time', 'Date', 'Day of Week', 'Month', 'PST', 'Refresh', 'Time', 'Time Zone', 'Year', 'action', 'card', 'content', 'date', 'day', 'dayOfWeek', 'description', 'details', 'footer', 'formattedTime', 'getDateAndTimeTool', 'image', 'imageAlt', 'month', 'name', 'object', 'properties', 'required', 'schema', 'shortDescription', 'text', 'timezone', 'title', 'type', 'url', 'year'] \ No newline at end of file diff --git a/.hypothesis/constants/63bd53c5a363bc27 b/.hypothesis/constants/63bd53c5a363bc27 new file mode 100644 index 0000000..fc0765b --- /dev/null +++ b/.hypothesis/constants/63bd53c5a363bc27 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/banking/models.py +# hypothesis_version: 6.151.4 + +[] \ No newline at end of file diff --git a/.hypothesis/constants/75356c4fc73869a0 b/.hypothesis/constants/75356c4fc73869a0 new file mode 100644 index 0000000..d73fa87 --- /dev/null +++ b/.hypothesis/constants/75356c4fc73869a0 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/banking/__init__.py +# hypothesis_version: 6.151.4 + +['ActionHistoryEntry', 'BankingState', 'Contact', 'Transaction'] \ No newline at end of file diff --git a/.hypothesis/constants/818cf4bd8172ecb2 b/.hypothesis/constants/818cf4bd8172ecb2 new file mode 100644 index 0000000..56e47b9 --- /dev/null +++ b/.hypothesis/constants/818cf4bd8172ecb2 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/__init__.py +# hypothesis_version: 6.151.4 + +['ToolManager'] \ No newline at end of file diff --git a/.hypothesis/constants/94611bd473223f81 b/.hypothesis/constants/94611bd473223f81 new file mode 100644 index 0000000..df21d80 --- /dev/null +++ b/.hypothesis/constants/94611bd473223f81 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/tool_manager.py +# hypothesis_version: 6.151.4 + +['description', 'inputSchema', 'json', 'name', 'schema', 'toolSpec'] \ No newline at end of file diff --git a/.hypothesis/constants/9b47bf4b51f4c068 b/.hypothesis/constants/9b47bf4b51f4c068 new file mode 100644 index 0000000..b6fda0c --- /dev/null +++ b/.hypothesis/constants/9b47bf4b51f4c068 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/base/__init__.py +# hypothesis_version: 6.151.4 + +['BaseTool', 'ToolRegistry'] \ No newline at end of file diff --git a/.hypothesis/constants/a3e01f3d677858a5 b/.hypothesis/constants/a3e01f3d677858a5 new file mode 100644 index 0000000..6846a9d --- /dev/null +++ b/.hypothesis/constants/a3e01f3d677858a5 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/media/sample_pdf_tool.py +# hypothesis_version: 6.151.4 + +['Sample PDF Document', 'Showing a sample PDF', 'content', 'description', 'name', 'object', 'pdf', 'properties', 'required', 'schema', 'shortDescription', 'showSamplePdfTool', 'text', 'title', 'type', 'url'] \ No newline at end of file diff --git a/.hypothesis/constants/b9ca80631071fc5e b/.hypothesis/constants/b9ca80631071fc5e new file mode 100644 index 0000000..219bf95 --- /dev/null +++ b/.hypothesis/constants/b9ca80631071fc5e @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/media/sample_video_tool.py +# hypothesis_version: 6.151.4 + +['&controls=0', 'Error', 'Show a YouTube video', 'YouTube Video', '^[A-Za-z0-9_-]{11}$', 'a9__D53WsUs', 'boolean', 'content', 'description', 'error', 'message', 'name', 'object', 'pattern', 'properties', 'schema', 'shortDescription', 'showControls', 'showSampleVideoTool', 'string', 'text', 'title', 'type', 'url', 'video', 'videoId'] \ No newline at end of file diff --git a/.hypothesis/constants/c055083c70179d96 b/.hypothesis/constants/c055083c70179d96 new file mode 100644 index 0000000..a2eb6f6 --- /dev/null +++ b/.hypothesis/constants/c055083c70179d96 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/base/tool.py +# hypothesis_version: 6.151.4 + +['description', 'model_result', 'name', 'object', 'properties', 'required', 'schema', 'shortDescription', 'type', 'ui_result'] \ No newline at end of file diff --git a/.hypothesis/constants/c52b48c8f3cf87ef b/.hypothesis/constants/c52b48c8f3cf87ef new file mode 100644 index 0000000..a57afb3 --- /dev/null +++ b/.hypothesis/constants/c52b48c8f3cf87ef @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/media/__init__.py +# hypothesis_version: 6.151.4 + +['SampleImageTool', 'SamplePdfTool', 'SampleVideoTool'] \ No newline at end of file diff --git a/.hypothesis/constants/ca868d2b6ff0d5f2 b/.hypothesis/constants/ca868d2b6ff0d5f2 new file mode 100644 index 0000000..df21d80 --- /dev/null +++ b/.hypothesis/constants/ca868d2b6ff0d5f2 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/tool_manager.py +# hypothesis_version: 6.151.4 + +['description', 'inputSchema', 'json', 'name', 'schema', 'toolSpec'] \ No newline at end of file diff --git a/.hypothesis/constants/cba1d6001768c887 b/.hypothesis/constants/cba1d6001768c887 new file mode 100644 index 0000000..66021f3 --- /dev/null +++ b/.hypothesis/constants/cba1d6001768c887 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/banking/send_money.py +# hypothesis_version: 6.151.4 + +['USD', 'action', 'amount', 'amount_sent', 'app', 'appName', 'available_contacts', 'banking', 'code', 'currency', 'current_balance', 'date', 'description', 'error', 'id', 'insufficient_funds', 'invalid_amount', 'message', 'money_sent', 'name', 'new_balance', 'number', 'object', 'properties', 'props', 'recipient', 'requested_amount', 'required', 'schema', 'send_money', 'shortDescription', 'shortfall', 'state', 'string', 'success', 'transaction', 'transaction_id', 'type', 'unknown_recipient'] \ No newline at end of file diff --git a/.hypothesis/constants/e10c1ba21899995c b/.hypothesis/constants/e10c1ba21899995c new file mode 100644 index 0000000..51e205d --- /dev/null +++ b/.hypothesis/constants/e10c1ba21899995c @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/base/registry.py +# hypothesis_version: 6.151.4 + +['name'] \ No newline at end of file diff --git a/.hypothesis/constants/f0490af62f47cfb8 b/.hypothesis/constants/f0490af62f47cfb8 new file mode 100644 index 0000000..f9d6dd7 --- /dev/null +++ b/.hypothesis/constants/f0490af62f47cfb8 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/banking/check_balance.py +# hypothesis_version: 6.151.4 + +['USD', 'action', 'app', 'appName', 'balance', 'banking', 'check_balance', 'currency', 'description', 'formatted_balance', 'highlight_balance', 'name', 'object', 'properties', 'props', 'required', 'schema', 'shortDescription', 'state', 'type'] \ No newline at end of file diff --git a/.hypothesis/constants/f373dd0371756678 b/.hypothesis/constants/f373dd0371756678 new file mode 100644 index 0000000..edf8d1c --- /dev/null +++ b/.hypothesis/constants/f373dd0371756678 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/banking/get_transactions.py +# hypothesis_version: 6.151.4 + +['USD', 'action', 'amount', 'app', 'appName', 'banking', 'count', 'currency', 'date', 'description', 'formatted_amount', 'get_transactions', 'id', 'incoming', 'integer', 'limit', 'name', 'object', 'outgoing', 'properties', 'props', 'recipient', 'required', 'schema', 'shortDescription', 'show_transactions', 'state', 'total_incoming', 'total_outgoing', 'transactions', 'type'] \ No newline at end of file diff --git a/.hypothesis/examples/04e6b3400353b141/9dd9fef11db189ef b/.hypothesis/examples/04e6b3400353b141/9dd9fef11db189ef new file mode 100644 index 0000000..1c3571e Binary files /dev/null and b/.hypothesis/examples/04e6b3400353b141/9dd9fef11db189ef differ diff --git a/.hypothesis/examples/04e6b3400353b141/f4627fbd0dc150fc b/.hypothesis/examples/04e6b3400353b141/f4627fbd0dc150fc new file mode 100644 index 0000000..a1d9871 Binary files /dev/null and b/.hypothesis/examples/04e6b3400353b141/f4627fbd0dc150fc differ diff --git a/.hypothesis/examples/9dd9fef11db189ef/7faca4740e7696f5 b/.hypothesis/examples/9dd9fef11db189ef/7faca4740e7696f5 new file mode 100644 index 0000000..7568472 Binary files /dev/null and b/.hypothesis/examples/9dd9fef11db189ef/7faca4740e7696f5 differ diff --git a/.hypothesis/examples/f4627fbd0dc150fc/8d2e19448e90e8e0 b/.hypothesis/examples/f4627fbd0dc150fc/8d2e19448e90e8e0 new file mode 100644 index 0000000..a988ce2 Binary files /dev/null and b/.hypothesis/examples/f4627fbd0dc150fc/8d2e19448e90e8e0 differ diff --git a/.hypothesis/unicode_data/15.1.0/charmap.json.gz b/.hypothesis/unicode_data/15.1.0/charmap.json.gz new file mode 100644 index 0000000..6e5661c Binary files /dev/null and b/.hypothesis/unicode_data/15.1.0/charmap.json.gz differ diff --git a/.kiro/specs/voice-banking-assistant/design.md b/.kiro/specs/voice-banking-assistant/design.md new file mode 100644 index 0000000..7625bcd --- /dev/null +++ b/.kiro/specs/voice-banking-assistant/design.md @@ -0,0 +1,433 @@ +# Design Document: Voice Banking Assistant + +## Overview + +This design transforms the existing Nova Sonic voice agent demo into a voice-powered banking assistant. The architecture leverages the existing tool system where backend Python tools handle business logic and return both model results (for the AI to speak) and UI results (for visual updates). The frontend will be restructured to show a banking dashboard on the left panel while maintaining the voice chat on the right. + +The key insight is that we'll use the existing `type: 'app'` tool output pattern to render a custom `BankingApp` component that maintains its own state and receives updates via props from tool executions. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Frontend (Next.js) │ +├─────────────────────────────────┬───────────────────────────────────┤ +│ Banking Dashboard │ Voice Chat Panel │ +│ ┌───────────────────────────┐ │ ┌─────────────────────────────┐ │ +│ │ Account Balance Card │ │ │ Model/Language Select │ │ +│ │ $5,000.00 │ │ │ Connect/Disconnect │ │ +│ └───────────────────────────┘ │ │ Record/Stop │ │ +│ ┌───────────────────────────┐ │ ├─────────────────────────────┤ │ +│ │ Quick Actions │ │ │ │ │ +│ │ [Contacts List] │ │ │ Chat Messages │ │ +│ └───────────────────────────┘ │ │ (User & Assistant) │ │ +│ ┌───────────────────────────┐ │ │ │ │ +│ │ Recent Transactions │ │ └─────────────────────────────┘ │ +│ │ - Coffee Shop -$4.50 │ │ │ +│ │ - Salary +$3,000 │ │ │ +│ └───────────────────────────┘ │ │ +│ ┌───────────────────────────┐ │ │ +│ │ Action History │ │ │ +│ │ [Voice command log] │ │ │ +│ └───────────────────────────┘ │ │ +└─────────────────────────────────┴───────────────────────────────────┘ + │ + │ WebSocket + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Backend (FastAPI) │ +├─────────────────────────────────────────────────────────────────────┤ +│ ToolManager │ +│ ├── CheckBalanceTool │ +│ ├── SendMoneyTool │ +│ ├── GetTransactionsTool │ +│ └── GetContactsTool │ +├─────────────────────────────────────────────────────────────────────┤ +│ BankingState (In-Memory) │ +│ ├── balance: float │ +│ ├── transactions: List[Transaction] │ +│ ├── contacts: List[Contact] │ +│ └── action_history: List[Action] │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Components and Interfaces + +### Backend Components + +#### 1. BankingState (Singleton) + +A shared in-memory state manager that holds all banking data. This is a singleton so all tools access the same state. + +```python +# backend/tools/categories/banking/state.py + +from dataclasses import dataclass, field +from typing import List, Optional +from datetime import datetime +import uuid + +@dataclass +class Transaction: + id: str + description: str + amount: float # Positive for incoming, negative for outgoing + date: datetime + recipient: Optional[str] = None + +@dataclass +class Contact: + id: str + name: str + avatar_color: str # For UI display + +@dataclass +class ActionHistoryEntry: + id: str + action: str + description: str + timestamp: datetime + +class BankingState: + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._initialize() + return cls._instance + + def _initialize(self): + self.balance: float = 5000.00 + self.transactions: List[Transaction] = [] + self.contacts: List[Contact] = [] + self.action_history: List[ActionHistoryEntry] = [] + self._seed_mock_data() + + def _seed_mock_data(self): + # Initialize with mock data + pass + + def get_state_snapshot(self) -> dict: + # Return current state for UI + pass + + def add_transaction(self, description: str, amount: float, recipient: str = None) -> Transaction: + # Add new transaction and update balance + pass + + def log_action(self, action: str, description: str) -> ActionHistoryEntry: + # Log voice action to history + pass +``` + +#### 2. Banking Tools + +Each tool follows the existing `BaseTool` pattern, returning `model_result` for the AI and `ui_result` for the frontend. + +```python +# Tool: check_balance +class CheckBalanceTool(BaseTool): + config = { + "name": "check_balance", + "description": "Check the user's current account balance", + "schema": {"type": "object", "properties": {}, "required": []} + } + + async def execute(self, content: dict) -> dict: + state = BankingState() + state.log_action("check_balance", "Checked account balance") + + return self.format_response( + model_result={"balance": state.balance, "currency": "USD"}, + ui_result={ + "type": "app", + "appName": "banking", + "props": { + "action": "highlight_balance", + "state": state.get_state_snapshot() + } + } + ) +``` + +```python +# Tool: send_money +class SendMoneyTool(BaseTool): + config = { + "name": "send_money", + "description": "Send money to a contact. Requires recipient name and amount.", + "schema": { + "type": "object", + "properties": { + "recipient": {"type": "string", "description": "Name of the recipient"}, + "amount": {"type": "number", "description": "Amount to send in USD"} + }, + "required": ["recipient", "amount"] + } + } + + async def execute(self, content: dict) -> dict: + state = BankingState() + recipient = content.get("recipient") + amount = content.get("amount") + + # Validation logic + # Create transaction + # Update balance + # Log action + + return self.format_response( + model_result={"success": True, "new_balance": state.balance, ...}, + ui_result={ + "type": "app", + "appName": "banking", + "props": { + "action": "money_sent", + "transaction": {...}, + "state": state.get_state_snapshot() + } + } + ) +``` + +### Frontend Components + +#### 1. BankingApp Component + +The main banking dashboard component that receives state updates via props. + +```typescript +// frontend/components/apps/BankingApp.tsx + +interface BankingAppProps { + action?: 'highlight_balance' | 'money_sent' | 'show_transactions'; + state?: BankingState; + transaction?: Transaction; +} + +interface BankingState { + balance: number; + transactions: Transaction[]; + contacts: Contact[]; + actionHistory: ActionHistoryEntry[]; +} +``` + +#### 2. Sub-components + +- `BalanceCard`: Displays current balance with highlight animation +- `TransactionList`: Shows recent transactions with add animation +- `ContactsList`: Shows available transfer recipients +- `ActionHistory`: Shows log of voice-triggered actions + +### Frontend State Management + +The banking state will be managed in two ways: +1. **Initial state**: Fetched via REST API on component mount +2. **Updates**: Received via WebSocket tool outputs that include the full state snapshot + +This ensures the UI always reflects the backend state after each voice command. + +## Data Models + +### Transaction +```typescript +interface Transaction { + id: string; + description: string; + amount: number; // Positive = incoming, negative = outgoing + date: string; // ISO date string + recipient?: string; // For outgoing transfers +} +``` + +### Contact +```typescript +interface Contact { + id: string; + name: string; + avatarColor: string; // Tailwind color class +} +``` + +### ActionHistoryEntry +```typescript +interface ActionHistoryEntry { + id: string; + action: string; // Tool name + description: string; // Human-readable description + timestamp: string; // ISO date string +} +``` + +### BankingState +```typescript +interface BankingState { + balance: number; + transactions: Transaction[]; + contacts: Contact[]; + actionHistory: ActionHistoryEntry[]; +} +``` + +## Correctness Properties + +*A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* + +### Property 1: Transaction Amount Formatting + +*For any* transaction with a positive amount, the formatted display string SHALL contain a "+" prefix and green color indicator. *For any* transaction with a negative amount, the formatted display string SHALL contain a "-" prefix and red color indicator. + +**Validates: Requirements 1.5, 1.6** + +### Property 2: Transaction List Completeness + +*For any* list of transactions provided to the Banking_UI, the rendered output SHALL contain the date, description, and amount for each transaction in the list. + +**Validates: Requirements 1.2** + +### Property 3: Contacts List Completeness + +*For any* list of contacts provided to the Banking_UI, the rendered output SHALL display all contact names. + +**Validates: Requirements 1.3** + +### Property 4: Check Balance Accuracy + +*For any* banking state with balance B, executing the check_balance tool SHALL return a model_result containing balance equal to B. + +**Validates: Requirements 2.2** + +### Property 5: Send Money Success Invariants + +*For any* valid send_money request with recipient R in contacts and amount A where A ≤ balance: +- The new balance SHALL equal (old balance - A) +- A new transaction SHALL be created with amount -A and recipient R +- The ui_result state SHALL reflect the updated balance and include the new transaction + +**Validates: Requirements 3.2, 3.3, 3.4, 3.5** + +### Property 6: Send Money Insufficient Funds + +*For any* send_money request with amount A where A > current balance, the tool SHALL return an error and the banking state (balance, transactions) SHALL remain unchanged. + +**Validates: Requirements 3.6** + +### Property 7: Send Money Unknown Recipient + +*For any* send_money request with recipient R where R is not in the contacts list, the tool SHALL return an error containing the list of valid contacts, and the banking state SHALL remain unchanged. + +**Validates: Requirements 3.7** + +### Property 8: Get Transactions Completeness + +*For any* banking state with transactions list T, executing get_transactions SHALL return all transactions in T. + +**Validates: Requirements 4.2** + +### Property 9: Action History Growth + +*For any* tool execution, the action_history list SHALL grow by exactly one entry containing the tool name and a timestamp. + +**Validates: Requirements 5.2** + +### Property 10: Action History Ordering + +*For any* action_history list with multiple entries, entries SHALL be ordered by timestamp in descending order (newest first). + +**Validates: Requirements 5.3** + +## Error Handling + +### Backend Error Handling + +1. **Insufficient Funds**: When `send_money` is called with amount > balance: + - Return `model_result` with `success: false` and `error: "insufficient_funds"` + - Include current balance and requested amount in error details + - Do not modify state + +2. **Unknown Recipient**: When `send_money` is called with unknown recipient: + - Return `model_result` with `success: false` and `error: "unknown_recipient"` + - Include list of valid contact names + - Do not modify state + +3. **Invalid Amount**: When `send_money` is called with amount ≤ 0: + - Return `model_result` with `success: false` and `error: "invalid_amount"` + - Do not modify state + +### Frontend Error Handling + +1. **WebSocket Disconnection**: Show reconnection UI, disable voice controls +2. **Tool Execution Failure**: Display error message in action history +3. **State Sync Failure**: Fall back to last known good state + +## Testing Strategy + +### Unit Tests + +Unit tests will verify specific examples and edge cases: + +1. **Initial State Tests**: + - Verify mock data initialization (balance = $5000, 5-10 transactions, 3-5 contacts) + - Verify transactions include both positive and negative amounts + +2. **UI Rendering Tests**: + - Verify balance card renders with correct formatting + - Verify transaction list renders all items + - Verify action history panel displays entries + +3. **Edge Case Tests**: + - Send exactly the full balance (should succeed) + - Send $0 (should fail) + - Send to recipient with case-insensitive matching + +### Property-Based Tests + +Property-based tests will use **Hypothesis** (Python) for backend and **fast-check** (TypeScript) for frontend, with minimum 100 iterations per property. + +Each property test will be tagged with: **Feature: voice-banking-assistant, Property {number}: {property_text}** + +1. **Backend Property Tests** (Hypothesis): + - Property 4: Check balance accuracy + - Property 5: Send money success invariants + - Property 6: Insufficient funds handling + - Property 7: Unknown recipient handling + - Property 8: Get transactions completeness + - Property 9: Action history growth + - Property 10: Action history ordering + +2. **Frontend Property Tests** (fast-check): + - Property 1: Transaction amount formatting + - Property 2: Transaction list completeness + - Property 3: Contacts list completeness + +### Test Data Generators + +```python +# Backend generators (Hypothesis) +from hypothesis import strategies as st + +valid_amount = st.floats(min_value=0.01, max_value=10000, allow_nan=False) +contact_name = st.sampled_from(["Alice", "Bob", "Charlie", "Diana", "Eve"]) +transaction = st.builds( + Transaction, + id=st.uuids().map(str), + description=st.text(min_size=1, max_size=50), + amount=st.floats(min_value=-5000, max_value=5000, allow_nan=False), + date=st.datetimes() +) +``` + +```typescript +// Frontend generators (fast-check) +import * as fc from 'fast-check'; + +const transactionArb = fc.record({ + id: fc.uuid(), + description: fc.string({ minLength: 1, maxLength: 50 }), + amount: fc.double({ min: -5000, max: 5000, noNaN: true }), + date: fc.date().map(d => d.toISOString()) +}); +``` + diff --git a/.kiro/specs/voice-banking-assistant/requirements.md b/.kiro/specs/voice-banking-assistant/requirements.md new file mode 100644 index 0000000..6afefce --- /dev/null +++ b/.kiro/specs/voice-banking-assistant/requirements.md @@ -0,0 +1,101 @@ +# Requirements Document + +## Introduction + +This feature transforms the existing Nova Sonic voice agent demo into a voice-powered banking assistant. The system will present a modern, Revolut-style banking interface where users can perform common banking operations through voice commands. The voice agent will execute banking functions (check balance, send money, view transactions) and the UI will reflect these actions in real-time. A transaction/action history panel will display all voice-triggered operations for demo purposes. + +## Glossary + +- **Voice_Agent**: The Nova Sonic speech-to-speech AI that processes voice commands and responds with voice and text +- **Banking_UI**: The frontend React application displaying account information, balances, and transaction history +- **Tool**: A backend function that the Voice_Agent can invoke to perform banking operations +- **Transaction**: A record of money movement (sent or received) between accounts +- **Contact**: A predefined recipient that the user can send money to +- **Action_History**: A log of all voice-triggered banking operations performed during the session +- **Account_State**: The in-memory representation of the user's banking data (balance, transactions, contacts) + +## Requirements + +### Requirement 1: Display Banking Dashboard + +**User Story:** As a user, I want to see my banking dashboard with account balance and recent transactions, so that I can understand my financial status at a glance. + +#### Acceptance Criteria + +1. WHEN the Banking_UI loads, THE Banking_UI SHALL display the user's current account balance prominently +2. WHEN the Banking_UI loads, THE Banking_UI SHALL display a list of recent transactions with date, description, and amount +3. WHEN the Banking_UI loads, THE Banking_UI SHALL display a list of contacts available for money transfers +4. THE Banking_UI SHALL use a modern, clean design aesthetic similar to Revolut (dark/light theme, card-based layout) +5. WHEN a transaction amount is positive, THE Banking_UI SHALL display it in green with a "+" prefix +6. WHEN a transaction amount is negative, THE Banking_UI SHALL display it in red with a "-" prefix + +### Requirement 2: Check Balance via Voice + +**User Story:** As a user, I want to ask the voice agent about my account balance, so that I can quickly know how much money I have without navigating the UI. + +#### Acceptance Criteria + +1. WHEN the user asks about their balance via voice, THE Voice_Agent SHALL invoke the check_balance Tool +2. WHEN the check_balance Tool executes, THE Tool SHALL return the current account balance to the Voice_Agent +3. WHEN the check_balance Tool executes, THE Banking_UI SHALL highlight or animate the balance display to draw attention +4. THE Voice_Agent SHALL respond verbally with the current balance amount + +### Requirement 3: Send Money via Voice + +**User Story:** As a user, I want to send money to a contact using voice commands, so that I can perform transfers hands-free. + +#### Acceptance Criteria + +1. WHEN the user requests to send money via voice, THE Voice_Agent SHALL invoke the send_money Tool with recipient and amount parameters +2. WHEN the send_money Tool receives a valid recipient and amount, THE Tool SHALL deduct the amount from the account balance +3. WHEN the send_money Tool receives a valid recipient and amount, THE Tool SHALL create a new transaction record +4. WHEN the send_money Tool executes successfully, THE Banking_UI SHALL update the balance display immediately +5. WHEN the send_money Tool executes successfully, THE Banking_UI SHALL add the new transaction to the transaction list with animation +6. IF the send_money Tool receives an amount greater than the available balance, THEN THE Tool SHALL return an error and not modify the Account_State +7. IF the send_money Tool receives an unknown recipient, THEN THE Tool SHALL return an error listing available contacts +8. THE Voice_Agent SHALL confirm the transfer verbally, stating the amount and recipient + +### Requirement 4: View Transaction History via Voice + +**User Story:** As a user, I want to ask about my recent transactions via voice, so that I can review my spending without looking at the screen. + +#### Acceptance Criteria + +1. WHEN the user asks about recent transactions via voice, THE Voice_Agent SHALL invoke the get_transactions Tool +2. WHEN the get_transactions Tool executes, THE Tool SHALL return the list of recent transactions +3. WHEN the get_transactions Tool executes, THE Banking_UI SHALL scroll to and highlight the transaction list +4. THE Voice_Agent SHALL verbally summarize the recent transactions (last 3-5 transactions) + +### Requirement 5: Display Action History + +**User Story:** As a demo presenter, I want to see a history of all voice-triggered actions, so that I can show viewers what operations were performed. + +#### Acceptance Criteria + +1. THE Banking_UI SHALL display an Action_History panel showing all voice-triggered operations +2. WHEN a Tool executes, THE Banking_UI SHALL add an entry to the Action_History with timestamp and action description +3. THE Action_History SHALL display entries in reverse chronological order (newest first) +4. WHEN a new action is added, THE Banking_UI SHALL animate the entry appearing in the Action_History + +### Requirement 6: Voice Agent Integration + +**User Story:** As a user, I want the voice chat interface to remain accessible alongside the banking UI, so that I can interact with the agent while viewing my account. + +#### Acceptance Criteria + +1. THE Banking_UI SHALL display the voice chat panel in a sidebar or overlay position +2. THE Banking_UI SHALL maintain the existing voice recording and playback functionality +3. WHEN the Voice_Agent is speaking, THE Banking_UI SHALL show a visual indicator of agent activity +4. THE Banking_UI SHALL allow the user to start/stop voice recording while viewing the banking dashboard + +### Requirement 7: Mock Data Initialization + +**User Story:** As a demo presenter, I want the application to start with realistic mock data, so that the demo looks authentic. + +#### Acceptance Criteria + +1. WHEN the application starts, THE Account_State SHALL initialize with a predefined balance (e.g., $5,000) +2. WHEN the application starts, THE Account_State SHALL initialize with 5-10 sample transactions +3. WHEN the application starts, THE Account_State SHALL initialize with 3-5 sample contacts +4. THE mock data SHALL reset when the page is refreshed +5. THE sample transactions SHALL include a mix of incoming and outgoing payments with realistic descriptions diff --git a/.kiro/specs/voice-banking-assistant/tasks.md b/.kiro/specs/voice-banking-assistant/tasks.md new file mode 100644 index 0000000..22170a9 --- /dev/null +++ b/.kiro/specs/voice-banking-assistant/tasks.md @@ -0,0 +1,152 @@ +# Implementation Plan: Voice Banking Assistant + +## Overview + +This implementation transforms the existing Nova Sonic voice agent demo into a voice-powered banking assistant. We'll build the backend banking tools first, then create the frontend banking dashboard, and finally wire everything together. The approach ensures each step builds on the previous one with no orphaned code. + +## Tasks + +- [x] 1. Create banking state management and data models + - [x] 1.1 Create the banking module directory structure and data models + - Create `backend/tools/categories/banking/` directory + - Create `models.py` with Transaction, Contact, ActionHistoryEntry dataclasses + - Create `__init__.py` to export models + - _Requirements: 7.1, 7.2, 7.3_ + - [x] 1.2 Implement BankingState singleton with mock data initialization + - Create `state.py` with BankingState class + - Implement `_seed_mock_data()` with $5000 balance, 8 transactions, 4 contacts + - Implement `get_state_snapshot()` method + - _Requirements: 7.1, 7.2, 7.3, 7.5_ + - [x] 1.3 Write property tests for BankingState initialization + - **Property 9: Action History Growth** - verify action logging + - **Property 10: Action History Ordering** - verify chronological order + - **Validates: Requirements 5.2, 5.3** + +- [x] 2. Implement backend banking tools + - [x] 2.1 Implement CheckBalanceTool + - Create `check_balance.py` extending BaseTool + - Return current balance in model_result + - Return app ui_result with highlight_balance action + - Log action to history + - _Requirements: 2.1, 2.2, 2.3_ + - [x] 2.2 Write property test for CheckBalanceTool + - **Property 4: Check Balance Accuracy** + - **Validates: Requirements 2.2** + - [x] 2.3 Implement SendMoneyTool + - Create `send_money.py` extending BaseTool + - Validate recipient exists in contacts + - Validate amount <= balance and amount > 0 + - Deduct amount, create transaction, log action + - Return success with updated state or error + - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7_ + - [x] 2.4 Write property tests for SendMoneyTool + - **Property 5: Send Money Success Invariants** + - **Property 6: Send Money Insufficient Funds** + - **Property 7: Send Money Unknown Recipient** + - **Validates: Requirements 3.2, 3.3, 3.4, 3.5, 3.6, 3.7** + - [x] 2.5 Implement GetTransactionsTool + - Create `get_transactions.py` extending BaseTool + - Return all transactions in model_result + - Return app ui_result with show_transactions action + - Log action to history + - _Requirements: 4.1, 4.2, 4.3_ + - [x] 2.6 Write property test for GetTransactionsTool + - **Property 8: Get Transactions Completeness** + - **Validates: Requirements 4.2** + - [x] 2.7 Register banking tools in ToolManager + - Update `backend/tools/tool_manager.py` to import and register banking tools + - Update `backend/tools/categories/banking/__init__.py` exports + - _Requirements: 2.1, 3.1, 4.1_ + +- [x] 3. Checkpoint - Backend tools complete + - Ensure all backend tests pass + - Verify tools are registered and accessible + - Ask the user if questions arise + +- [x] 4. Create frontend banking components + - [x] 4.1 Create BankingApp main component structure + - Create `frontend/components/apps/BankingApp.tsx` + - Define TypeScript interfaces for BankingState, Transaction, Contact, ActionHistoryEntry + - Create component shell with props interface + - _Requirements: 1.1, 1.2, 1.3, 6.1_ + - [x] 4.2 Implement BalanceCard component + - Create balance display with large font, currency formatting + - Add highlight animation when action is 'highlight_balance' + - Use Revolut-style dark card design + - _Requirements: 1.1, 1.4, 2.3_ + - [x] 4.3 Implement TransactionList component + - Create scrollable transaction list + - Format amounts with +/- prefix and green/red colors + - Show date, description, amount for each transaction + - Add animation for new transactions + - _Requirements: 1.2, 1.5, 1.6, 3.5_ + - [x] 4.4 Write property tests for transaction formatting + - **Property 1: Transaction Amount Formatting** + - **Property 2: Transaction List Completeness** + - **Validates: Requirements 1.2, 1.5, 1.6** + - [x] 4.5 Implement ContactsList component + - Create horizontal scrollable contact avatars + - Show contact name and colored avatar + - _Requirements: 1.3_ + - [x] 4.6 Write property test for contacts rendering + - **Property 3: Contacts List Completeness** + - **Validates: Requirements 1.3** + - [x] 4.7 Implement ActionHistory component + - Create collapsible action history panel + - Show action name, description, timestamp + - Display in reverse chronological order + - Add slide-in animation for new entries + - _Requirements: 5.1, 5.2, 5.3, 5.4_ + +- [x] 5. Integrate BankingApp with tool output system + - [x] 5.1 Update ToolOutput component to render BankingApp + - Modify `frontend/components/tool-outputs/ToolOutput.tsx` + - Add case for `appName === 'banking'` to render BankingApp + - Pass props from tool output to BankingApp + - _Requirements: 2.3, 3.4, 3.5, 4.3_ + - [x] 5.2 Update types.ts with BankingAppToolOutput interface + - Add BankingAppToolOutput to types + - Export updated ToolOutput union type + - _Requirements: 2.3, 3.4, 4.3_ + +- [x] 6. Update page layout for banking dashboard + - [x] 6.1 Restructure page.tsx layout + - Replace generic tool output area with BankingApp as default view + - Maintain voice chat panel on right side + - Add state management for banking data + - _Requirements: 6.1, 6.2, 6.4_ + - [x] 6.2 Add REST endpoint for initial state fetch + - Create `backend/api/apps/banking.py` with GET /api/banking/state endpoint + - Return initial BankingState snapshot + - _Requirements: 7.1, 7.2, 7.3_ + - [x] 6.3 Implement frontend state initialization + - Fetch initial state on component mount + - Update state when tool outputs are received + - _Requirements: 1.1, 1.2, 1.3, 6.1_ + +- [x] 7. Final integration and polish + - [x] 7.1 Add visual feedback for voice agent activity + - Show speaking indicator when agent is responding + - Pulse animation on balance during check_balance + - _Requirements: 6.3, 2.3_ + - [x] 7.2 Style refinements for Revolut aesthetic + - Apply dark theme option + - Refine card shadows and borders + - Ensure smooth animations + - _Requirements: 1.4_ + +- [x] 8. Final checkpoint - All features complete + - Ensure all tests pass + - Verify voice commands work end-to-end + - Test: check balance, send money, view transactions + - Ask the user if questions arise + +## Notes + +- All tasks including property tests are required +- Each task references specific requirements for traceability +- Checkpoints ensure incremental validation +- Property tests validate universal correctness properties +- Unit tests validate specific examples and edge cases +- The BankingState singleton ensures all tools share the same state +- State resets on page refresh (no persistence needed for demo) diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/backend/.hypothesis/constants/04f2136ccc19ab10 b/backend/.hypothesis/constants/04f2136ccc19ab10 new file mode 100644 index 0000000..6cccbc7 --- /dev/null +++ b/backend/.hypothesis/constants/04f2136ccc19ab10 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/order/__init__.py +# hypothesis_version: 6.151.4 + +['TrackOrderTool'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/0618c790d33baeca b/backend/.hypothesis/constants/0618c790d33baeca new file mode 100644 index 0000000..a2320bc --- /dev/null +++ b/backend/.hypothesis/constants/0618c790d33baeca @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/media/sample_image_tool.py +# hypothesis_version: 6.151.4 + +['Sample Image', 'content', 'description', 'image', 'name', 'object', 'properties', 'required', 'schema', 'shortDescription', 'showSampleImageTool', 'title', 'type', 'url'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/0a00cf04224d1343 b/backend/.hypothesis/constants/0a00cf04224d1343 new file mode 100644 index 0000000..5fb69a4 --- /dev/null +++ b/backend/.hypothesis/constants/0a00cf04224d1343 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/order/track_order_tool.py +# hypothesis_version: 6.151.4 + +[10000, '#', '%Y-%m-%d', 'Current Status', 'Delayed', 'Delivered', 'Distribution Center', 'Enabled', 'Estimated Delivery', 'Front Door', 'In transit', 'Notifications', 'Order received', 'Out for delivery', 'Processing', 'Shipped', 'Track Another Order', 'Tracking an order', 'action', 'additionalInfo', 'boolean', 'card', 'content', 'currentLocation', 'deliveryLocation', 'description', 'details', 'estimatedDelivery', 'footer', 'name', 'notificationStatus', 'object', 'orderId', 'orderNumber', 'orderStatus', 'properties', 'requestNotifications', 'required', 'schema', 'shortDescription', 'string', 'text', 'title', 'trackOrderTool', 'type', 'url'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/0a746ffa7128899a b/backend/.hypothesis/constants/0a746ffa7128899a new file mode 100644 index 0000000..2ec1838 --- /dev/null +++ b/backend/.hypothesis/constants/0a746ffa7128899a @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/banking/state.py +# hypothesis_version: 6.151.4 + +[-120.0, -85.23, -45.8, -14.99, -4.5, 29.99, 500.0, 3000.0, 5000.0, 'Alice', 'BankingState', 'Bob', 'Charlie', 'Coffee Shop', 'Diana', 'Electric Bill', 'Freelance Payment', 'Grocery Store', 'Power Company', 'Restaurant', 'Salary Deposit', 'Subscription Service', 'action', 'actionHistory', 'amount', 'avatarColor', 'balance', 'blue', 'contacts', 'date', 'description', 'green', 'id', 'name', 'orange', 'purple', 'recipient', 'timestamp', 'transactions'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/33d8627ad75175ce b/backend/.hypothesis/constants/33d8627ad75175ce new file mode 100644 index 0000000..51830de --- /dev/null +++ b/backend/.hypothesis/constants/33d8627ad75175ce @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/banking/__init__.py +# hypothesis_version: 6.151.4 + +['ActionHistoryEntry', 'BankingState', 'CheckBalanceTool', 'Contact', 'GetTransactionsTool', 'SendMoneyTool', 'Transaction'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/4c4ee5bb1d98c4c9 b/backend/.hypothesis/constants/4c4ee5bb1d98c4c9 new file mode 100644 index 0000000..6c166df --- /dev/null +++ b/backend/.hypothesis/constants/4c4ee5bb1d98c4c9 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/utility/__init__.py +# hypothesis_version: 6.151.4 + +['DateAndTimeTool'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/605aa399669e812f b/backend/.hypothesis/constants/605aa399669e812f new file mode 100644 index 0000000..cc93c26 --- /dev/null +++ b/backend/.hypothesis/constants/605aa399669e812f @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/utility/datetime_tool.py +# hypothesis_version: 6.151.4 + +['#', '%A', '%A, %B %d, %Y', '%B', '%I:%M %p', '%Y-%m-%d', 'America/Los_Angeles', 'Current Date & Time', 'Date', 'Day of Week', 'Month', 'PST', 'Refresh', 'Time', 'Time Zone', 'Year', 'action', 'card', 'content', 'date', 'day', 'dayOfWeek', 'description', 'details', 'footer', 'formattedTime', 'getDateAndTimeTool', 'image', 'imageAlt', 'month', 'name', 'object', 'properties', 'required', 'schema', 'shortDescription', 'text', 'timezone', 'title', 'type', 'url', 'year'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/63bd53c5a363bc27 b/backend/.hypothesis/constants/63bd53c5a363bc27 new file mode 100644 index 0000000..fc0765b --- /dev/null +++ b/backend/.hypothesis/constants/63bd53c5a363bc27 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/banking/models.py +# hypothesis_version: 6.151.4 + +[] \ No newline at end of file diff --git a/backend/.hypothesis/constants/75356c4fc73869a0 b/backend/.hypothesis/constants/75356c4fc73869a0 new file mode 100644 index 0000000..d73fa87 --- /dev/null +++ b/backend/.hypothesis/constants/75356c4fc73869a0 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/banking/__init__.py +# hypothesis_version: 6.151.4 + +['ActionHistoryEntry', 'BankingState', 'Contact', 'Transaction'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/818cf4bd8172ecb2 b/backend/.hypothesis/constants/818cf4bd8172ecb2 new file mode 100644 index 0000000..56e47b9 --- /dev/null +++ b/backend/.hypothesis/constants/818cf4bd8172ecb2 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/__init__.py +# hypothesis_version: 6.151.4 + +['ToolManager'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/94611bd473223f81 b/backend/.hypothesis/constants/94611bd473223f81 new file mode 100644 index 0000000..df21d80 --- /dev/null +++ b/backend/.hypothesis/constants/94611bd473223f81 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/tool_manager.py +# hypothesis_version: 6.151.4 + +['description', 'inputSchema', 'json', 'name', 'schema', 'toolSpec'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/9b47bf4b51f4c068 b/backend/.hypothesis/constants/9b47bf4b51f4c068 new file mode 100644 index 0000000..b6fda0c --- /dev/null +++ b/backend/.hypothesis/constants/9b47bf4b51f4c068 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/base/__init__.py +# hypothesis_version: 6.151.4 + +['BaseTool', 'ToolRegistry'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/a3e01f3d677858a5 b/backend/.hypothesis/constants/a3e01f3d677858a5 new file mode 100644 index 0000000..6846a9d --- /dev/null +++ b/backend/.hypothesis/constants/a3e01f3d677858a5 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/media/sample_pdf_tool.py +# hypothesis_version: 6.151.4 + +['Sample PDF Document', 'Showing a sample PDF', 'content', 'description', 'name', 'object', 'pdf', 'properties', 'required', 'schema', 'shortDescription', 'showSamplePdfTool', 'text', 'title', 'type', 'url'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/b9ca80631071fc5e b/backend/.hypothesis/constants/b9ca80631071fc5e new file mode 100644 index 0000000..219bf95 --- /dev/null +++ b/backend/.hypothesis/constants/b9ca80631071fc5e @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/media/sample_video_tool.py +# hypothesis_version: 6.151.4 + +['&controls=0', 'Error', 'Show a YouTube video', 'YouTube Video', '^[A-Za-z0-9_-]{11}$', 'a9__D53WsUs', 'boolean', 'content', 'description', 'error', 'message', 'name', 'object', 'pattern', 'properties', 'schema', 'shortDescription', 'showControls', 'showSampleVideoTool', 'string', 'text', 'title', 'type', 'url', 'video', 'videoId'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/c055083c70179d96 b/backend/.hypothesis/constants/c055083c70179d96 new file mode 100644 index 0000000..a2eb6f6 --- /dev/null +++ b/backend/.hypothesis/constants/c055083c70179d96 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/base/tool.py +# hypothesis_version: 6.151.4 + +['description', 'model_result', 'name', 'object', 'properties', 'required', 'schema', 'shortDescription', 'type', 'ui_result'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/c52b48c8f3cf87ef b/backend/.hypothesis/constants/c52b48c8f3cf87ef new file mode 100644 index 0000000..a57afb3 --- /dev/null +++ b/backend/.hypothesis/constants/c52b48c8f3cf87ef @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/media/__init__.py +# hypothesis_version: 6.151.4 + +['SampleImageTool', 'SamplePdfTool', 'SampleVideoTool'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/ca868d2b6ff0d5f2 b/backend/.hypothesis/constants/ca868d2b6ff0d5f2 new file mode 100644 index 0000000..df21d80 --- /dev/null +++ b/backend/.hypothesis/constants/ca868d2b6ff0d5f2 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/tool_manager.py +# hypothesis_version: 6.151.4 + +['description', 'inputSchema', 'json', 'name', 'schema', 'toolSpec'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/cba1d6001768c887 b/backend/.hypothesis/constants/cba1d6001768c887 new file mode 100644 index 0000000..66021f3 --- /dev/null +++ b/backend/.hypothesis/constants/cba1d6001768c887 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/banking/send_money.py +# hypothesis_version: 6.151.4 + +['USD', 'action', 'amount', 'amount_sent', 'app', 'appName', 'available_contacts', 'banking', 'code', 'currency', 'current_balance', 'date', 'description', 'error', 'id', 'insufficient_funds', 'invalid_amount', 'message', 'money_sent', 'name', 'new_balance', 'number', 'object', 'properties', 'props', 'recipient', 'requested_amount', 'required', 'schema', 'send_money', 'shortDescription', 'shortfall', 'state', 'string', 'success', 'transaction', 'transaction_id', 'type', 'unknown_recipient'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/e10c1ba21899995c b/backend/.hypothesis/constants/e10c1ba21899995c new file mode 100644 index 0000000..51e205d --- /dev/null +++ b/backend/.hypothesis/constants/e10c1ba21899995c @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/base/registry.py +# hypothesis_version: 6.151.4 + +['name'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/f0490af62f47cfb8 b/backend/.hypothesis/constants/f0490af62f47cfb8 new file mode 100644 index 0000000..f9d6dd7 --- /dev/null +++ b/backend/.hypothesis/constants/f0490af62f47cfb8 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/banking/check_balance.py +# hypothesis_version: 6.151.4 + +['USD', 'action', 'app', 'appName', 'balance', 'banking', 'check_balance', 'currency', 'description', 'formatted_balance', 'highlight_balance', 'name', 'object', 'properties', 'props', 'required', 'schema', 'shortDescription', 'state', 'type'] \ No newline at end of file diff --git a/backend/.hypothesis/constants/f373dd0371756678 b/backend/.hypothesis/constants/f373dd0371756678 new file mode 100644 index 0000000..edf8d1c --- /dev/null +++ b/backend/.hypothesis/constants/f373dd0371756678 @@ -0,0 +1,4 @@ +# file: /Users/alfonaz/Desktop/Projects/sonicDemo1/sample-nova-sonic-agentic-chatbot/backend/tools/categories/banking/get_transactions.py +# hypothesis_version: 6.151.4 + +['USD', 'action', 'amount', 'app', 'appName', 'banking', 'count', 'currency', 'date', 'description', 'formatted_amount', 'get_transactions', 'id', 'incoming', 'integer', 'limit', 'name', 'object', 'outgoing', 'properties', 'props', 'recipient', 'required', 'schema', 'shortDescription', 'show_transactions', 'state', 'total_incoming', 'total_outgoing', 'transactions', 'type'] \ No newline at end of file diff --git a/backend/.hypothesis/unicode_data/15.1.0/charmap.json.gz b/backend/.hypothesis/unicode_data/15.1.0/charmap.json.gz new file mode 100644 index 0000000..81ba90c Binary files /dev/null and b/backend/.hypothesis/unicode_data/15.1.0/charmap.json.gz differ diff --git a/backend/api/apps/__init__.py b/backend/api/apps/__init__.py index 45a2818..b71e1a7 100644 --- a/backend/api/apps/__init__.py +++ b/backend/api/apps/__init__.py @@ -1,3 +1,5 @@ from .echo import router as echo_router +from .banking import router as banking_router +from .finance import router as finance_router -routers = [echo_router] \ No newline at end of file +routers = [echo_router, banking_router, finance_router] \ No newline at end of file diff --git a/backend/api/apps/banking.py b/backend/api/apps/banking.py new file mode 100644 index 0000000..7d2e4d2 --- /dev/null +++ b/backend/api/apps/banking.py @@ -0,0 +1,22 @@ +""" +REST API endpoint for banking state. +""" +from fastapi import APIRouter + +from tools.categories.banking.state import BankingState + +router = APIRouter() + + +@router.get("/api/banking/state") +async def get_banking_state(): + """ + Get the current banking state snapshot. + + Returns the initial BankingState including balance, transactions, + contacts, and action history. + + Requirements: 7.1, 7.2, 7.3 + """ + state = BankingState() + return state.get_state_snapshot() diff --git a/backend/api/apps/finance.py b/backend/api/apps/finance.py new file mode 100644 index 0000000..ef86ea3 --- /dev/null +++ b/backend/api/apps/finance.py @@ -0,0 +1,20 @@ +""" +REST API endpoint for finance state. +""" +from fastapi import APIRouter + +from tools.categories.finance.state import FinanceState + +router = APIRouter() + + +@router.get("/api/finance/state") +async def get_finance_state(): + """ + Get the current finance state snapshot. + + Returns the FinanceState including balance, savings, transactions, + contacts, budgets, bills, goals, and action history. + """ + state = FinanceState() + return state.get_state_snapshot() diff --git a/backend/main.py b/backend/main.py index 1754be9..add6499 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,7 +1,7 @@ from fastapi import FastAPI, WebSocket from fastapi.middleware.cors import CORSMiddleware import asyncio -from nova_sonic_simple import SimpleNovaSonic +from nova_sonic_simple import SimpleNovaSonic, CHUNK_SIZE import logging import os import wave @@ -185,14 +185,14 @@ async def handle_tool_use(self, event_data): if self.active_connection: await self.active_connection.send_text(json.dumps(error_result)) - async def connect(self, websocket: WebSocket): + async def connect(self, websocket: WebSocket, model_id: str = "amazon.nova-sonic-v1:0", language: str = "en"): await websocket.accept() self.active_connection = websocket - logger.info("WebSocket connection accepted") + logger.info(f"WebSocket connection accepted with model: {model_id}, language: {language}") - self.nova_client = SimpleNovaSonic() + self.nova_client = SimpleNovaSonic(model_id=model_id, language=language) await self.nova_client.start_session() - logger.info("Nova Sonic session started") + logger.info(f"Nova Sonic session started with model: {model_id}, language: {language}") # --- Send conversation history after system prompt --- history = self.get_history() @@ -454,8 +454,11 @@ async def handle_ui_interaction(self, interaction_data): @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): - logger.info("New WebSocket connection request") - await manager.connect(websocket) + # Get model and language from query parameters + model_id = websocket.query_params.get("model", "amazon.nova-sonic-v1:0") + language = websocket.query_params.get("language", "en") + logger.info(f"New WebSocket connection request with model: {model_id}, language: {language}") + await manager.connect(websocket, model_id=model_id, language=language) # Send tool configurations tool_configs = manager.nova_client.tool_manager.get_tool_configs() if manager.nova_client else [] diff --git a/backend/nova_sonic_simple.py b/backend/nova_sonic_simple.py index 492abba..19dfd58 100644 --- a/backend/nova_sonic_simple.py +++ b/backend/nova_sonic_simple.py @@ -46,9 +46,10 @@ def get_aws_credentials_resolver(): return EnvironmentCredentialsResolver() class SimpleNovaSonic: - def __init__(self, model_id='amazon.nova-sonic-v1:0', region='us-east-1'): + def __init__(self, model_id='amazon.nova-sonic-v1:0', region='us-east-1', language='en'): self.model_id = model_id self.region = region + self.language = language self.client = None self.stream = None self.response = None @@ -156,9 +157,22 @@ async def start_session(self): ''' await self.send_event(text_content_start) - system_prompt = "You are a friendly assistant. The user and you will engage in a spoken dialog " \ - "exchanging the transcripts of a natural real-time conversation. Keep your responses short, " \ - "generally two or three sentences for chatty scenarios. Important:If you are using a tool, mention that you are gathering information." + # Language-specific instructions + language_names = { + 'en': 'English', + 'es': 'Spanish', + 'de': 'German', + 'fr': 'French', + 'it': 'Italian', + 'pt': 'Portuguese', + 'hi': 'Hindi' + } + language_name = language_names.get(self.language, 'English') + + system_prompt = f"You are a friendly assistant. The user and you will engage in a spoken dialog " \ + f"exchanging the transcripts of a natural real-time conversation. Keep your responses short, " \ + f"generally two or three sentences for chatty scenarios. Important: If you are using a tool, mention that you are gathering information. " \ + f"IMPORTANT: You MUST respond in {language_name} language only. The user will speak in {language_name} and you must reply in {language_name}." text_input = f''' {{ diff --git a/backend/requirements.txt b/backend/requirements.txt index 52fb992..7bc712b 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -33,3 +33,6 @@ typing_extensions==4.13.2 urllib3==2.4.0 uvicorn==0.34.2 websockets==15.0.1 +hypothesis==6.100.0 +pytest==8.2.0 +pytest-asyncio==0.23.6 diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..09d41c4 --- /dev/null +++ b/backend/tests/__init__.py @@ -0,0 +1 @@ +# Backend tests package diff --git a/backend/tests/test_banking_state.py b/backend/tests/test_banking_state.py new file mode 100644 index 0000000..48cab49 --- /dev/null +++ b/backend/tests/test_banking_state.py @@ -0,0 +1,516 @@ +""" +Property-based tests for BankingState. + +Feature: voice-banking-assistant +Tests Property 9: Action History Growth and Property 10: Action History Ordering +Validates: Requirements 5.2, 5.3 +""" +import pytest +from hypothesis import given, strategies as st, settings +from datetime import datetime +import time + +import sys +sys.path.insert(0, 'backend') + +from tools.categories.banking.models import ActionHistoryEntry +from tools.categories.banking.state import BankingState + + +@pytest.fixture(autouse=True) +def reset_banking_state(): + """Reset the BankingState singleton before each test.""" + BankingState.reset() + yield + BankingState.reset() + + +# Strategies for generating test data +action_name_strategy = st.sampled_from([ + "check_balance", + "send_money", + "get_transactions", + "get_contacts" +]) + +description_strategy = st.text( + min_size=1, + max_size=100, + alphabet=st.characters(whitelist_categories=('L', 'N', 'P', 'Z')) +) + + +class TestActionHistoryGrowth: + """ + Property 9: Action History Growth + + For any tool execution, the action_history list SHALL grow by exactly + one entry containing the tool name and a timestamp. + + **Feature: voice-banking-assistant, Property 9: Action History Growth** + **Validates: Requirements 5.2** + """ + + @given( + action=action_name_strategy, + description=description_strategy + ) + @settings(max_examples=100) + def test_action_history_grows_by_one(self, action: str, description: str): + """ + Property 9: Action History Growth + + For any action logged, the action_history list SHALL grow by exactly one entry. + """ + # Reset state at the start of each hypothesis example + BankingState.reset() + state = BankingState() + initial_count = len(state.action_history) + + # Log an action + entry = state.log_action(action, description) + + # Verify the list grew by exactly one + assert len(state.action_history) == initial_count + 1 + + # Verify the entry contains the tool name + assert entry.action == action + + # Verify the entry has a timestamp + assert isinstance(entry.timestamp, datetime) + + # Verify the entry is in the history + assert entry in state.action_history + + @given( + actions=st.lists( + st.tuples(action_name_strategy, description_strategy), + min_size=1, + max_size=10 + ) + ) + @settings(max_examples=100) + def test_multiple_actions_grow_history_correctly(self, actions): + """ + Property 9 extended: Multiple actions grow history by the correct count. + """ + # Reset state at the start of each hypothesis example + BankingState.reset() + state = BankingState() + initial_count = len(state.action_history) + + for action, description in actions: + state.log_action(action, description) + + # Verify the list grew by exactly the number of actions + assert len(state.action_history) == initial_count + len(actions) + + +class TestActionHistoryOrdering: + """ + Property 10: Action History Ordering + + For any action_history list with multiple entries, entries SHALL be + ordered by timestamp in descending order (newest first) when retrieved + for display. + + Note: The internal storage is append-only (chronological), but the + get_state_snapshot() returns them in the order they were added. + The UI is responsible for displaying in reverse chronological order. + + **Feature: voice-banking-assistant, Property 10: Action History Ordering** + **Validates: Requirements 5.3** + """ + + @given( + actions=st.lists( + st.tuples(action_name_strategy, description_strategy), + min_size=2, + max_size=10 + ) + ) + @settings(max_examples=100) + def test_action_history_maintains_chronological_order(self, actions): + """ + Property 10: Action History Ordering + + For any sequence of logged actions, entries SHALL be stored in + chronological order (oldest first in the list). + """ + # Reset state at the start of each hypothesis example + BankingState.reset() + state = BankingState() + + entries = [] + for action, description in actions: + entry = state.log_action(action, description) + entries.append(entry) + # Small delay to ensure distinct timestamps + time.sleep(0.001) + + # Verify entries are in chronological order (oldest first) + for i in range(len(state.action_history) - 1): + current = state.action_history[i] + next_entry = state.action_history[i + 1] + # Each entry should have timestamp <= next entry + assert current.timestamp <= next_entry.timestamp + + @given( + actions=st.lists( + st.tuples(action_name_strategy, description_strategy), + min_size=1, + max_size=5 + ) + ) + @settings(max_examples=100) + def test_snapshot_preserves_action_order(self, actions): + """ + Property 10 extended: get_state_snapshot preserves action order. + """ + # Reset state at the start of each hypothesis example + BankingState.reset() + state = BankingState() + initial_history_count = len(state.action_history) + + for action, description in actions: + state.log_action(action, description) + time.sleep(0.001) + + snapshot = state.get_state_snapshot() + action_history = snapshot["actionHistory"] + + # Verify the snapshot contains all logged actions (plus any initial ones) + assert len(action_history) == initial_history_count + len(actions) + + # Verify timestamps are in chronological order + for i in range(len(action_history) - 1): + current_ts = action_history[i]["timestamp"] + next_ts = action_history[i + 1]["timestamp"] + assert current_ts <= next_ts + + +# Import the CheckBalanceTool for property testing +from tools.categories.banking.check_balance import CheckBalanceTool + + +class TestCheckBalanceAccuracy: + """ + Property 4: Check Balance Accuracy + + For any banking state with balance B, executing the check_balance tool + SHALL return a model_result containing balance equal to B. + + **Feature: voice-banking-assistant, Property 4: Check Balance Accuracy** + **Validates: Requirements 2.2** + """ + + @given( + balance=st.floats(min_value=0.0, max_value=100000.0, allow_nan=False, allow_infinity=False) + ) + @settings(max_examples=100) + @pytest.mark.asyncio + async def test_check_balance_returns_accurate_balance(self, balance: float): + """ + Property 4: Check Balance Accuracy + + For any banking state with balance B, executing check_balance SHALL + return model_result containing balance equal to B. + """ + # Reset state and set a specific balance + BankingState.reset() + state = BankingState() + state.balance = balance + + # Execute the check_balance tool + tool = CheckBalanceTool() + result = await tool.execute({}) + + # Verify the model_result contains the correct balance + assert result["model_result"]["balance"] == balance + assert result["model_result"]["currency"] == "USD" + + # Verify the ui_result contains the correct state + assert result["ui_result"]["type"] == "app" + assert result["ui_result"]["appName"] == "banking" + assert result["ui_result"]["props"]["action"] == "highlight_balance" + assert result["ui_result"]["props"]["state"]["balance"] == balance + + +# Import the SendMoneyTool for property testing +from tools.categories.banking.send_money import SendMoneyTool + + +# Strategy for valid amounts (positive, reasonable range) +valid_amount_strategy = st.floats(min_value=0.01, max_value=1000.0, allow_nan=False, allow_infinity=False) + +# Strategy for contact names from the mock data +contact_name_strategy = st.sampled_from(["Alice", "Bob", "Charlie", "Diana"]) + + +class TestSendMoneySuccessInvariants: + """ + Property 5: Send Money Success Invariants + + For any valid send_money request with recipient R in contacts and amount A + where A ≤ balance: + - The new balance SHALL equal (old balance - A) + - A new transaction SHALL be created with amount -A and recipient R + - The ui_result state SHALL reflect the updated balance and include the new transaction + + **Feature: voice-banking-assistant, Property 5: Send Money Success Invariants** + **Validates: Requirements 3.2, 3.3, 3.4, 3.5** + """ + + @given( + recipient=contact_name_strategy, + amount=valid_amount_strategy + ) + @settings(max_examples=100) + @pytest.mark.asyncio + async def test_send_money_success_invariants(self, recipient: str, amount: float): + """ + Property 5: Send Money Success Invariants + + For any valid send_money request, the balance decreases by the amount, + a transaction is created, and the UI state reflects the changes. + """ + # Reset state to ensure sufficient balance + BankingState.reset() + state = BankingState() + + # Ensure we have enough balance for the transfer + state.balance = max(state.balance, amount + 100) + old_balance = state.balance + old_transaction_count = len(state.transactions) + + # Execute the send_money tool + tool = SendMoneyTool() + result = await tool.execute({"recipient": recipient, "amount": amount}) + + # Verify success + assert result["model_result"]["success"] is True + + # Verify new balance equals (old balance - amount) + expected_balance = old_balance - amount + assert abs(state.balance - expected_balance) < 0.001 # Float comparison + assert abs(result["model_result"]["new_balance"] - expected_balance) < 0.001 + + # Verify a new transaction was created + assert len(state.transactions) == old_transaction_count + 1 + + # Verify the transaction has correct amount (-A) and recipient + new_transaction = state.transactions[-1] + assert abs(new_transaction.amount - (-amount)) < 0.001 + assert new_transaction.recipient == recipient + + # Verify ui_result contains updated state + ui_state = result["ui_result"]["props"]["state"] + assert abs(ui_state["balance"] - expected_balance) < 0.001 + + # Verify the new transaction is in the ui_result + transaction_in_result = result["ui_result"]["props"]["transaction"] + assert abs(transaction_in_result["amount"] - (-amount)) < 0.001 + assert transaction_in_result["recipient"] == recipient + + +class TestSendMoneyInsufficientFunds: + """ + Property 6: Send Money Insufficient Funds + + For any send_money request with amount A where A > current balance, + the tool SHALL return an error and the banking state (balance, transactions) + SHALL remain unchanged. + + **Feature: voice-banking-assistant, Property 6: Send Money Insufficient Funds** + **Validates: Requirements 3.6** + """ + + @given( + recipient=contact_name_strategy, + excess_amount=st.floats(min_value=0.01, max_value=1000.0, allow_nan=False, allow_infinity=False) + ) + @settings(max_examples=100) + @pytest.mark.asyncio + async def test_send_money_insufficient_funds(self, recipient: str, excess_amount: float): + """ + Property 6: Send Money Insufficient Funds + + For any amount exceeding balance, the tool returns an error and state is unchanged. + """ + # Reset state + BankingState.reset() + state = BankingState() + + # Set a known balance and request more than available + state.balance = 100.0 + amount = state.balance + excess_amount # Always exceeds balance + + old_balance = state.balance + old_transactions = list(state.transactions) + old_transaction_count = len(state.transactions) + + # Execute the send_money tool + tool = SendMoneyTool() + result = await tool.execute({"recipient": recipient, "amount": amount}) + + # Verify error response + assert result["model_result"]["success"] is False + assert result["model_result"]["error"] == "insufficient_funds" + + # Verify balance is unchanged + assert state.balance == old_balance + + # Verify transactions are unchanged + assert len(state.transactions) == old_transaction_count + + +class TestSendMoneyUnknownRecipient: + """ + Property 7: Send Money Unknown Recipient + + For any send_money request with recipient R where R is not in the contacts list, + the tool SHALL return an error containing the list of valid contacts, + and the banking state SHALL remain unchanged. + + **Feature: voice-banking-assistant, Property 7: Send Money Unknown Recipient** + **Validates: Requirements 3.7** + """ + + @given( + unknown_recipient=st.text(min_size=1, max_size=20, alphabet=st.characters(whitelist_categories=('L',))).filter( + lambda x: x.lower() not in ["alice", "bob", "charlie", "diana"] + ), + amount=valid_amount_strategy + ) + @settings(max_examples=100) + @pytest.mark.asyncio + async def test_send_money_unknown_recipient(self, unknown_recipient: str, amount: float): + """ + Property 7: Send Money Unknown Recipient + + For any unknown recipient, the tool returns an error with valid contacts + and state is unchanged. + """ + # Reset state + BankingState.reset() + state = BankingState() + + old_balance = state.balance + old_transaction_count = len(state.transactions) + + # Execute the send_money tool with unknown recipient + tool = SendMoneyTool() + result = await tool.execute({"recipient": unknown_recipient, "amount": amount}) + + # Verify error response + assert result["model_result"]["success"] is False + assert result["model_result"]["error"] == "unknown_recipient" + + # Verify available contacts are returned + assert "available_contacts" in result["model_result"] + available = result["model_result"]["available_contacts"] + assert len(available) > 0 + assert all(name in ["Alice", "Bob", "Charlie", "Diana"] for name in available) + + # Verify balance is unchanged + assert state.balance == old_balance + + # Verify transactions are unchanged + assert len(state.transactions) == old_transaction_count + + +# Import the GetTransactionsTool for property testing +from tools.categories.banking.get_transactions import GetTransactionsTool +from tools.categories.banking.models import Transaction +from datetime import datetime, timedelta +import uuid + + +# Strategy for generating transactions +transaction_strategy = st.builds( + Transaction, + id=st.uuids().map(str), + description=st.text(min_size=1, max_size=50, alphabet=st.characters(whitelist_categories=('L', 'N', 'P', 'Z'))), + amount=st.floats(min_value=-5000, max_value=5000, allow_nan=False, allow_infinity=False).filter(lambda x: abs(x) > 0.01), + date=st.datetimes(min_value=datetime(2020, 1, 1), max_value=datetime(2030, 12, 31)), + recipient=st.one_of(st.none(), st.text(min_size=1, max_size=20, alphabet=st.characters(whitelist_categories=('L',)))) +) + + +class TestGetTransactionsCompleteness: + """ + Property 8: Get Transactions Completeness + + For any banking state with transactions list T, executing get_transactions + SHALL return all transactions in T. + + **Feature: voice-banking-assistant, Property 8: Get Transactions Completeness** + **Validates: Requirements 4.2** + """ + + @given( + transactions=st.lists(transaction_strategy, min_size=0, max_size=20) + ) + @settings(max_examples=100) + @pytest.mark.asyncio + async def test_get_transactions_returns_all(self, transactions): + """ + Property 8: Get Transactions Completeness + + For any list of transactions, get_transactions returns all of them. + """ + # Reset state + BankingState.reset() + state = BankingState() + + # Replace transactions with our generated list + state.transactions = transactions + + # Execute the get_transactions tool + tool = GetTransactionsTool() + result = await tool.execute({}) + + # Verify all transactions are returned + returned_transactions = result["model_result"]["transactions"] + assert len(returned_transactions) == len(transactions) + + # Verify each transaction is present with correct data + returned_ids = {t["id"] for t in returned_transactions} + original_ids = {t.id for t in transactions} + assert returned_ids == original_ids + + # Verify the count matches + assert result["model_result"]["count"] == len(transactions) + + # Verify ui_result contains the state + ui_state = result["ui_result"]["props"]["state"] + assert len(ui_state["transactions"]) == len(transactions) + + @given( + transactions=st.lists(transaction_strategy, min_size=1, max_size=20) + ) + @settings(max_examples=100) + @pytest.mark.asyncio + async def test_get_transactions_preserves_amounts(self, transactions): + """ + Property 8 extended: Transaction amounts are preserved correctly. + """ + # Reset state + BankingState.reset() + state = BankingState() + + # Replace transactions with our generated list + state.transactions = transactions + + # Execute the get_transactions tool + tool = GetTransactionsTool() + result = await tool.execute({}) + + # Verify amounts are preserved + returned_transactions = result["model_result"]["transactions"] + + for original in transactions: + # Find the matching returned transaction + matching = [t for t in returned_transactions if t["id"] == original.id] + assert len(matching) == 1 + assert abs(matching[0]["amount"] - original.amount) < 0.001 diff --git a/backend/tools/categories/banking/__init__.py b/backend/tools/categories/banking/__init__.py new file mode 100644 index 0000000..50efae4 --- /dev/null +++ b/backend/tools/categories/banking/__init__.py @@ -0,0 +1,4 @@ +"""Banking tools category.""" +from .state import BankingState + +__all__ = ["BankingState"] diff --git a/backend/tools/categories/banking/state.py b/backend/tools/categories/banking/state.py new file mode 100644 index 0000000..e595c2f --- /dev/null +++ b/backend/tools/categories/banking/state.py @@ -0,0 +1,163 @@ +""" +Banking state management singleton. +""" +import uuid +from datetime import datetime, timedelta +from typing import List, Optional, Dict, Any +from dataclasses import dataclass + + +@dataclass +class Transaction: + """Represents a banking transaction.""" + id: str + description: str + amount: float + date: datetime + recipient: Optional[str] = None + + +@dataclass +class Contact: + """Represents a contact for transfers.""" + id: str + name: str + avatar_color: str + + +@dataclass +class ActionHistoryEntry: + """Represents an action in the history.""" + id: str + action: str + description: str + timestamp: datetime + + +class BankingState: + """ + Singleton class that manages all banking data in-memory. + + This ensures all tools access the same state instance. + State resets when the application restarts. + """ + _instance: Optional['BankingState'] = None + + def __new__(cls) -> 'BankingState': + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._initialize() + return cls._instance + + def _initialize(self) -> None: + """Initialize the banking state with default values.""" + self.balance: float = 5000.00 + self.transactions: List[Transaction] = [] + self.contacts: List[Contact] = [] + self.action_history: List[ActionHistoryEntry] = [] + self._seed_mock_data() + + def _seed_mock_data(self) -> None: + """Populate the state with realistic mock data for demo purposes.""" + now = datetime.now() + + # Initialize contacts + self.contacts = [ + Contact(id=str(uuid.uuid4()), name="Alice", avatar_color="blue"), + Contact(id=str(uuid.uuid4()), name="Bob", avatar_color="green"), + Contact(id=str(uuid.uuid4()), name="Charlie", avatar_color="purple"), + ] + + # Initialize transactions + self.transactions = [ + Transaction( + id=str(uuid.uuid4()), + description="Direct Deposit", + amount=2500.00, + date=now - timedelta(days=5) + ), + Transaction( + id=str(uuid.uuid4()), + description="Grocery Store", + amount=-85.50, + date=now - timedelta(days=3) + ), + Transaction( + id=str(uuid.uuid4()), + description="Gas Station", + amount=-45.00, + date=now - timedelta(days=2) + ), + Transaction( + id=str(uuid.uuid4()), + description="Online Purchase", + amount=-120.00, + date=now - timedelta(days=1) + ), + ] + + def get_state_snapshot(self) -> Dict[str, Any]: + """Return the current state as a dictionary for UI consumption.""" + return { + "balance": self.balance, + "transactions": [ + { + "id": t.id, + "description": t.description, + "amount": t.amount, + "date": t.date.isoformat(), + "recipient": t.recipient + } + for t in self.transactions + ], + "contacts": [ + {"id": c.id, "name": c.name, "avatarColor": c.avatar_color} + for c in self.contacts + ], + "actionHistory": [ + { + "id": a.id, "action": a.action, + "description": a.description, "timestamp": a.timestamp.isoformat() + } + for a in self.action_history + ] + } + + def add_transaction( + self, description: str, amount: float, recipient: Optional[str] = None + ) -> Transaction: + """Add a new transaction and update the balance.""" + transaction = Transaction( + id=str(uuid.uuid4()), description=description, + amount=amount, date=datetime.now(), recipient=recipient + ) + self.transactions.append(transaction) + self.balance += amount + return transaction + + def log_action(self, action: str, description: str) -> ActionHistoryEntry: + """Log a voice-triggered action to the history.""" + entry = ActionHistoryEntry( + id=str(uuid.uuid4()), action=action, + description=description, timestamp=datetime.now() + ) + self.action_history.append(entry) + return entry + + def find_contact_by_name(self, name: str) -> Optional[Contact]: + """Find a contact by name (case-insensitive).""" + name_lower = name.lower() + for contact in self.contacts: + if contact.name.lower() == name_lower: + return contact + return None + + def get_contact_names(self) -> List[str]: + """Get a list of all contact names.""" + return [c.name for c in self.contacts] + + @classmethod + def reset(cls) -> None: + """Reset the singleton instance. Useful for testing.""" + if cls._instance is not None: + cls._instance._initialize() diff --git a/backend/tools/categories/finance/__init__.py b/backend/tools/categories/finance/__init__.py new file mode 100644 index 0000000..3f031a1 --- /dev/null +++ b/backend/tools/categories/finance/__init__.py @@ -0,0 +1,38 @@ +# Finance module exports +from .models import Transaction, Contact, ActionHistoryEntry, Stock, Budget, Bill, FinancialGoal, PortfolioHolding +from .state import FinanceState +from .check_balance import CheckBalanceTool +from .send_money import SendMoneyTool +from .get_transactions import GetTransactionsTool +from .get_stock_quote import GetStockQuoteTool +from .get_budget import GetBudgetTool +from .pay_bill import PayBillTool +from .get_financial_goals import GetFinancialGoalsTool +from .add_expense import AddExpenseTool +from .buy_stock import BuyStockTool +from .get_portfolio import GetPortfolioTool + +__all__ = [ + # Models + "Transaction", + "Contact", + "ActionHistoryEntry", + "Stock", + "Budget", + "Bill", + "FinancialGoal", + "PortfolioHolding", + # State + "FinanceState", + # Tools + "CheckBalanceTool", + "SendMoneyTool", + "GetTransactionsTool", + "GetStockQuoteTool", + "GetBudgetTool", + "PayBillTool", + "GetFinancialGoalsTool", + "AddExpenseTool", + "BuyStockTool", + "GetPortfolioTool", +] diff --git a/backend/tools/categories/finance/add_expense.py b/backend/tools/categories/finance/add_expense.py new file mode 100644 index 0000000..3cea730 --- /dev/null +++ b/backend/tools/categories/finance/add_expense.py @@ -0,0 +1,158 @@ +""" +AddExpenseTool - Records a new expense with category tracking. +""" +from typing import Dict, Any +from ...base.tool import BaseTool +from .state import FinanceState +from .models import BudgetCategory + + +class AddExpenseTool(BaseTool): + """ + Tool for recording expenses with budget category tracking. + + Creates a transaction and updates the corresponding budget category. + """ + + def __init__(self): + super().__init__() + self.config = { + "name": "add_expense", + "description": "Record a new expense with category tracking. Use this when the user wants to log a purchase, record spending, or track an expense. Categories include groceries, dining, transportation, utilities, entertainment, shopping, healthcare, and other.", + "shortDescription": "Recording an expense", + "schema": { + "type": "object", + "properties": { + "description": { + "type": "string", + "description": "Description of the expense (e.g., 'Lunch at cafe', 'Gas station')" + }, + "amount": { + "type": "number", + "description": "Amount spent in USD (positive number)" + }, + "category": { + "type": "string", + "enum": ["groceries", "dining", "transportation", "utilities", + "entertainment", "shopping", "healthcare", "other"], + "description": "Spending category for budget tracking" + } + }, + "required": ["description", "amount", "category"] + } + } + + async def execute(self, content: Dict[str, Any]) -> Dict[str, Any]: + """Execute the add expense operation.""" + state = FinanceState() + + description = content.get("description", "") + amount = content.get("amount", 0) + category_str = content.get("category", "other") + + # Validate amount + if amount <= 0: + state.log_action("add_expense", f"Failed: Invalid amount ${amount}") + + model_result = { + "success": False, + "error": "invalid_amount", + "message": "Amount must be greater than zero" + } + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "error", + "error": {"code": "invalid_amount", "message": "Invalid amount"}, + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) + + # Validate category + try: + category = BudgetCategory(category_str) + except ValueError: + category = BudgetCategory.OTHER + + # Check sufficient funds + if amount > state.balance: + state.log_action("add_expense", f"Failed: Insufficient funds for ${amount:.2f}") + + model_result = { + "success": False, + "error": "insufficient_funds", + "message": f"Insufficient funds. Current balance: ${state.balance:.2f}", + "current_balance": state.balance, + "requested_amount": amount + } + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "error", + "error": {"code": "insufficient_funds", "message": "Insufficient funds"}, + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) + + # Record the expense + transaction = state.add_transaction( + description=description, + amount=-amount, # Negative for expense + category=category + ) + + # Get updated budget for this category + budget_info = None + for budget in state.budgets: + if budget.category == category: + remaining = budget.limit - budget.spent + percent_used = (budget.spent / budget.limit * 100) if budget.limit > 0 else 0 + budget_info = { + "category": category.value, + "spent": budget.spent, + "limit": budget.limit, + "remaining": remaining, + "percent_used": round(percent_used, 1), + "over_budget": budget.spent > budget.limit + } + break + + state.log_action("add_expense", f"Recorded ${amount:.2f} expense for {description}") + + model_result = { + "success": True, + "description": description, + "amount": amount, + "formatted_amount": f"${amount:,.2f}", + "category": category.value, + "new_balance": state.balance, + "transaction_id": transaction.id, + "budget_status": budget_info + } + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "expense_added", + "transaction": { + "id": transaction.id, + "description": transaction.description, + "amount": transaction.amount, + "category": category.value, + "date": transaction.date.isoformat() + }, + "budgetStatus": budget_info, + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) diff --git a/backend/tools/categories/finance/buy_stock.py b/backend/tools/categories/finance/buy_stock.py new file mode 100644 index 0000000..35aedcb --- /dev/null +++ b/backend/tools/categories/finance/buy_stock.py @@ -0,0 +1,167 @@ +""" +BuyStockTool - Simulates buying stocks. +""" +from typing import Dict, Any +from datetime import datetime +from ...base.tool import BaseTool +from .state import FinanceState + + +class BuyStockTool(BaseTool): + """ + Tool for buying stocks (simulated). + + Validates stock exists, checks sufficient funds, and executes purchase. + """ + + def __init__(self): + super().__init__() + self.config = { + "name": "buy_stock", + "description": "Buy shares of a stock. Use this when the user wants to invest, buy stocks, or purchase shares. Requires stock symbol and either number of shares or dollar amount. Available stocks: AAPL, GOOGL, MSFT, AMZN, TSLA.", + "shortDescription": "Buying stock", + "schema": { + "type": "object", + "properties": { + "symbol": { + "type": "string", + "description": "Stock ticker symbol (e.g., AAPL, GOOGL, MSFT)" + }, + "shares": { + "type": "number", + "description": "Number of shares to buy (optional if amount is provided)" + }, + "amount": { + "type": "number", + "description": "Dollar amount to invest (optional if shares is provided)" + } + }, + "required": ["symbol"] + } + } + + async def execute(self, content: Dict[str, Any]) -> Dict[str, Any]: + """Execute the buy stock operation.""" + state = FinanceState() + symbol = content.get("symbol", "").upper() + shares = content.get("shares") + amount = content.get("amount") + + # Get stock info + stock = state.get_stock(symbol) + + if stock is None: + available_symbols = list(state.stocks.keys()) + state.log_action("buy_stock", f"Stock {symbol} not found") + + model_result = { + "success": False, + "error": "symbol_not_found", + "message": f"Stock symbol '{symbol}' not available for trading", + "available_symbols": available_symbols + } + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "error", + "error": {"code": "symbol_not_found", "message": f"Stock {symbol} not found"}, + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) + + # Calculate shares and total cost + if shares is not None and shares > 0: + num_shares = shares + total_cost = num_shares * stock.price + elif amount is not None and amount > 0: + num_shares = round(amount / stock.price, 4) + total_cost = amount + else: + state.log_action("buy_stock", "Failed: No shares or amount specified") + + model_result = { + "success": False, + "error": "invalid_input", + "message": "Please specify either number of shares or dollar amount to invest" + } + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "error", + "error": {"code": "invalid_input", "message": "Specify shares or amount"}, + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) + + # Check sufficient funds + if total_cost > state.balance: + state.log_action("buy_stock", f"Insufficient funds for ${total_cost:.2f}") + + model_result = { + "success": False, + "error": "insufficient_funds", + "message": f"Insufficient funds. Need ${total_cost:.2f}, have ${state.balance:.2f}", + "required": total_cost, + "available": state.balance + } + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "error", + "error": {"code": "insufficient_funds", "message": "Insufficient funds"}, + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) + + # Execute purchase + transaction = state.add_transaction( + description=f"Buy {num_shares} shares of {symbol}", + amount=-total_cost, + recipient=f"{symbol} Investment" + ) + + # Add to portfolio + state.add_to_portfolio(symbol, num_shares, stock.price) + + state.log_action("buy_stock", f"Bought {num_shares} shares of {symbol} for ${total_cost:.2f}") + + model_result = { + "success": True, + "symbol": symbol, + "company_name": stock.name, + "shares_purchased": num_shares, + "price_per_share": stock.price, + "total_cost": total_cost, + "formatted_cost": f"${total_cost:,.2f}", + "new_balance": state.balance, + "transaction_id": transaction.id + } + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "stock_purchased", + "purchase": { + "symbol": symbol, + "shares": num_shares, + "price": stock.price, + "total": total_cost + }, + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) diff --git a/backend/tools/categories/finance/check_balance.py b/backend/tools/categories/finance/check_balance.py new file mode 100644 index 0000000..e5a2cfa --- /dev/null +++ b/backend/tools/categories/finance/check_balance.py @@ -0,0 +1,68 @@ +""" +CheckBalanceTool - Returns the user's current account balances. +""" +from typing import Dict, Any +from ...base.tool import BaseTool +from .state import FinanceState + + +class CheckBalanceTool(BaseTool): + """ + Tool for checking the user's current account balances. + + Returns checking and savings balances to the model for verbal response. + """ + + def __init__(self): + super().__init__() + self.config = { + "name": "check_balance", + "description": "Check the user's current account balances including checking and savings. Use this when the user asks about their balance, how much money they have, or their account status.", + "shortDescription": "Checking account balances", + "schema": { + "type": "object", + "properties": { + "account_type": { + "type": "string", + "enum": ["checking", "savings", "all"], + "description": "Which account balance to check (defaults to all)" + } + }, + "required": [] + } + } + + async def execute(self, content: Dict[str, Any] = None) -> Dict[str, Any]: + """Execute the check balance operation.""" + state = FinanceState() + content = content or {} + account_type = content.get("account_type", "all") + + state.log_action("check_balance", f"Checked {account_type} balance") + + model_result = {"currency": "USD"} + + if account_type in ["checking", "all"]: + model_result["checking_balance"] = state.balance + model_result["formatted_checking"] = f"${state.balance:,.2f}" + + if account_type in ["savings", "all"]: + model_result["savings_balance"] = state.savings_balance + model_result["formatted_savings"] = f"${state.savings_balance:,.2f}" + + if account_type == "all": + total = state.balance + state.savings_balance + model_result["total_balance"] = total + model_result["formatted_total"] = f"${total:,.2f}" + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "highlight_balance", + "accountType": account_type, + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) diff --git a/backend/tools/categories/finance/get_budget.py b/backend/tools/categories/finance/get_budget.py new file mode 100644 index 0000000..f88625d --- /dev/null +++ b/backend/tools/categories/finance/get_budget.py @@ -0,0 +1,102 @@ +""" +GetBudgetTool - Returns budget information and spending status. +""" +from typing import Dict, Any +from ...base.tool import BaseTool +from .state import FinanceState +from .models import BudgetCategory + + +class GetBudgetTool(BaseTool): + """ + Tool for getting budget information and spending status. + + Returns budget limits, current spending, and remaining amounts. + """ + + def __init__(self): + super().__init__() + self.config = { + "name": "get_budget", + "description": "Get the user's budget information including spending limits and current spending by category. Use this when the user asks about their budget, spending limits, how much they've spent, or remaining budget.", + "shortDescription": "Getting budget status", + "schema": { + "type": "object", + "properties": { + "category": { + "type": "string", + "enum": ["groceries", "dining", "transportation", "utilities", + "entertainment", "shopping", "healthcare", "other", "all"], + "description": "Budget category to check (defaults to all)" + } + }, + "required": [] + } + } + + async def execute(self, content: Dict[str, Any] = None) -> Dict[str, Any]: + """Execute the get budget operation.""" + state = FinanceState() + content = content or {} + category_filter = content.get("category", "all") + + state.log_action("get_budget", f"Retrieved budget for {category_filter}") + + budgets = state.budgets + + # Filter by category if specified + if category_filter != "all": + try: + cat = BudgetCategory(category_filter) + budgets = [b for b in budgets if b.category == cat] + except ValueError: + pass + + # Format budget data + formatted_budgets = [] + total_limit = 0 + total_spent = 0 + + for budget in budgets: + remaining = budget.limit - budget.spent + percent_used = (budget.spent / budget.limit * 100) if budget.limit > 0 else 0 + status = "on_track" if percent_used < 80 else ("warning" if percent_used < 100 else "over_budget") + + formatted_budgets.append({ + "category": budget.category.value, + "limit": budget.limit, + "formatted_limit": f"${budget.limit:,.2f}", + "spent": budget.spent, + "formatted_spent": f"${budget.spent:,.2f}", + "remaining": remaining, + "formatted_remaining": f"${remaining:,.2f}", + "percent_used": round(percent_used, 1), + "status": status + }) + + total_limit += budget.limit + total_spent += budget.spent + + total_remaining = total_limit - total_spent + + model_result = { + "budgets": formatted_budgets, + "count": len(formatted_budgets), + "total_limit": total_limit, + "total_spent": total_spent, + "total_remaining": total_remaining, + "formatted_total_remaining": f"${total_remaining:,.2f}", + "currency": "USD" + } + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "show_budget", + "category": category_filter, + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) diff --git a/backend/tools/categories/finance/get_financial_goals.py b/backend/tools/categories/finance/get_financial_goals.py new file mode 100644 index 0000000..4dbe1e1 --- /dev/null +++ b/backend/tools/categories/finance/get_financial_goals.py @@ -0,0 +1,114 @@ +""" +GetFinancialGoalsTool - Returns financial goals and progress. +""" +from typing import Dict, Any +from datetime import date +from ...base.tool import BaseTool +from .state import FinanceState + + +class GetFinancialGoalsTool(BaseTool): + """ + Tool for getting financial goals and savings progress. + + Returns goal targets, current progress, and projected completion. + """ + + def __init__(self): + super().__init__() + self.config = { + "name": "get_financial_goals", + "description": "Get the user's financial goals and savings progress. Use this when the user asks about their savings goals, financial targets, how close they are to their goals, or wants to check progress on emergency fund, vacation fund, or other savings goals.", + "shortDescription": "Getting financial goals", + "schema": { + "type": "object", + "properties": { + "goal_name": { + "type": "string", + "description": "Specific goal name to check (optional, returns all if not specified)" + } + }, + "required": [] + } + } + + async def execute(self, content: Dict[str, Any] = None) -> Dict[str, Any]: + """Execute the get financial goals operation.""" + state = FinanceState() + content = content or {} + goal_name_filter = content.get("goal_name") + + state.log_action("get_financial_goals", "Retrieved financial goals") + + goals = state.goals + + # Filter by goal name if specified + if goal_name_filter: + name_lower = goal_name_filter.lower() + goals = [g for g in goals if name_lower in g.name.lower()] + + # Format goals + formatted_goals = [] + total_target = 0 + total_saved = 0 + + today = date.today() + + for goal in goals: + remaining = goal.target_amount - goal.current_amount + percent_complete = (goal.current_amount / goal.target_amount * 100) if goal.target_amount > 0 else 0 + + # Calculate days remaining if target date exists + days_remaining = None + on_track = None + if goal.target_date: + days_remaining = (goal.target_date - today).days + if days_remaining > 0 and remaining > 0: + required_daily = remaining / days_remaining + # Assume ~$500/month savings capacity for on_track calculation + monthly_capacity = 500 + on_track = (required_daily * 30) <= monthly_capacity + + formatted_goals.append({ + "name": goal.name, + "description": goal.description, + "target_amount": goal.target_amount, + "formatted_target": f"${goal.target_amount:,.2f}", + "current_amount": goal.current_amount, + "formatted_current": f"${goal.current_amount:,.2f}", + "remaining": remaining, + "formatted_remaining": f"${remaining:,.2f}", + "percent_complete": round(percent_complete, 1), + "target_date": goal.target_date.isoformat() if goal.target_date else None, + "days_remaining": days_remaining, + "on_track": on_track + }) + + total_target += goal.target_amount + total_saved += goal.current_amount + + total_remaining = total_target - total_saved + overall_percent = (total_saved / total_target * 100) if total_target > 0 else 0 + + model_result = { + "goals": formatted_goals, + "count": len(formatted_goals), + "total_target": total_target, + "total_saved": total_saved, + "total_remaining": total_remaining, + "formatted_total_remaining": f"${total_remaining:,.2f}", + "overall_percent_complete": round(overall_percent, 1), + "currency": "USD" + } + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "show_goals", + "goalFilter": goal_name_filter, + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) diff --git a/backend/tools/categories/finance/get_portfolio.py b/backend/tools/categories/finance/get_portfolio.py new file mode 100644 index 0000000..1313695 --- /dev/null +++ b/backend/tools/categories/finance/get_portfolio.py @@ -0,0 +1,104 @@ +""" +GetPortfolioTool - Returns the user's investment portfolio. +""" +from typing import Dict, Any +from ...base.tool import BaseTool +from .state import FinanceState + + +class GetPortfolioTool(BaseTool): + """ + Tool for getting the user's investment portfolio. + + Returns all holdings with current values and gains/losses. + """ + + def __init__(self): + super().__init__() + self.config = { + "name": "get_portfolio", + "description": "Get the user's investment portfolio showing all stock holdings, current values, and gains/losses. Use this when the user asks about their investments, portfolio, holdings, or how their stocks are doing.", + "shortDescription": "Getting investment portfolio", + "schema": { + "type": "object", + "properties": { + "symbol": { + "type": "string", + "description": "Optional: specific stock symbol to check (returns all if not specified)" + } + }, + "required": [] + } + } + + async def execute(self, content: Dict[str, Any] = None) -> Dict[str, Any]: + """Execute the get portfolio operation.""" + state = FinanceState() + content = content or {} + symbol_filter = content.get("symbol", "").upper() if content.get("symbol") else None + + state.log_action("get_portfolio", "Retrieved investment portfolio") + + holdings = state.portfolio + + # Filter by symbol if specified + if symbol_filter: + holdings = [h for h in holdings if h.symbol == symbol_filter] + + # Calculate current values and gains + formatted_holdings = [] + total_invested = 0 + total_current_value = 0 + + for holding in holdings: + stock = state.get_stock(holding.symbol) + if stock: + current_price = stock.price + current_value = holding.shares * current_price + cost_basis = holding.shares * holding.average_cost + gain_loss = current_value - cost_basis + gain_loss_percent = (gain_loss / cost_basis * 100) if cost_basis > 0 else 0 + + formatted_holdings.append({ + "symbol": holding.symbol, + "company_name": stock.name, + "shares": holding.shares, + "average_cost": holding.average_cost, + "current_price": current_price, + "cost_basis": cost_basis, + "current_value": current_value, + "gain_loss": gain_loss, + "gain_loss_percent": round(gain_loss_percent, 2), + "formatted_value": f"${current_value:,.2f}", + "formatted_gain": f"${gain_loss:+,.2f}", + "is_profit": gain_loss >= 0 + }) + + total_invested += cost_basis + total_current_value += current_value + + total_gain_loss = total_current_value - total_invested + total_gain_percent = (total_gain_loss / total_invested * 100) if total_invested > 0 else 0 + + model_result = { + "holdings": formatted_holdings, + "count": len(formatted_holdings), + "total_invested": total_invested, + "total_current_value": total_current_value, + "total_gain_loss": total_gain_loss, + "total_gain_percent": round(total_gain_percent, 2), + "formatted_total_value": f"${total_current_value:,.2f}", + "formatted_total_gain": f"${total_gain_loss:+,.2f}", + "currency": "USD" + } + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "show_portfolio", + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) diff --git a/backend/tools/categories/finance/get_stock_quote.py b/backend/tools/categories/finance/get_stock_quote.py new file mode 100644 index 0000000..5b337db --- /dev/null +++ b/backend/tools/categories/finance/get_stock_quote.py @@ -0,0 +1,98 @@ +""" +GetStockQuoteTool - Returns stock price information. +""" +from typing import Dict, Any +from ...base.tool import BaseTool +from .state import FinanceState + + +class GetStockQuoteTool(BaseTool): + """ + Tool for getting stock price quotes. + + Returns current price, change, and percentage change for a stock symbol. + """ + + def __init__(self): + super().__init__() + self.config = { + "name": "get_stock_quote", + "description": "Get the current stock price and market data for a given stock symbol. Use this when the user asks about stock prices, market performance, or wants to check their investments. Supports major stocks like AAPL, GOOGL, MSFT, AMZN, TSLA.", + "shortDescription": "Getting stock quote", + "schema": { + "type": "object", + "properties": { + "symbol": { + "type": "string", + "description": "Stock ticker symbol (e.g., AAPL, GOOGL, MSFT)" + } + }, + "required": ["symbol"] + } + } + + async def execute(self, content: Dict[str, Any]) -> Dict[str, Any]: + """Execute the get stock quote operation.""" + state = FinanceState() + symbol = content.get("symbol", "").upper() + + stock = state.get_stock(symbol) + + if stock is None: + state.log_action("get_stock_quote", f"Stock {symbol} not found") + available_symbols = list(state.stocks.keys()) + + model_result = { + "success": False, + "error": "symbol_not_found", + "message": f"Stock symbol '{symbol}' not found in watchlist", + "available_symbols": available_symbols + } + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "stock_not_found", + "symbol": symbol, + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) + + state.log_action("get_stock_quote", f"Retrieved quote for {symbol}") + + direction = "up" if stock.change >= 0 else "down" + + model_result = { + "success": True, + "symbol": stock.symbol, + "company_name": stock.name, + "price": stock.price, + "formatted_price": f"${stock.price:,.2f}", + "change": stock.change, + "formatted_change": f"${abs(stock.change):,.2f}", + "change_percent": stock.change_percent, + "formatted_percent": f"{abs(stock.change_percent):.2f}%", + "direction": direction, + "last_updated": stock.last_updated.isoformat() + } + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "show_stock_quote", + "stock": { + "symbol": stock.symbol, + "name": stock.name, + "price": stock.price, + "change": stock.change, + "changePercent": stock.change_percent + }, + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) diff --git a/backend/tools/categories/finance/get_transactions.py b/backend/tools/categories/finance/get_transactions.py new file mode 100644 index 0000000..7453c5d --- /dev/null +++ b/backend/tools/categories/finance/get_transactions.py @@ -0,0 +1,113 @@ +""" +GetTransactionsTool - Returns the user's transaction history. +""" +from typing import Dict, Any +from ...base.tool import BaseTool +from .state import FinanceState +from .models import BudgetCategory + + +class GetTransactionsTool(BaseTool): + """ + Tool for retrieving the user's transaction history. + + Returns transactions with optional filtering by category or type. + """ + + def __init__(self): + super().__init__() + self.config = { + "name": "get_transactions", + "description": "Get the user's recent transaction history. Use this when the user asks about their transactions, spending, recent activity, or payment history. Can filter by category or transaction type.", + "shortDescription": "Getting transaction history", + "schema": { + "type": "object", + "properties": { + "limit": { + "type": "integer", + "description": "Maximum number of transactions to return (optional)" + }, + "category": { + "type": "string", + "enum": ["groceries", "dining", "transportation", "utilities", + "entertainment", "shopping", "healthcare", "other"], + "description": "Filter by spending category (optional)" + }, + "type": { + "type": "string", + "enum": ["incoming", "outgoing", "all"], + "description": "Filter by transaction type (optional, defaults to all)" + } + }, + "required": [] + } + } + + async def execute(self, content: Dict[str, Any] = None) -> Dict[str, Any]: + """Execute the get transactions operation.""" + state = FinanceState() + content = content or {} + + state.log_action("get_transactions", "Retrieved transaction history") + + transactions = state.transactions.copy() + + # Filter by category if specified + category_filter = content.get("category") + if category_filter: + try: + cat = BudgetCategory(category_filter) + transactions = [t for t in transactions if t.category == cat] + except ValueError: + pass + + # Filter by type if specified + type_filter = content.get("type", "all") + if type_filter == "incoming": + transactions = [t for t in transactions if t.amount > 0] + elif type_filter == "outgoing": + transactions = [t for t in transactions if t.amount < 0] + + # Apply limit + limit = content.get("limit") + if limit is not None and isinstance(limit, int) and limit > 0: + transactions = transactions[-limit:] + + # Format transactions + formatted_transactions = [ + { + "id": t.id, + "description": t.description, + "amount": t.amount, + "formatted_amount": f"${abs(t.amount):,.2f}", + "type": "incoming" if t.amount > 0 else "outgoing", + "date": t.date.isoformat(), + "recipient": t.recipient, + "category": t.category.value if t.category else None + } + for t in transactions + ] + + total_incoming = sum(t.amount for t in transactions if t.amount > 0) + total_outgoing = sum(abs(t.amount) for t in transactions if t.amount < 0) + + model_result = { + "transactions": formatted_transactions, + "count": len(formatted_transactions), + "total_incoming": total_incoming, + "total_outgoing": total_outgoing, + "net_flow": total_incoming - total_outgoing, + "currency": "USD" + } + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "show_transactions", + "filters": {"category": category_filter, "type": type_filter}, + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) diff --git a/backend/tools/categories/finance/models.py b/backend/tools/categories/finance/models.py new file mode 100644 index 0000000..cdb6fe6 --- /dev/null +++ b/backend/tools/categories/finance/models.py @@ -0,0 +1,178 @@ +""" +Data models for the Financial Assistant. +""" +from dataclasses import dataclass, field +from datetime import datetime, date +from typing import Optional, List +from enum import Enum + + +class BudgetCategory(Enum): + """Budget spending categories.""" + GROCERIES = "groceries" + DINING = "dining" + TRANSPORTATION = "transportation" + UTILITIES = "utilities" + ENTERTAINMENT = "entertainment" + SHOPPING = "shopping" + HEALTHCARE = "healthcare" + OTHER = "other" + + +class BillStatus(Enum): + """Bill payment status.""" + PENDING = "pending" + PAID = "paid" + OVERDUE = "overdue" + + +@dataclass +class Transaction: + """ + A record of money movement (sent or received) between accounts. + + Attributes: + id: Unique identifier for the transaction + description: Human-readable description of the transaction + amount: Transaction amount (positive for incoming, negative for outgoing) + date: Timestamp when the transaction occurred + recipient: Name of the recipient (for outgoing transfers) + category: Budget category for expense tracking + """ + id: str + description: str + amount: float + date: datetime + recipient: Optional[str] = None + category: Optional[BudgetCategory] = None + + +@dataclass +class Contact: + """ + A predefined recipient that the user can send money to. + + Attributes: + id: Unique identifier for the contact + name: Display name of the contact + avatar_color: Color for UI avatar display + """ + id: str + name: str + avatar_color: str + + +@dataclass +class ActionHistoryEntry: + """ + A log entry for voice-triggered financial operations. + + Attributes: + id: Unique identifier for the action entry + action: Name of the tool/action executed + description: Human-readable description of what was done + timestamp: When the action was performed + """ + id: str + action: str + description: str + timestamp: datetime + + +@dataclass +class Stock: + """ + Stock quote information. + + Attributes: + symbol: Stock ticker symbol + name: Company name + price: Current price + change: Price change from previous close + change_percent: Percentage change + last_updated: When the quote was last updated + """ + symbol: str + name: str + price: float + change: float + change_percent: float + last_updated: datetime + + +@dataclass +class Budget: + """ + Monthly budget for a spending category. + + Attributes: + id: Unique identifier + category: Budget category + limit: Monthly spending limit + spent: Amount spent this month + month: Budget month (YYYY-MM format) + """ + id: str + category: BudgetCategory + limit: float + spent: float + month: str + + +@dataclass +class Bill: + """ + A recurring bill or payment due. + + Attributes: + id: Unique identifier + name: Bill name/payee + amount: Amount due + due_date: Payment due date + status: Payment status + category: Associated budget category + """ + id: str + name: str + amount: float + due_date: date + status: BillStatus + category: BudgetCategory + + +@dataclass +class FinancialGoal: + """ + A savings or financial goal. + + Attributes: + id: Unique identifier + name: Goal name + target_amount: Target amount to save + current_amount: Amount saved so far + target_date: Target completion date + description: Optional description + """ + id: str + name: str + target_amount: float + current_amount: float + target_date: Optional[date] = None + description: Optional[str] = None + + +@dataclass +class PortfolioHolding: + """ + A stock holding in the user's portfolio. + + Attributes: + symbol: Stock ticker symbol + shares: Number of shares owned + average_cost: Average cost per share + purchase_date: Date of first purchase + """ + symbol: str + shares: float + average_cost: float + purchase_date: date diff --git a/backend/tools/categories/finance/pay_bill.py b/backend/tools/categories/finance/pay_bill.py new file mode 100644 index 0000000..cf6f082 --- /dev/null +++ b/backend/tools/categories/finance/pay_bill.py @@ -0,0 +1,143 @@ +""" +PayBillTool - Pays a pending bill. +""" +from typing import Dict, Any +from ...base.tool import BaseTool +from .state import FinanceState +from .models import BillStatus + + +class PayBillTool(BaseTool): + """ + Tool for paying pending bills. + + Validates bill exists and is not already paid, then processes payment. + """ + + def __init__(self): + super().__init__() + self.config = { + "name": "pay_bill", + "description": "Pay a pending bill. Use this when the user wants to pay a bill, make a bill payment, or settle an outstanding payment. Available bills include Rent, Internet, Car Insurance, and Phone Bill.", + "shortDescription": "Paying a bill", + "schema": { + "type": "object", + "properties": { + "bill_name": { + "type": "string", + "description": "Name of the bill to pay (e.g., 'Rent', 'Internet', 'Phone Bill')" + } + }, + "required": ["bill_name"] + } + } + + async def execute(self, content: Dict[str, Any]) -> Dict[str, Any]: + """Execute the pay bill operation.""" + state = FinanceState() + bill_name = content.get("bill_name", "") + + # Find the bill + bill = state.get_bill_by_name(bill_name) + + if bill is None: + available_bills = [b.name for b in state.bills if b.status != BillStatus.PAID] + state.log_action("pay_bill", f"Bill '{bill_name}' not found") + + model_result = { + "success": False, + "error": "bill_not_found", + "message": f"Bill '{bill_name}' not found", + "available_bills": available_bills + } + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "error", + "error": {"code": "bill_not_found", "message": f"Bill '{bill_name}' not found"}, + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) + + # Check if already paid + if bill.status == BillStatus.PAID: + state.log_action("pay_bill", f"Bill '{bill.name}' already paid") + + model_result = { + "success": False, + "error": "already_paid", + "message": f"Bill '{bill.name}' has already been paid" + } + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "error", + "error": {"code": "already_paid", "message": f"Bill already paid"}, + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) + + # Check sufficient funds + if bill.amount > state.balance: + state.log_action("pay_bill", f"Insufficient funds for {bill.name}") + + model_result = { + "success": False, + "error": "insufficient_funds", + "message": f"Insufficient funds. Bill amount: ${bill.amount:.2f}, Balance: ${state.balance:.2f}", + "bill_amount": bill.amount, + "current_balance": state.balance + } + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "error", + "error": {"code": "insufficient_funds", "message": "Insufficient funds"}, + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) + + # Process payment + was_overdue = bill.status == BillStatus.OVERDUE + transaction = state.pay_bill(bill) + + state.log_action("pay_bill", f"Paid {bill.name} - ${bill.amount:.2f}") + + model_result = { + "success": True, + "bill_name": bill.name, + "amount_paid": bill.amount, + "formatted_amount": f"${bill.amount:,.2f}", + "new_balance": state.balance, + "was_overdue": was_overdue, + "transaction_id": transaction.id + } + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "bill_paid", + "bill": { + "id": bill.id, + "name": bill.name, + "amount": bill.amount, + "wasOverdue": was_overdue + }, + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) diff --git a/backend/tools/categories/finance/send_money.py b/backend/tools/categories/finance/send_money.py new file mode 100644 index 0000000..f012901 --- /dev/null +++ b/backend/tools/categories/finance/send_money.py @@ -0,0 +1,125 @@ +""" +SendMoneyTool - Sends money to a contact. +""" +from typing import Dict, Any +from ...base.tool import BaseTool +from .state import FinanceState + + +class SendMoneyTool(BaseTool): + """ + Tool for sending money to a contact. + + Validates recipient exists in contacts and amount is valid, + then deducts from balance and creates a transaction record. + """ + + def __init__(self): + super().__init__() + self.config = { + "name": "send_money", + "description": "Send money to a contact. Requires recipient name and amount. Use this when the user wants to transfer money, pay someone, or send funds to a contact.", + "shortDescription": "Sending money to contact", + "schema": { + "type": "object", + "properties": { + "recipient": { + "type": "string", + "description": "Name of the recipient (must be an existing contact)" + }, + "amount": { + "type": "number", + "description": "Amount to send in USD (must be positive and not exceed balance)" + } + }, + "required": ["recipient", "amount"] + } + } + + async def execute(self, content: Dict[str, Any]) -> Dict[str, Any]: + """Execute the send money operation.""" + state = FinanceState() + + recipient_name = content.get("recipient", "") + amount = content.get("amount", 0) + + # Validate amount is positive + if amount <= 0: + state.log_action("send_money", f"Failed: Invalid amount ${amount:.2f}") + return self._error_response( + state, "invalid_amount", "Amount must be greater than zero", + {"requested_amount": amount} + ) + + # Validate recipient exists in contacts + contact = state.find_contact_by_name(recipient_name) + if contact is None: + available_contacts = state.get_contact_names() + state.log_action("send_money", f"Failed: Unknown recipient '{recipient_name}'") + return self._error_response( + state, "unknown_recipient", f"Contact '{recipient_name}' not found", + {"available_contacts": available_contacts} + ) + + # Validate sufficient funds + if amount > state.balance: + state.log_action("send_money", f"Failed: Insufficient funds for ${amount:.2f}") + return self._error_response( + state, "insufficient_funds", + f"Insufficient funds. Current balance: ${state.balance:.2f}", + {"current_balance": state.balance, "requested_amount": amount, + "shortfall": amount - state.balance} + ) + + # Execute the transfer + transaction = state.add_transaction( + description=f"Transfer to {contact.name}", + amount=-amount, + recipient=contact.name + ) + + state.log_action("send_money", f"Sent ${amount:.2f} to {contact.name}") + + model_result = { + "success": True, + "amount_sent": amount, + "recipient": contact.name, + "new_balance": state.balance, + "currency": "USD", + "transaction_id": transaction.id + } + + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "money_sent", + "transaction": { + "id": transaction.id, + "description": transaction.description, + "amount": transaction.amount, + "date": transaction.date.isoformat(), + "recipient": transaction.recipient + }, + "state": state.get_state_snapshot() + } + } + + return self.format_response(model_result, ui_result) + + def _error_response( + self, state: FinanceState, error_code: str, + message: str, details: Dict[str, Any] + ) -> Dict[str, Any]: + """Create a standardized error response.""" + model_result = {"success": False, "error": error_code, "message": message, **details} + ui_result = { + "type": "app", + "appName": "finance", + "props": { + "action": "error", + "error": {"code": error_code, "message": message}, + "state": state.get_state_snapshot() + } + } + return self.format_response(model_result, ui_result) diff --git a/backend/tools/categories/finance/state.py b/backend/tools/categories/finance/state.py new file mode 100644 index 0000000..1587c64 --- /dev/null +++ b/backend/tools/categories/finance/state.py @@ -0,0 +1,375 @@ +""" +Finance state management singleton for the Financial Assistant. +""" +import uuid +from datetime import datetime, timedelta, date +from typing import List, Optional, Dict, Any + +from .models import ( + Transaction, Contact, ActionHistoryEntry, Stock, Budget, Bill, FinancialGoal, + BudgetCategory, BillStatus, PortfolioHolding +) + + +class FinanceState: + """ + Singleton class that manages all financial data in-memory. + + This ensures all tools access the same state instance. + State resets when the application restarts. + """ + _instance: Optional['FinanceState'] = None + + def __new__(cls) -> 'FinanceState': + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._initialize() + return cls._instance + + def _initialize(self) -> None: + """Initialize the finance state with default values.""" + self.balance: float = 12500.00 + self.savings_balance: float = 8500.00 + self.transactions: List[Transaction] = [] + self.contacts: List[Contact] = [] + self.action_history: List[ActionHistoryEntry] = [] + self.stocks: Dict[str, Stock] = {} + self.budgets: List[Budget] = [] + self.bills: List[Bill] = [] + self.goals: List[FinancialGoal] = [] + self.portfolio: List[PortfolioHolding] = [] + self._seed_mock_data() + + def _seed_mock_data(self) -> None: + """Populate the state with realistic mock data for demo purposes.""" + now = datetime.now() + current_month = now.strftime("%Y-%m") + + # Initialize contacts + self.contacts = [ + Contact(id=str(uuid.uuid4()), name="Alice", avatar_color="blue"), + Contact(id=str(uuid.uuid4()), name="Bob", avatar_color="green"), + Contact(id=str(uuid.uuid4()), name="Charlie", avatar_color="purple"), + Contact(id=str(uuid.uuid4()), name="Diana", avatar_color="orange"), + ] + + # Initialize transactions with categories + self.transactions = [ + Transaction( + id=str(uuid.uuid4()), + description="Salary Deposit", + amount=5500.00, + date=now - timedelta(days=7), + category=None + ), + Transaction( + id=str(uuid.uuid4()), + description="Whole Foods Market", + amount=-125.50, + date=now - timedelta(days=6), + category=BudgetCategory.GROCERIES + ), + Transaction( + id=str(uuid.uuid4()), + description="Uber Ride", + amount=-24.80, + date=now - timedelta(days=5), + category=BudgetCategory.TRANSPORTATION + ), + Transaction( + id=str(uuid.uuid4()), + description="Netflix Subscription", + amount=-15.99, + date=now - timedelta(days=4), + category=BudgetCategory.ENTERTAINMENT + ), + Transaction( + id=str(uuid.uuid4()), + description="Electric Bill Payment", + amount=-145.00, + date=now - timedelta(days=3), + recipient="Power Company", + category=BudgetCategory.UTILITIES + ), + Transaction( + id=str(uuid.uuid4()), + description="Restaurant - Italian Place", + amount=-68.50, + date=now - timedelta(days=2), + category=BudgetCategory.DINING + ), + Transaction( + id=str(uuid.uuid4()), + description="Dividend Payment - AAPL", + amount=45.00, + date=now - timedelta(days=1), + category=None + ), + Transaction( + id=str(uuid.uuid4()), + description="Amazon Purchase", + amount=-89.99, + date=now - timedelta(hours=12), + category=BudgetCategory.SHOPPING + ), + ] + + # Initialize stock watchlist with mock data + self.stocks = { + "AAPL": Stock( + symbol="AAPL", name="Apple Inc.", price=178.50, + change=2.35, change_percent=1.33, last_updated=now + ), + "GOOGL": Stock( + symbol="GOOGL", name="Alphabet Inc.", price=141.25, + change=-0.85, change_percent=-0.60, last_updated=now + ), + "MSFT": Stock( + symbol="MSFT", name="Microsoft Corporation", price=378.90, + change=4.20, change_percent=1.12, last_updated=now + ), + "AMZN": Stock( + symbol="AMZN", name="Amazon.com Inc.", price=185.60, + change=1.45, change_percent=0.79, last_updated=now + ), + "TSLA": Stock( + symbol="TSLA", name="Tesla Inc.", price=248.75, + change=-5.30, change_percent=-2.09, last_updated=now + ), + } + + # Initialize monthly budgets + self.budgets = [ + Budget(id=str(uuid.uuid4()), category=BudgetCategory.GROCERIES, + limit=600.00, spent=125.50, month=current_month), + Budget(id=str(uuid.uuid4()), category=BudgetCategory.DINING, + limit=300.00, spent=68.50, month=current_month), + Budget(id=str(uuid.uuid4()), category=BudgetCategory.TRANSPORTATION, + limit=200.00, spent=24.80, month=current_month), + Budget(id=str(uuid.uuid4()), category=BudgetCategory.UTILITIES, + limit=250.00, spent=145.00, month=current_month), + Budget(id=str(uuid.uuid4()), category=BudgetCategory.ENTERTAINMENT, + limit=150.00, spent=15.99, month=current_month), + Budget(id=str(uuid.uuid4()), category=BudgetCategory.SHOPPING, + limit=400.00, spent=89.99, month=current_month), + ] + + # Initialize bills + today = date.today() + self.bills = [ + Bill(id=str(uuid.uuid4()), name="Rent", amount=1800.00, + due_date=today + timedelta(days=5), status=BillStatus.PENDING, + category=BudgetCategory.OTHER), + Bill(id=str(uuid.uuid4()), name="Internet - Comcast", amount=79.99, + due_date=today + timedelta(days=10), status=BillStatus.PENDING, + category=BudgetCategory.UTILITIES), + Bill(id=str(uuid.uuid4()), name="Car Insurance", amount=125.00, + due_date=today + timedelta(days=15), status=BillStatus.PENDING, + category=BudgetCategory.TRANSPORTATION), + Bill(id=str(uuid.uuid4()), name="Phone Bill", amount=85.00, + due_date=today - timedelta(days=2), status=BillStatus.OVERDUE, + category=BudgetCategory.UTILITIES), + ] + + # Initialize financial goals + self.goals = [ + FinancialGoal( + id=str(uuid.uuid4()), name="Emergency Fund", + target_amount=15000.00, current_amount=8500.00, + target_date=date(2026, 12, 31), + description="6 months of expenses" + ), + FinancialGoal( + id=str(uuid.uuid4()), name="Vacation Fund", + target_amount=5000.00, current_amount=1200.00, + target_date=date(2026, 6, 1), + description="Summer trip to Europe" + ), + FinancialGoal( + id=str(uuid.uuid4()), name="New Car Down Payment", + target_amount=10000.00, current_amount=3500.00, + target_date=date(2027, 1, 1), + description="Down payment for new vehicle" + ), + ] + + # Initialize portfolio with some existing holdings + self.portfolio = [ + PortfolioHolding( + symbol="AAPL", shares=10, average_cost=165.00, + purchase_date=date.today() - timedelta(days=90) + ), + PortfolioHolding( + symbol="MSFT", shares=5, average_cost=350.00, + purchase_date=date.today() - timedelta(days=60) + ), + ] + + def get_state_snapshot(self) -> Dict[str, Any]: + """Return the current state as a dictionary for UI consumption.""" + return { + "balance": self.balance, + "savingsBalance": self.savings_balance, + "transactions": [ + { + "id": t.id, + "description": t.description, + "amount": t.amount, + "date": t.date.isoformat(), + "recipient": t.recipient, + "category": t.category.value if t.category else None + } + for t in self.transactions + ], + "contacts": [ + {"id": c.id, "name": c.name, "avatarColor": c.avatar_color} + for c in self.contacts + ], + "actionHistory": [ + { + "id": a.id, "action": a.action, + "description": a.description, "timestamp": a.timestamp.isoformat() + } + for a in self.action_history + ], + "budgets": [ + { + "id": b.id, "category": b.category.value, + "limit": b.limit, "spent": b.spent, "month": b.month + } + for b in self.budgets + ], + "bills": [ + { + "id": b.id, "name": b.name, "amount": b.amount, + "dueDate": b.due_date.isoformat(), "status": b.status.value, + "category": b.category.value + } + for b in self.bills + ], + "goals": [ + { + "id": g.id, "name": g.name, + "targetAmount": g.target_amount, "currentAmount": g.current_amount, + "targetDate": g.target_date.isoformat() if g.target_date else None, + "description": g.description + } + for g in self.goals + ], + "portfolio": [ + { + "symbol": p.symbol, "shares": p.shares, + "averageCost": p.average_cost, + "purchaseDate": p.purchase_date.isoformat() + } + for p in self.portfolio + ] + } + + def add_transaction( + self, description: str, amount: float, + recipient: Optional[str] = None, category: Optional[BudgetCategory] = None + ) -> Transaction: + """Add a new transaction and update the balance.""" + transaction = Transaction( + id=str(uuid.uuid4()), description=description, + amount=amount, date=datetime.now(), + recipient=recipient, category=category + ) + self.transactions.append(transaction) + self.balance += amount + + # Update budget if category is specified and amount is negative (expense) + if category and amount < 0: + self._update_budget_spent(category, abs(amount)) + + return transaction + + def _update_budget_spent(self, category: BudgetCategory, amount: float) -> None: + """Update the spent amount for a budget category.""" + current_month = datetime.now().strftime("%Y-%m") + for budget in self.budgets: + if budget.category == category and budget.month == current_month: + budget.spent += amount + break + + def log_action(self, action: str, description: str) -> ActionHistoryEntry: + """Log a voice-triggered action to the history.""" + entry = ActionHistoryEntry( + id=str(uuid.uuid4()), action=action, + description=description, timestamp=datetime.now() + ) + self.action_history.append(entry) + return entry + + def find_contact_by_name(self, name: str) -> Optional[Contact]: + """Find a contact by name (case-insensitive).""" + name_lower = name.lower() + for contact in self.contacts: + if contact.name.lower() == name_lower: + return contact + return None + + def get_contact_names(self) -> List[str]: + """Get a list of all contact names.""" + return [c.name for c in self.contacts] + + def get_stock(self, symbol: str) -> Optional[Stock]: + """Get stock quote by symbol.""" + return self.stocks.get(symbol.upper()) + + def get_bill_by_name(self, name: str) -> Optional[Bill]: + """Find a bill by name (case-insensitive partial match).""" + name_lower = name.lower() + for bill in self.bills: + if name_lower in bill.name.lower(): + return bill + return None + + def pay_bill(self, bill: Bill) -> Transaction: + """Pay a bill and create a transaction.""" + bill.status = BillStatus.PAID + return self.add_transaction( + description=f"Bill Payment - {bill.name}", + amount=-bill.amount, + recipient=bill.name, + category=bill.category + ) + + def add_to_portfolio(self, symbol: str, shares: float, price: float) -> PortfolioHolding: + """Add shares to portfolio, updating existing holding or creating new one.""" + symbol = symbol.upper() + + # Check if we already have this stock + for holding in self.portfolio: + if holding.symbol == symbol: + # Update average cost + total_shares = holding.shares + shares + total_cost = (holding.shares * holding.average_cost) + (shares * price) + holding.average_cost = total_cost / total_shares + holding.shares = total_shares + return holding + + # Create new holding + new_holding = PortfolioHolding( + symbol=symbol, + shares=shares, + average_cost=price, + purchase_date=date.today() + ) + self.portfolio.append(new_holding) + return new_holding + + def get_portfolio_holding(self, symbol: str) -> Optional[PortfolioHolding]: + """Get a portfolio holding by symbol.""" + symbol = symbol.upper() + for holding in self.portfolio: + if holding.symbol == symbol: + return holding + return None + + @classmethod + def reset(cls) -> None: + """Reset the singleton instance. Useful for testing.""" + if cls._instance is not None: + cls._instance._initialize() diff --git a/backend/tools/categories/media/__init__.py b/backend/tools/categories/media/__init__.py deleted file mode 100644 index f40eff1..0000000 --- a/backend/tools/categories/media/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .sample_image_tool import SampleImageTool -from .sample_pdf_tool import SamplePdfTool -from .sample_video_tool import SampleVideoTool - -__all__ = ['SampleImageTool', 'SamplePdfTool', 'SampleVideoTool'] \ No newline at end of file diff --git a/backend/tools/categories/media/sample_image_tool.py b/backend/tools/categories/media/sample_image_tool.py deleted file mode 100644 index 64885eb..0000000 --- a/backend/tools/categories/media/sample_image_tool.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import Dict, Any -from ...base.tool import BaseTool - -class SampleImageTool(BaseTool): - def __init__(self): - super().__init__() - self.config = { - "name": "showSampleImageTool", - "description": "Display a sample image in the tool output panel", - "shortDescription": "Showing a sample image", - "schema": { - "type": "object", - "properties": {}, - "required": [] - } - } - - async def execute(self, content: Dict[str, Any] = None) -> Dict[str, Any]: - # Model result - detailed data for the model to use - model_result = { - "type": "image", - "url": "https://placekitten.com/800/400", # Placeholder image - "description": "A sample image of a kitten" - } - - # UI result - formatted for human display - ui_result = { - "type": "image", - "content": { - "title": "Sample Image", - "url": "https://placekitten.com/800/400", - "description": "A sample image of a kitten" - } - } - - return self.format_response(model_result, ui_result) \ No newline at end of file diff --git a/backend/tools/categories/media/sample_pdf_tool.py b/backend/tools/categories/media/sample_pdf_tool.py deleted file mode 100644 index c8e91d0..0000000 --- a/backend/tools/categories/media/sample_pdf_tool.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import Dict, Any -from ...base.tool import BaseTool - -class SamplePdfTool(BaseTool): - def __init__(self): - super().__init__() - self.config = { - "name": "showSamplePdfTool", - "description": "Display a sample PDF document in the tool output panel", - "shortDescription": "Showing a sample PDF", - "schema": { - "type": "object", - "properties": {}, - "required": [] - } - } - - async def execute(self, content: Dict[str, Any] = None) -> Dict[str, Any]: - # Using a reliable sample PDF URL - pdf_url = "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf" - - # Model result - detailed data for the model to use - model_result = { - "type": "text", - "text": "Showing sample PDF in the left pane" - } - - # UI result - formatted for human display - ui_result = { - "type": "pdf", - "content": { - "title": "Sample PDF Document", - "url": pdf_url, - "description": "This is a sample PDF document showing various PDF features." - } - } - - return self.format_response(model_result, ui_result) \ No newline at end of file diff --git a/backend/tools/categories/media/sample_video_tool.py b/backend/tools/categories/media/sample_video_tool.py deleted file mode 100644 index db6d07a..0000000 --- a/backend/tools/categories/media/sample_video_tool.py +++ /dev/null @@ -1,69 +0,0 @@ -from typing import Dict, Any -from ...base.tool import BaseTool - -class SampleVideoTool(BaseTool): - def __init__(self): - super().__init__() - self.config = { - "name": "showSampleVideoTool", - "description": "Display a YouTube video in the tool output panel. Use this tool when you need to show a video demonstration, tutorial, or any YouTube content. The tool will embed the video directly in the conversation.", - "shortDescription": "Show a YouTube video", - "schema": { - "type": "object", - "properties": { - "videoId": { - "type": "string", - "description": "Optional: YouTube video ID to display. If not provided, will show a default video.", - "pattern": "^[A-Za-z0-9_-]{11}$" - }, - "showControls": { - "type": "boolean", - "description": "Optional: Whether to show video controls. Defaults to true." - } - } - } - } - - async def execute(self, content: Dict[str, Any] = None) -> Dict[str, Any]: - try: - # Get video ID from content or use default - video_id = content.get("videoId", "a9__D53WsUs") - show_controls = content.get("showControls", True) - - # Build embed URL with appropriate parameters - embed_url = f"https://www.youtube-nocookie.com/embed/{video_id}?rel=0&modestbranding=1" - if not show_controls: - embed_url += "&controls=0" - - # Model result - detailed data for the model to use - model_result = { - "type": "video", - "videoId": video_id, - "url": embed_url, - "showControls": show_controls - } - - # UI result - formatted for human display - ui_result = { - "type": "video", - "content": { - "title": "YouTube Video", - "url": embed_url, - "description": "Embedded YouTube video player" - } - } - - return self.format_response(model_result, ui_result) - - except Exception as e: - error_message = f"Failed to load video: {str(e)}" - return self.format_response( - {"error": error_message}, - { - "type": "text", - "content": { - "title": "Error", - "message": error_message - } - } - ) \ No newline at end of file diff --git a/backend/tools/categories/order/__init__.py b/backend/tools/categories/order/__init__.py deleted file mode 100644 index 0af4cf7..0000000 --- a/backend/tools/categories/order/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .track_order_tool import TrackOrderTool - -__all__ = ['TrackOrderTool'] \ No newline at end of file diff --git a/backend/tools/categories/order/track_order_tool.py b/backend/tools/categories/order/track_order_tool.py deleted file mode 100644 index ccbff09..0000000 --- a/backend/tools/categories/order/track_order_tool.py +++ /dev/null @@ -1,104 +0,0 @@ -import datetime -import hashlib -import random -from typing import Dict, Any -from ...base.tool import BaseTool - -class TrackOrderTool(BaseTool): - def __init__(self): - super().__init__() - self.config = { - "name": "trackOrderTool", - "description": "Track the status of an order by order ID", - "shortDescription": "Tracking an order", - "schema": { - "type": "object", - "properties": { - "orderId": { - "type": "string", - "description": "The order ID to track" - }, - "requestNotifications": { - "type": "boolean", - "description": "Whether to request notifications for this order" - } - }, - "required": ["orderId"] - } - } - - async def execute(self, content: Dict[str, Any]) -> Dict[str, Any]: - order_id = content.get("orderId", "") - request_notifications = content.get("requestNotifications", False) - - # Create deterministic randomness based on order ID - seed = int(hashlib.md5(str(order_id).encode(), usedforsecurity=False).hexdigest(), 16) % 10000 - random.seed(seed) - - # Possible statuses with weights - statuses = [ - "Order received", - "Processing", - "Preparing for shipment", - "Shipped", - "In transit", - "Out for delivery", - "Delivered", - "Delayed" - ] - weights = [10, 15, 15, 20, 20, 10, 5, 3] - status = random.choices(statuses, weights=weights, k=1)[0] - - # Generate delivery date based on status - today = datetime.datetime.now() - if status == "Delivered": - delivery_days = -random.randint(0, 3) - elif status == "Out for delivery": - delivery_days = 0 - else: - delivery_days = random.randint(1, 10) - - estimated_delivery = (today + datetime.timedelta(days=delivery_days)).strftime("%Y-%m-%d") - - # Model result - detailed data for the model to use - model_result = { - "orderStatus": status, - "orderNumber": order_id, - "estimatedDelivery": estimated_delivery, - "notificationStatus": f"You will receive notifications for order {order_id}" if request_notifications else "" - } - - if status == "In transit": - model_result["currentLocation"] = "Distribution Center" - elif status == "Delivered": - model_result["deliveryLocation"] = "Front Door" - elif status == "Delayed": - model_result["additionalInfo"] = "Weather delays possible" - - # UI result - formatted as a card for better presentation - ui_result = { - "type": "card", - "content": { - "title": f"Order #{order_id}", - "description": f"Status: {status}", - "details": { - "Estimated Delivery": estimated_delivery, - "Current Status": status - }, - "footer": { - "text": model_result.get("additionalInfo", "") or - model_result.get("currentLocation", "") or - model_result.get("deliveryLocation", ""), - "action": { - "text": "Track Another Order", - "url": "#" - } - } - } - } - - # Add notifications info if requested - if request_notifications: - ui_result["content"]["details"]["Notifications"] = "Enabled" - - return self.format_response(model_result, ui_result) \ No newline at end of file diff --git a/backend/tools/categories/utility/__init__.py b/backend/tools/categories/utility/__init__.py deleted file mode 100644 index 1107864..0000000 --- a/backend/tools/categories/utility/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .datetime_tool import DateAndTimeTool - -__all__ = ['DateAndTimeTool'] \ No newline at end of file diff --git a/backend/tools/categories/utility/datetime_tool.py b/backend/tools/categories/utility/datetime_tool.py deleted file mode 100644 index 6804878..0000000 --- a/backend/tools/categories/utility/datetime_tool.py +++ /dev/null @@ -1,65 +0,0 @@ -import datetime -import pytz -from typing import Dict, Any -from ...base.tool import BaseTool -import time - -class DateAndTimeTool(BaseTool): - def __init__(self): - super().__init__() - self.config = { - "name": "getDateAndTimeTool", - "description": "Get information about the current date and time", - "shortDescription": "Getting date and time information", - "schema": { - "type": "object", - "properties": {}, - "required": [] - } - } - - async def execute(self, content: Dict[str, Any] = None) -> Dict[str, Any]: - # Get current date in PST timezone - pst_timezone = pytz.timezone("America/Los_Angeles") - pst_date = datetime.datetime.now(pst_timezone) - - # time.sleep(10) - - # Model result - detailed data for the model to use - model_result = { - "formattedTime": pst_date.strftime("%I:%M %p"), - "date": pst_date.strftime("%Y-%m-%d"), - "year": pst_date.year, - "month": pst_date.month, - "day": pst_date.day, - "dayOfWeek": pst_date.strftime("%A").upper(), - "timezone": "PST" - } - - # UI result - formatted for human display using card - ui_result = { - "type": "card", - "content": { - "title": "Current Date & Time", - "description": "Current time in Pacific Time Zone", - "details": { - "Date": pst_date.strftime("%A, %B %d, %Y"), - "Time": pst_date.strftime("%I:%M %p"), - "Time Zone": "Pacific Time (PST/PDT)", - "Day of Week": pst_date.strftime("%A"), - "Month": pst_date.strftime("%B"), - "Year": str(pst_date.year) - }, - "image": "https://images.unsplash.com/photo-1501139083538-0139583c060f?q=80&w=2940&auto=format&fit=crop", - "imageAlt": "Sundial representing time", - "footer": { - "text": f"Last updated: {pst_date.strftime('%I:%M:%S %p')}", - "action": { - "text": "Refresh", - "url": "#" - } - } - } - } - - return self.format_response(model_result, ui_result) \ No newline at end of file diff --git a/backend/tools/tool_manager.py b/backend/tools/tool_manager.py index d50663c..c121761 100644 --- a/backend/tools/tool_manager.py +++ b/backend/tools/tool_manager.py @@ -1,10 +1,19 @@ -import os import json from typing import Dict, Any, List from .base import ToolRegistry -from .categories.utility import DateAndTimeTool -from .categories.media import SampleImageTool, SamplePdfTool, SampleVideoTool -from .categories.order import TrackOrderTool +from .categories.finance import ( + CheckBalanceTool, + SendMoneyTool, + GetTransactionsTool, + GetStockQuoteTool, + GetBudgetTool, + PayBillTool, + GetFinancialGoalsTool, + AddExpenseTool, + BuyStockTool, + GetPortfolioTool, +) + class ToolManager: def __init__(self): @@ -12,25 +21,32 @@ def __init__(self): self._initialize_registry() def _initialize_registry(self) -> None: - """Initialize the tool registry with all available tools""" - # Register all tools + """Initialize the tool registry with all available financial tools""" self.registry.register_tools([ - # Utility tools - DateAndTimeTool(), + # Account tools + CheckBalanceTool(), + SendMoneyTool(), + GetTransactionsTool(), + + # Budget & expense tools + GetBudgetTool(), + AddExpenseTool(), + + # Bills & payments + PayBillTool(), - # Media tools - SampleImageTool(), - SamplePdfTool(), - SampleVideoTool(), + # Investments + GetStockQuoteTool(), + BuyStockTool(), + GetPortfolioTool(), - # Order tools - TrackOrderTool(), + # Goals & planning + GetFinancialGoalsTool(), ]) async def execute_tool(self, tool_name: str, content: Dict[str, Any]) -> Dict[str, Any]: """Execute a tool by name""" try: - # All tools are now in the registry return await self.registry.execute_tool(tool_name, content) except KeyError: raise KeyError(f"Tool '{tool_name}' not found") @@ -38,7 +54,6 @@ async def execute_tool(self, tool_name: str, content: Dict[str, Any]) -> Dict[st def get_tool_configs(self) -> List[Dict[str, Any]]: """Get all tool configurations formatted for Nova Sonic""" configs = self.registry.get_tool_configs() - # Format each tool config according to Nova Sonic's expected format return [ { "toolSpec": { @@ -50,4 +65,4 @@ def get_tool_configs(self) -> List[Dict[str, Any]]: } } for config in configs - ] \ No newline at end of file + ] diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 0eebe84..7998d84 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -54,57 +54,57 @@ --card: 0 0% 100%; --card-foreground: 222.2 84% 4.9%; - + --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; - + --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; - + --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; - + --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; - + --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; - + --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; - + --radius: 0.5rem; } - + .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; - + --card: 222.2 84% 4.9%; --card-foreground: 210 40% 98%; - + --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; - + --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; - + --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; - + --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; - + --accent: 217.2 32.6% 17.5%; --accent-foreground: 210 40% 98%; - + --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; - + --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; @@ -115,7 +115,41 @@ * { @apply border-border; } + body { @apply bg-background text-foreground; } } + +/* Custom scrollbar styling for Revolut aesthetic */ +.scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; +} + +.scrollbar-hide::-webkit-scrollbar { + display: none; +} + +/* Smooth scrollbar for transaction lists */ +.overflow-y-auto { + scrollbar-width: thin; + scrollbar-color: rgba(148, 163, 184, 0.3) transparent; +} + +.overflow-y-auto::-webkit-scrollbar { + width: 6px; +} + +.overflow-y-auto::-webkit-scrollbar-track { + background: transparent; +} + +.overflow-y-auto::-webkit-scrollbar-thumb { + background-color: rgba(148, 163, 184, 0.3); + border-radius: 3px; +} + +.overflow-y-auto::-webkit-scrollbar-thumb:hover { + background-color: rgba(148, 163, 184, 0.5); +} \ No newline at end of file diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index fa94081..b64f8fc 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -5,27 +5,70 @@ import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; import AudioCapture from '@/components/audio-capture'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { User, Bot, Mic, MicOff, Power, PowerOff, X } from 'lucide-react'; +import { User, Bot, Mic, MicOff, Power, PowerOff, X, ChevronDown } from 'lucide-react'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import AudioCaptureMediaRecorder from '@/components/audio-capture-mediarecorder'; import { ToolOutput } from '@/components/tool-outputs/ToolOutput'; -import type { ToolOutput as ToolOutputType } from '@/components/tool-outputs/types'; +import type { ToolOutput as ToolOutputType, FinanceAppToolOutput } from '@/components/tool-outputs/types'; import { AudioPlaybackService } from '@/components/audio-playback'; import { motion, AnimatePresence } from 'framer-motion'; +import { FinanceApp, FinanceAction } from '@/components/apps/FinanceApp'; +import type { FinanceState } from '@/components/apps/FinanceApp'; interface TextMessage { text: string; role: string; } +interface ModelOption { + id: string; + name: string; + description: string; +} + +const AVAILABLE_MODELS: ModelOption[] = [ + { + id: 'amazon.nova-sonic-v1:0', + name: 'Nova Sonic', + description: 'Speech-to-speech model for conversational AI' + }, + { + id: 'amazon.nova-2-sonic-v1:0', + name: 'Nova 2 Sonic', + description: 'Enhanced multilingual speech-to-speech model' + } +]; + +interface LanguageOption { + code: string; + name: string; +} + +const AVAILABLE_LANGUAGES: LanguageOption[] = [ + { code: 'en', name: 'English' }, + { code: 'es', name: 'Spanish' }, + { code: 'de', name: 'German' }, + { code: 'fr', name: 'French' }, + { code: 'it', name: 'Italian' }, + { code: 'pt', name: 'Portuguese' }, + { code: 'hi', name: 'Hindi' }, +]; + // Add these message animation variants before the Home component const messageVariants = { - initial: { - opacity: 0, + initial: { + opacity: 0, y: 20, scale: 0.95 }, - animate: { - opacity: 1, + animate: { + opacity: 1, y: 0, scale: 1, transition: { @@ -34,8 +77,8 @@ const messageVariants = { damping: 20 } }, - exit: { - opacity: 0, + exit: { + opacity: 0, scale: 0.95, transition: { duration: 0.2 } } @@ -43,7 +86,7 @@ const messageVariants = { const avatarVariants = { initial: { scale: 0 }, - animate: { + animate: { scale: 1, transition: { type: "spring", @@ -59,11 +102,18 @@ export default function Home() { const [recording, setRecording] = useState(false); const [textOutputs, setTextOutputs] = useState([]); const [toolUiOutput, setToolUiOutput] = useState(null); - const [currentTool, setCurrentTool] = useState<{name: string, content: string} | null>(null); + const [currentTool, setCurrentTool] = useState<{ name: string, content: string } | null>(null); const [waitingForTool, setWaitingForTool] = useState(false); const [wsKey, setWsKey] = useState(0); const [isThinking, setIsThinking] = useState(false); const [toolConfigs, setToolConfigs] = useState([]); + const [selectedModel, setSelectedModel] = useState(AVAILABLE_MODELS[0].id); + const [selectedLanguage, setSelectedLanguage] = useState(AVAILABLE_LANGUAGES[0].code); + // Finance state management + const [financeState, setFinanceState] = useState(null); + const [financeAction, setFinanceAction] = useState(null); + const [isLoadingFinanceState, setIsLoadingFinanceState] = useState(true); + const [isAgentSpeaking, setIsAgentSpeaking] = useState(false); // Generate typing sound programmatically instead of using MP3 const [audioContext] = useState(() => typeof window !== 'undefined' ? new (window.AudioContext || (window as any).webkitAudioContext)() : null); const typingSoundInterval = useRef(null); @@ -84,10 +134,10 @@ export default function Home() { const connect = () => { if (wsRef.current) wsRef.current.close(); - wsRef.current = new WebSocket('ws://localhost:8000/ws'); + wsRef.current = new WebSocket(`ws://localhost:8000/ws?model=${encodeURIComponent(selectedModel)}&language=${encodeURIComponent(selectedLanguage)}`); setStatus('Connecting...'); setWsKey(k => k + 1); - + wsRef.current.onopen = () => setStatus('Connected'); wsRef.current.onclose = () => { setStatus('Disconnected'); @@ -99,7 +149,7 @@ export default function Home() { try { const msg = JSON.parse(event.data); console.log('[WS MESSAGE]', msg); - + if (msg.event) handleEventMessage(msg.event); } catch (e) { console.error('Error parsing message:', e); @@ -123,29 +173,29 @@ export default function Home() { } else { console.log('[SKIPPING DUPLICATE]', message); } - }, []); + }, []); // Function to generate a typing sound programmatically const playTypingBeep = useCallback(() => { if (!audioContext) return; - + try { // Create a short beep sound (like a keyboard click) const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); - + oscillator.connect(gainNode); gainNode.connect(audioContext.destination); - + // Set frequency for a subtle click sound oscillator.frequency.setValueAtTime(800, audioContext.currentTime); oscillator.frequency.exponentialRampToValueAtTime(400, audioContext.currentTime + 0.1); - + // Set volume envelope for a quick click gainNode.gain.setValueAtTime(0, audioContext.currentTime); gainNode.gain.linearRampToValueAtTime(0.1, audioContext.currentTime + 0.01); gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.1); - + oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.1); } catch (err) { @@ -180,15 +230,37 @@ export default function Home() { }; }, []); + // Fetch initial finance state on component mount + useEffect(() => { + const fetchFinanceState = async () => { + try { + setIsLoadingFinanceState(true); + const response = await fetch('http://localhost:8000/api/finance/state'); + if (response.ok) { + const data = await response.json(); + setFinanceState(data); + } else { + console.error('Failed to fetch finance state:', response.status); + } + } catch (error) { + console.error('Error fetching finance state:', error); + } finally { + setIsLoadingFinanceState(false); + } + }; + + fetchFinanceState(); + }, []); + const handleEventMessage = (event: any) => { console.log('[WS EVENT]', event); - + if (event.init) { // Store tool configurations setToolConfigs(event.init.toolConfigs); return; } - + if (event.contentStart && event.contentStart.type === 'TEXT') { const contentId = event.contentStart.contentId; let stage = 'FINAL'; @@ -211,7 +283,7 @@ export default function Home() { console.log('[TOOL USE]', event.toolUse); setWaitingForTool(true); const toolName = event.toolUse.toolName; - + // Find the tool configuration and get its short description const toolConfig = toolConfigs.find((t: any) => t.name === toolName); setCurrentTool({ @@ -235,7 +307,7 @@ export default function Home() { } else if (event.toolUiOutput) { // Handle tool UI output console.log('[TOOL UI OUTPUT]', event.toolUiOutput); - + // Handle barge-in events if (event.toolUiOutput.type === 'barge_in') { console.log('[BARGE IN] Stopping audio playback'); @@ -243,7 +315,7 @@ export default function Home() { playbackServiceRef.current.stop(); } } - + // Handle tool execution progress events if (event.toolUiOutput.type === 'tool_exec_progress') { const status = event.toolUiOutput.content.status; @@ -254,6 +326,19 @@ export default function Home() { } } + // Update finance state when finance tool outputs are received + if (event.toolUiOutput.type === 'app' && event.toolUiOutput.appName === 'finance') { + const financeOutput = event.toolUiOutput as FinanceAppToolOutput; + if (financeOutput.props?.state) { + setFinanceState(financeOutput.props.state); + } + if (financeOutput.props?.action) { + setFinanceAction(financeOutput.props.action); + // Clear the action after animation completes + setTimeout(() => setFinanceAction(null), 1500); + } + } + // Set the entire toolUiOutput object to preserve all fields (appName, props, etc) setToolUiOutput(event.toolUiOutput); } else if (event.contentEnd && event.contentEnd.type === 'TOOL') { @@ -268,7 +353,10 @@ export default function Home() { if (playbackServiceRef.current) { playbackServiceRef.current.playPCM(audioBytes); } - + + // Agent is speaking when audio is being output + setIsAgentSpeaking(true); + // If there's pending text for this contentId, show it const contentId = event.audioOutput.contentId || 'default'; if (contentId && pendingTexts.current[contentId]) { @@ -277,11 +365,14 @@ export default function Home() { displayedTextContentIds.current.add(contentId); delete pendingTexts.current[contentId]; } + } else if (event.contentEnd && event.contentEnd.type === 'AUDIO') { + // Agent stopped speaking when audio content ends + setIsAgentSpeaking(false); } else if (event.textOutput) { const text = event.textOutput.content; const role = event.textOutput.role || 'ASSISTANT'; const contentId = event.textOutput.contentId; - + console.log('[TEXT EVENT]', { text, role, @@ -303,7 +394,7 @@ export default function Home() { } else if (role === 'ASSISTANT') { // For assistant messages, check if we have corresponding audio const hasAudioOutput = event.audioOutput && event.audioOutput.contentId === contentId; - + // Show text immediately if no audio is expected if (!contentId || !hasAudioOutput) { console.log('[SHOWING ASSISTANT TEXT IMMEDIATELY]', { text, role }); @@ -380,7 +471,7 @@ export default function Home() { const base64LPCM = (base64String: string): string => { const byteCharacters = atob(base64String); const byteArrays = new Uint8Array(byteCharacters.length); - + for (let i = 0; i < byteCharacters.length; i++) { byteArrays[i] = byteCharacters.charCodeAt(i); } @@ -391,7 +482,7 @@ export default function Home() { const byteRate = sampleRate * numChannels * (bitsPerSample / 8); const blockAlign = numChannels * (bitsPerSample / 8); const wavSize = byteArrays.length + 36; - + const wavHeader = new Uint8Array(44); const view = new DataView(wavHeader.buffer); @@ -468,7 +559,7 @@ export default function Home() { return (
- {/* Left pane for tool output */} + {/* Left pane for banking dashboard */}
{/* */} - {toolUiOutput && ( + {toolUiOutput && toolUiOutput.type !== 'app' && ( + + {isExpanded && ( + + {sortedHistory.map((entry, index) => ( + +
+ + {entry.action} + + + {formatTimestamp(entry.timestamp)} + +
+

+ {entry.description} +

+
+ ))} + {sortedHistory.length === 0 && ( +
+ No actions recorded yet +
+ )} +
+ )} +
+
+ ); +} + + +// Default empty state +const defaultState: BankingState = { + balance: 0, + transactions: [], + contacts: [], + actionHistory: [], +}; + +// VoiceAgentIndicator Component - shows when agent is speaking +interface VoiceAgentIndicatorProps { + isActive: boolean; +} + +function VoiceAgentIndicator({ isActive }: VoiceAgentIndicatorProps) { + if (!isActive) return null; + + return ( + +
+ {[0, 1, 2, 3].map((i) => ( + + ))} +
+ + Agent speaking... + +
+ ); +} + +// Main BankingApp Component +export function BankingApp({ action, state = defaultState, transaction, isAgentSpeaking = false }: BankingAppProps) { + const [highlightBalance, setHighlightBalance] = useState(false); + const [highlightTransaction, setHighlightTransaction] = useState(false); + const [isPulsingBalance, setIsPulsingBalance] = useState(false); + + // Handle action-based animations + useEffect(() => { + if (action === 'highlight_balance') { + setHighlightBalance(true); + setIsPulsingBalance(true); + const timer = setTimeout(() => { + setHighlightBalance(false); + setIsPulsingBalance(false); + }, 2000); + return () => clearTimeout(timer); + } + if (action === 'money_sent') { + setHighlightBalance(true); + setHighlightTransaction(true); + const timer = setTimeout(() => { + setHighlightBalance(false); + setHighlightTransaction(false); + }, 1000); + return () => clearTimeout(timer); + } + }, [action, transaction]); + + return ( +
+ + + + + + + +
+ ); +} + +export default BankingApp; diff --git a/frontend/components/apps/FinanceApp.tsx b/frontend/components/apps/FinanceApp.tsx new file mode 100644 index 0000000..4bda684 --- /dev/null +++ b/frontend/components/apps/FinanceApp.tsx @@ -0,0 +1,486 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Wallet, + ArrowUpRight, + ArrowDownLeft, + TrendingUp, + Receipt, + Target, + PiggyBank, + Lightbulb, + User +} from 'lucide-react'; + +// TypeScript interfaces for finance data models +export interface Transaction { + id: string; + description: string; + amount: number; + date: string; + recipient?: string; + category?: string; +} + +export interface Contact { + id: string; + name: string; + avatarColor: string; +} + +export interface ActionHistoryEntry { + id: string; + action: string; + description: string; + timestamp: string; +} + +export interface Budget { + id: string; + category: string; + limit: number; + spent: number; + month: string; +} + +export interface Bill { + id: string; + name: string; + amount: number; + dueDate: string; + status: string; + category: string; +} + +export interface FinancialGoal { + id: string; + name: string; + targetAmount: number; + currentAmount: number; + targetDate?: string; + description?: string; +} + +export interface FinanceState { + balance: number; + savingsBalance?: number; + transactions: Transaction[]; + contacts: Contact[]; + actionHistory: ActionHistoryEntry[]; + budgets?: Budget[]; + bills?: Bill[]; + goals?: FinancialGoal[]; +} + +export type FinanceAction = + | 'highlight_balance' + | 'money_sent' + | 'show_transactions' + | 'show_budget' + | 'show_goals' + | 'bill_paid' + | 'expense_added' + | 'show_stock_quote' + | 'stock_purchased' + | 'show_portfolio' + | null; + +export interface FinanceAppProps { + action?: FinanceAction; + state?: FinanceState; + transaction?: Transaction; + isAgentSpeaking?: boolean; +} + +// Suggested actions for users to try +const SUGGESTED_ACTIONS = [ + { icon: Wallet, text: "What's my balance?", category: "Account" }, + { icon: ArrowUpRight, text: "Send $50 to Alice", category: "Transfer" }, + { icon: ArrowDownLeft, text: "Show my transactions", category: "History" }, + { icon: Receipt, text: "Check my budget", category: "Budget" }, + { icon: TrendingUp, text: "Get Apple stock price", category: "Stocks" }, + { icon: TrendingUp, text: "Buy 5 shares of Tesla", category: "Invest" }, + { icon: PiggyBank, text: "Show my portfolio", category: "Invest" }, + { icon: Target, text: "How are my savings goals?", category: "Goals" }, +]; + +// Utility functions +export function formatCurrency(amount: number): string { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + }).format(Math.abs(amount)); +} + +export function formatTransactionAmount(amount: number): { text: string; colorClass: string } { + if (amount >= 0) { + return { text: `+${formatCurrency(amount)}`, colorClass: 'text-green-500' }; + } + return { text: `-${formatCurrency(amount)}`, colorClass: 'text-red-500' }; +} + +export function formatDate(dateString: string): string { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); +} + +export function formatTimestamp(dateString: string): string { + const date = new Date(dateString); + return date.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); +} + +// SuggestedActions Component +interface SuggestedActionsProps { + shouldCollapse?: boolean; +} + +function SuggestedActions({ shouldCollapse = false }: SuggestedActionsProps) { + const [isExpanded, setIsExpanded] = useState(true); + + // Collapse when action is performed + useEffect(() => { + if (shouldCollapse) { + setIsExpanded(false); + } + }, [shouldCollapse]); + + return ( +
+ + + {isExpanded && ( + +
+ {SUGGESTED_ACTIONS.map((action, index) => ( + +
+ +
+
+

+ "{action.text}" +

+
+ + {action.category} + +
+ ))} +
+
+ )} +
+
+ ); +} + + +// BalanceCard Component +interface BalanceCardProps { + balance: number; + savingsBalance?: number; + highlight?: boolean; + isPulsing?: boolean; +} + +function BalanceCard({ balance, savingsBalance, highlight = false, isPulsing = false }: BalanceCardProps) { + return ( + +
+ {isPulsing && ( + + )} +
+
+

Checking

+ + {formatCurrency(balance)} + +
+ {savingsBalance !== undefined && ( +
+

Savings

+

+ {formatCurrency(savingsBalance)} +

+
+ )} +
+

Available Balance

+
+ ); +} + +// TransactionList Component +interface TransactionListProps { + transactions: Transaction[]; + highlightLatest?: boolean; +} + +function TransactionList({ transactions, highlightLatest = false }: TransactionListProps) { + // Sort transactions by date (newest first) + const sortedTransactions = [...transactions].sort( + (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() + ); + + return ( +
+
+

Recent Transactions

+
+
+ + {sortedTransactions.slice(0, 6).map((transaction, index) => { + const { text, colorClass } = formatTransactionAmount(transaction.amount); + const isLatest = index === 0 && highlightLatest; + return ( + +
+

{transaction.description}

+
+

{formatDate(transaction.date)}

+ {transaction.category && ( + + {transaction.category} + + )} +
+
+

{text}

+
+ ); + })} +
+ {transactions.length === 0 && ( +
No transactions yet
+ )} +
+
+ ); +} + +// ContactsList Component +interface ContactsListProps { + contacts: Contact[]; +} + +function ContactsList({ contacts }: ContactsListProps) { + return ( +
+

Quick Send

+
+ {contacts.map((contact, index) => ( + + + + +

{contact.name}

+
+ ))} +
+
+ ); +} + +// ActionHistory Component +interface ActionHistoryProps { + actionHistory: ActionHistoryEntry[]; +} + +function ActionHistory({ actionHistory }: ActionHistoryProps) { + const [isExpanded, setIsExpanded] = useState(false); + const sortedHistory = [...actionHistory].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); + + return ( +
+ + + {isExpanded && ( + + {sortedHistory.map((entry, index) => ( +
+
+ {entry.action} + {formatTimestamp(entry.timestamp)} +
+

{entry.description}

+
+ ))} + {sortedHistory.length === 0 && ( +
No actions recorded yet
+ )} +
+ )} +
+
+ ); +} + +// VoiceAgentIndicator Component +interface VoiceAgentIndicatorProps { + isActive: boolean; +} + +function VoiceAgentIndicator({ isActive }: VoiceAgentIndicatorProps) { + if (!isActive) return null; + return ( + +
+ {[0, 1, 2, 3].map((i) => ( + + ))} +
+ Agent speaking... +
+ ); +} + +// Default empty state +const defaultState: FinanceState = { + balance: 0, + savingsBalance: 0, + transactions: [], + contacts: [], + actionHistory: [], + budgets: [], + bills: [], + goals: [], +}; + +// Main FinanceApp Component +export function FinanceApp({ action, state = defaultState, transaction, isAgentSpeaking = false }: FinanceAppProps) { + const [highlightBalance, setHighlightBalance] = useState(false); + const [highlightTransaction, setHighlightTransaction] = useState(false); + const [isPulsingBalance, setIsPulsingBalance] = useState(false); + const [hasPerformedAction, setHasPerformedAction] = useState(false); + + useEffect(() => { + if (action) { + // Collapse suggestions when any action is performed + setHasPerformedAction(true); + } + + if (action === 'highlight_balance') { + setHighlightBalance(true); + setIsPulsingBalance(true); + const timer = setTimeout(() => { + setHighlightBalance(false); + setIsPulsingBalance(false); + }, 2000); + return () => clearTimeout(timer); + } + if (action === 'money_sent' || action === 'bill_paid' || action === 'expense_added' || action === 'stock_purchased') { + setHighlightBalance(true); + setHighlightTransaction(true); + const timer = setTimeout(() => { + setHighlightBalance(false); + setHighlightTransaction(false); + }, 1000); + return () => clearTimeout(timer); + } + }, [action, transaction]); + + return ( +
+ + + + + + + + +
+ ); +} + +export default FinanceApp; diff --git a/frontend/components/apps/__tests__/BankingApp.property.test.ts b/frontend/components/apps/__tests__/BankingApp.property.test.ts new file mode 100644 index 0000000..d46fcf2 --- /dev/null +++ b/frontend/components/apps/__tests__/BankingApp.property.test.ts @@ -0,0 +1,140 @@ +/** + * Property-Based Tests for BankingApp + * Feature: voice-banking-assistant + * + * These tests validate universal properties across all inputs using fast-check. + */ + +import { describe, it, expect } from 'vitest'; +import * as fc from 'fast-check'; +import { + formatTransactionAmount, + formatCurrency, + Transaction, + Contact, +} from '../BankingApp'; + +// Generators for test data +const transactionArb = fc.record({ + id: fc.uuid(), + description: fc.string({ minLength: 1, maxLength: 50 }), + amount: fc.double({ min: -10000, max: 10000, noNaN: true }), + date: fc.date({ min: new Date('2020-01-01'), max: new Date('2030-12-31') }).map((d) => d.toISOString()), + recipient: fc.option(fc.string({ minLength: 1, maxLength: 20 }), { nil: undefined }), +}); + +const contactArb = fc.record({ + id: fc.uuid(), + name: fc.string({ minLength: 1, maxLength: 30 }), + avatarColor: fc.constantFrom('bg-blue-500', 'bg-green-500', 'bg-purple-500', 'bg-red-500', 'bg-yellow-500'), +}); + +describe('BankingApp Property Tests', () => { + /** + * Feature: voice-banking-assistant, Property 1: Transaction Amount Formatting + * + * For any transaction with a positive amount, the formatted display string + * SHALL contain a "+" prefix and green color indicator. For any transaction + * with a negative amount, the formatted display string SHALL contain a "-" + * prefix and red color indicator. + * + * Validates: Requirements 1.5, 1.6 + */ + describe('Property 1: Transaction Amount Formatting', () => { + it('positive amounts should have + prefix and green color', () => { + fc.assert( + fc.property( + fc.double({ min: 0.01, max: 10000, noNaN: true }), + (amount) => { + const result = formatTransactionAmount(amount); + expect(result.text.startsWith('+')).toBe(true); + expect(result.colorClass).toBe('text-green-500'); + } + ), + { numRuns: 100 } + ); + }); + + it('negative amounts should have - prefix and red color', () => { + fc.assert( + fc.property( + fc.double({ min: -10000, max: -0.01, noNaN: true }), + (amount) => { + const result = formatTransactionAmount(amount); + expect(result.text.startsWith('-')).toBe(true); + expect(result.colorClass).toBe('text-red-500'); + } + ), + { numRuns: 100 } + ); + }); + + it('zero amount should be treated as positive (green)', () => { + const result = formatTransactionAmount(0); + expect(result.text.startsWith('+')).toBe(true); + expect(result.colorClass).toBe('text-green-500'); + }); + }); + + + /** + * Feature: voice-banking-assistant, Property 2: Transaction List Completeness + * + * For any list of transactions provided to the Banking_UI, the rendered output + * SHALL contain the date, description, and amount for each transaction in the list. + * + * Validates: Requirements 1.2 + */ + describe('Property 2: Transaction List Completeness', () => { + it('all transaction data should be preserved in formatting', () => { + fc.assert( + fc.property( + fc.array(transactionArb, { minLength: 0, maxLength: 20 }), + (transactions: Transaction[]) => { + for (const tx of transactions) { + const amountResult = formatTransactionAmount(tx.amount); + expect(amountResult.text).toBeDefined(); + expect(amountResult.colorClass).toBeDefined(); + + const currencyStr = formatCurrency(tx.amount); + expect(currencyStr).toContain('$'); + + expect(tx.description.length).toBeGreaterThan(0); + expect(() => new Date(tx.date)).not.toThrow(); + } + } + ), + { numRuns: 100 } + ); + }); + }); + + /** + * Feature: voice-banking-assistant, Property 3: Contacts List Completeness + * + * For any list of contacts provided to the Banking_UI, the rendered output + * SHALL display all contact names. + * + * Validates: Requirements 1.3 + */ + describe('Property 3: Contacts List Completeness', () => { + it('all contact names should be valid and displayable', () => { + fc.assert( + fc.property( + fc.array(contactArb, { minLength: 0, maxLength: 10 }), + (contacts: Contact[]) => { + for (const contact of contacts) { + expect(contact.name.length).toBeGreaterThan(0); + expect(contact.id).toBeDefined(); + expect(contact.avatarColor).toMatch(/^bg-\w+-\d+$/); + + const firstChar = contact.name.charAt(0).toUpperCase(); + expect(firstChar).toBeDefined(); + } + } + ), + { numRuns: 100 } + ); + }); + }); +}); diff --git a/frontend/components/tool-outputs/ToolOutput.tsx b/frontend/components/tool-outputs/ToolOutput.tsx index 22f90fe..6794265 100644 --- a/frontend/components/tool-outputs/ToolOutput.tsx +++ b/frontend/components/tool-outputs/ToolOutput.tsx @@ -1,22 +1,13 @@ -import { ToolOutput as ToolOutputType } from './types'; +import { ToolOutput as ToolOutputType, AppToolOutput, BankingAppToolOutput } from './types'; import { TextOutput } from './TextOutput'; import { CardOutput } from './CardOutput'; import { ImageOutput } from './ImageOutput'; import { VideoOutput } from './VideoOutput'; import { PdfOutput } from './PdfOutput'; import { ButtonOutput } from './ButtonOutput'; -import { TOOL_OUTPUT_STYLES } from '@/config/tool-outputs'; import { BargeinOutput } from './BargeinOutput'; -import { AnimatedContainer } from './AnimatedContainer'; -import { AnimatePresence } from 'framer-motion'; -import { SummaryOutput } from './SummaryOutput'; import { EchoApp } from '../apps/EchoApp'; - -const UnsupportedOutput = ({ type }: { type: string }) => ( - -

Unsupported content type: {type}

-
-); +import { BankingApp } from '../apps/BankingApp'; interface ToolOutputProps { output: ToolOutputType; @@ -32,8 +23,16 @@ const ToolOutput: React.FC = ({ output, websocket }) => { {output.type === 'barge_in' && ( )} - {output.type === 'app' && ( - + {output.type === 'app' && (output as AppToolOutput).appName === 'banking' && ( + + )} + {output.type === 'app' && (output as AppToolOutput).appName !== 'banking' && ( + )} {output.type === 'card' && output.content && ( @@ -50,7 +49,7 @@ const ToolOutput: React.FC = ({ output, websocket }) => { {output.type === 'button' && output.content && ( )} - {[ 'barge_in', 'app', 'card', 'text', 'image', 'video', 'pdf', 'button' ].indexOf(output.type) === -1 && ( + {['barge_in', 'app', 'card', 'text', 'image', 'video', 'pdf', 'button'].indexOf(output.type) === -1 && (
Unknown output type: {output.type}
)}
diff --git a/frontend/components/tool-outputs/types.ts b/frontend/components/tool-outputs/types.ts index 4844601..929e54c 100644 --- a/frontend/components/tool-outputs/types.ts +++ b/frontend/components/tool-outputs/types.ts @@ -86,6 +86,130 @@ export interface AppToolOutput { props?: Record; } +// Finance-specific types +export interface FinanceTransaction { + id: string; + description: string; + amount: number; + date: string; + recipient?: string; + category?: string; +} + +export interface FinanceContact { + id: string; + name: string; + avatarColor: string; +} + +export interface FinanceActionHistoryEntry { + id: string; + action: string; + description: string; + timestamp: string; +} + +export interface FinanceBudget { + id: string; + category: string; + limit: number; + spent: number; + month: string; +} + +export interface FinanceBill { + id: string; + name: string; + amount: number; + dueDate: string; + status: string; + category: string; +} + +export interface FinancialGoal { + id: string; + name: string; + targetAmount: number; + currentAmount: number; + targetDate?: string; + description?: string; +} + +export interface FinanceState { + balance: number; + savingsBalance?: number; + transactions: FinanceTransaction[]; + contacts: FinanceContact[]; + actionHistory: FinanceActionHistoryEntry[]; + budgets?: FinanceBudget[]; + bills?: FinanceBill[]; + goals?: FinancialGoal[]; +} + +export type FinanceAction = + | 'highlight_balance' + | 'money_sent' + | 'show_transactions' + | 'show_budget' + | 'show_goals' + | 'bill_paid' + | 'expense_added' + | 'show_stock_quote' + | 'stock_purchased' + | 'show_portfolio' + | null; + +export interface FinanceAppToolOutput { + type: 'app'; + appName: 'finance'; + props?: { + action?: FinanceAction; + state?: FinanceState; + transaction?: FinanceTransaction; + }; +} + +// Legacy Banking types (kept for backwards compatibility) +export interface BankingTransaction { + id: string; + description: string; + amount: number; + date: string; + recipient?: string; +} + +export interface BankingContact { + id: string; + name: string; + avatarColor: string; +} + +export interface BankingActionHistoryEntry { + id: string; + action: string; + description: string; + timestamp: string; +} + +export interface BankingState { + balance: number; + transactions: BankingTransaction[]; + contacts: BankingContact[]; + actionHistory: BankingActionHistoryEntry[]; +} + +export type BankingAction = 'highlight_balance' | 'money_sent' | 'show_transactions' | null; + +export interface BankingAppToolOutput { + type: 'app'; + appName: 'banking'; + props?: { + action?: BankingAction; + state?: BankingState; + transaction?: BankingTransaction; + }; +} + export type ToolOutput = | TextToolOutput | ImageToolOutput @@ -94,4 +218,6 @@ export type ToolOutput = | ButtonToolOutput | BargeinToolOutput | AppToolOutput + | FinanceAppToolOutput + | BankingAppToolOutput | CardToolOutput; \ No newline at end of file diff --git a/frontend/components/ui/select.tsx b/frontend/components/ui/select.tsx new file mode 100644 index 0000000..4ed60d8 --- /dev/null +++ b/frontend/components/ui/select.tsx @@ -0,0 +1,159 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { Check, ChevronDown, ChevronUp } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d53a141..8c61cbc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@radix-ui/react-avatar": "^1.1.7", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.4", "@radix-ui/react-slider": "^1.3.2", "@radix-ui/react-slot": "^1.2.0", @@ -24,14 +25,21 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.2", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@vitejs/plugin-react": "^4.7.0", "eslint": "^9", "eslint-config-next": "15.3.1", + "fast-check": "^4.5.3", + "happy-dom": "^20.4.0", + "jsdom": "^27.0.1", "tailwindcss": "^4", "tw-animate-css": "^1.2.8", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.0.18" } }, "node_modules/@alloc/quick-lru": { @@ -47,831 +55,2278 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@emnapi/core": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", - "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz", + "integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" + "@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.4" } }, - "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.6", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz", + "integrity": "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.4" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", - "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "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.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", - "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.4.3" + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@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": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" }, "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "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==", + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "license": "MIT", + "bin": { + "json5": "lib/cli.js" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": ">=6" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=6.9.0" } }, - "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.0" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", - "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "Apache-2.0", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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==", + "dev": true, + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.0" } }, - "node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.15" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "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" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@eslint/js": { - "version": "9.25.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz", - "integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.0" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "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==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.0" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "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==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.13.0", - "levn": "^0.4.1" - }, + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.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==", + "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==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=18.18.0" + "node": ">=6.9.0" } }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { - "node": ">=18.18.0" + "node": ">=6.9.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.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==", + "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": "Apache-2.0", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, "engines": { - "node": ">=12.22" + "node": ">=6.9.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "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": "Apache-2.0", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, "engines": { - "node": ">=18.18" + "node": ">=6.9.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" + "node": ">=6.9.0" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": 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/@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", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.26.tgz", + "integrity": "sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0" + }, + "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", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", + "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", + "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", + "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": [ - "arm64" + "ppc64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "aix" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "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": [ - "x64" + "arm" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "android" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "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": [ - "arm" + "arm64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "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": [ - "arm64" + "x64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "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": [ - "s390x" + "arm64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "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" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "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" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "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" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "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" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "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" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "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": [ - "s390x" + "ia32" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "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": [ - "x64" + "loong64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" + "node": ">=18" } }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "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": [ - "arm64" + "mips64el" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + "node": ">=18" } }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "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": [ - "x64" + "ppc64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + "node": ">=18" } }, - "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "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": [ - "wasm32" + "riscv64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "dev": true, + "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.2.0" - }, + "os": [ + "linux" + ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "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": [ - "ia32" + "s390x" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "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" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.9.tgz", - "integrity": "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==", + "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, - "dependencies": { - "@emnapi/core": "^1.4.0", - "@emnapi/runtime": "^1.4.0", - "@tybys/wasm-util": "^0.9.0" - } - }, - "node_modules/@next/env": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.6.tgz", - "integrity": "sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==", - "license": "MIT" - }, - "node_modules/@next/eslint-plugin-next": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.1.tgz", - "integrity": "sha512-oEs4dsfM6iyER3jTzMm4kDSbrQJq8wZw5fmT6fg2V3SMo+kgG+cShzLfEV20senZzv8VF+puNLheiGPlBGsv2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-glob": "3.3.1" + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@next/swc-darwin-arm64": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.6.tgz", - "integrity": "sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==", + "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": [ - "arm64" + "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "netbsd" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.6.tgz", - "integrity": "sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg==", + "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": [ - "x64" + "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "openbsd" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.6.tgz", - "integrity": "sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg==", + "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": [ - "arm64" + "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "openbsd" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.6.tgz", - "integrity": "sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==", + "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": [ - "linux" + "openharmony" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.6.tgz", - "integrity": "sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==", + "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": [ - "linux" + "sunos" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.6.tgz", - "integrity": "sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==", + "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": [ - "x64" + "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.6.tgz", - "integrity": "sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==", + "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": [ - "arm64" + "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">= 10" + "node": ">=18" } }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.6.tgz", - "integrity": "sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ==", + "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": ">= 10" + "node": ">=18" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", + "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "eslint-visitor-keys": "^3.4.3" }, "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==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 8" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "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==", + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "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.2.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", + "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "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": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "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": ">= 8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "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==", + "node_modules/@eslint/js": { + "version": "9.25.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz", + "integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==", "dev": true, "license": "MIT", "engines": { - "node": ">=12.4.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@radix-ui/number": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", - "license": "MIT" + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", - "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", + "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.5" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, - "node_modules/@radix-ui/react-avatar": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.7.tgz", - "integrity": "sha512-V7ODUt4mUoJTe3VUxZw6nfURxaPALVqmDQh501YmaQsk3D8AZQrOPRnfKn4H7JGDLBc0KqLhT94H79nV88ppNg==", + "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.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@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==", + "dev": 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==", + "dev": 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==", + "dev": 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==", + "dev": 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==", + "dev": 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.9", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.9.tgz", + "integrity": "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.0", + "@emnapi/runtime": "^1.4.0", + "@tybys/wasm-util": "^0.9.0" + } + }, + "node_modules/@next/env": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.6.tgz", + "integrity": "sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.1.tgz", + "integrity": "sha512-oEs4dsfM6iyER3jTzMm4kDSbrQJq8wZw5fmT6fg2V3SMo+kgG+cShzLfEV20senZzv8VF+puNLheiGPlBGsv2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.6.tgz", + "integrity": "sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.6.tgz", + "integrity": "sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.6.tgz", + "integrity": "sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.6.tgz", + "integrity": "sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.6.tgz", + "integrity": "sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.6.tgz", + "integrity": "sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.6.tgz", + "integrity": "sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.6.tgz", + "integrity": "sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ==", + "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/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.7.tgz", + "integrity": "sha512-V7ODUt4mUoJTe3VUxZw6nfURxaPALVqmDQh501YmaQsk3D8AZQrOPRnfKn4H7JGDLBc0KqLhT94H79nV88ppNg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.4.tgz", + "integrity": "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { @@ -889,16 +2344,13 @@ } } }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.4.tgz", - "integrity": "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==", + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-slot": "1.2.0" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -915,11 +2367,14 @@ } } }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -930,43 +2385,111 @@ } } }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", - "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.0" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -983,6 +2506,24 @@ } } }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-separator": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.4.tgz", @@ -1026,26 +2567,180 @@ }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", - "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1057,47 +2752,59 @@ } } }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", @@ -1109,71 +2816,368 @@ } } }, - "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", - "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz", + "integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz", + "integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", + "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", + "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz", + "integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz", + "integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz", + "integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz", + "integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz", + "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz", + "integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz", + "integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz", + "integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz", + "integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz", + "integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz", + "integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz", + "integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz", + "integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz", + "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz", + "integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz", + "integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz", + "integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.5.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "optional": true, + "os": [ + "openharmony" + ] }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz", + "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz", + "integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz", + "integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz", + "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@rtsao/scc": { "version": "1.1.0", @@ -1189,6 +3193,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -1472,6 +3483,64 @@ "tailwindcss": "4.1.4" } }, + "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", + "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/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "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/@tybys/wasm-util": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", @@ -1483,10 +3552,80 @@ "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.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "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" }, @@ -1534,6 +3673,23 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.31.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz", @@ -1978,38 +4134,170 @@ ], "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.7.2.tgz", + "integrity": "sha512-Ywv20XHvHTDRQs12jd3MY8X5C8KLjDbg/jyaal/QLKx3fAShhJyD4blEANInsjxW3P7isHx1Blt56iUDDJO3jg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.7.2.tgz", + "integrity": "sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "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.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.7.2.tgz", - "integrity": "sha512-Ywv20XHvHTDRQs12jd3MY8X5C8KLjDbg/jyaal/QLKx3fAShhJyD4blEANInsjxW3P7isHx1Blt56iUDDJO3jg==", - "cpu": [ - "ia32" - ], + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.7.2.tgz", - "integrity": "sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, "node_modules/acorn": { "version": "8.14.1", @@ -2034,6 +4322,16 @@ "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/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2051,6 +4349,16 @@ "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", @@ -2074,6 +4382,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -2242,6 +4562,16 @@ "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", @@ -2302,6 +4632,26 @@ "dev": true, "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "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.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2326,6 +4676,40 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": 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": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -2398,9 +4782,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001715", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", - "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", "funding": [ { "type": "opencollective", @@ -2417,6 +4801,16 @@ ], "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", @@ -2513,6 +4907,13 @@ "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==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2528,6 +4929,46 @@ "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/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -2542,6 +4983,30 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-urls": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "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", @@ -2614,6 +5079,13 @@ } } }, + "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", @@ -2657,6 +5129,16 @@ "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -2667,6 +5149,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -2680,6 +5168,13 @@ "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", @@ -2695,6 +5190,13 @@ "node": ">= 0.4" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.279", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz", + "integrity": "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg==", + "dev": true, + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -2716,6 +5218,19 @@ "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.23.9", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", @@ -2830,6 +5345,13 @@ "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", @@ -2890,6 +5412,58 @@ "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==", + "dev": 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", @@ -3319,6 +5893,16 @@ "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", @@ -3329,6 +5913,39 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-check": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.5.3.tgz", + "integrity": "sha512-IE9csY7lnhxBnA8g/WI5eg/hygA6MGWJMSNfFRrBlXUciADEhS1EDB0SIsMSvzubzIlOBbVITSsypCsW717poA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^7.0.0" + }, + "engines": { + "node": ">=12.17.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", @@ -3497,6 +6114,21 @@ } } }, + "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", @@ -3538,6 +6170,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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==", + "dev": 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", @@ -3563,6 +6205,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -3678,6 +6329,47 @@ "dev": true, "license": "MIT" }, + "node_modules/happy-dom": { + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.4.0.tgz", + "integrity": "sha512-RDeQm3dT9n0A5f/TszjUmNCLEuPnMGv3Tv4BmNINebz/h17PA6LMBcxJ5FrcqltNBMh9jA/8ufgDdBYUdBt+eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "entities": "^4.5.0", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/happy-dom/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/happy-dom/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -3772,6 +6464,60 @@ "node": ">= 0.4" } }, + "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/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", @@ -4074,6 +6820,13 @@ "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", @@ -4281,6 +7034,59 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "27.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.1.tgz", + "integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/dom-selector": "^6.7.2", + "cssstyle": "^5.3.1", + "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", + "rrweb-cssom": "^0.8.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" + }, + "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==", + "dev": 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", @@ -4650,6 +7456,16 @@ "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/lucide-react": { "version": "0.468.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", @@ -4659,6 +7475,26 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, + "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/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4669,6 +7505,13 @@ "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", @@ -4861,6 +7704,13 @@ "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==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4984,6 +7834,17 @@ "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", @@ -5065,6 +7926,19 @@ "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", @@ -5092,6 +7966,13 @@ "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", @@ -5160,6 +8041,41 @@ "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", @@ -5182,6 +8098,23 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5231,6 +8164,85 @@ "dev": true, "license": "MIT" }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5275,6 +8287,16 @@ "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.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -5327,6 +8349,58 @@ "node": ">=0.10.0" } }, + "node_modules/rollup": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz", + "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==", + "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.57.0", + "@rollup/rollup-android-arm64": "4.57.0", + "@rollup/rollup-darwin-arm64": "4.57.0", + "@rollup/rollup-darwin-x64": "4.57.0", + "@rollup/rollup-freebsd-arm64": "4.57.0", + "@rollup/rollup-freebsd-x64": "4.57.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", + "@rollup/rollup-linux-arm-musleabihf": "4.57.0", + "@rollup/rollup-linux-arm64-gnu": "4.57.0", + "@rollup/rollup-linux-arm64-musl": "4.57.0", + "@rollup/rollup-linux-loong64-gnu": "4.57.0", + "@rollup/rollup-linux-loong64-musl": "4.57.0", + "@rollup/rollup-linux-ppc64-gnu": "4.57.0", + "@rollup/rollup-linux-ppc64-musl": "4.57.0", + "@rollup/rollup-linux-riscv64-gnu": "4.57.0", + "@rollup/rollup-linux-riscv64-musl": "4.57.0", + "@rollup/rollup-linux-s390x-gnu": "4.57.0", + "@rollup/rollup-linux-x64-gnu": "4.57.0", + "@rollup/rollup-linux-x64-musl": "4.57.0", + "@rollup/rollup-openbsd-x64": "4.57.0", + "@rollup/rollup-openharmony-arm64": "4.57.0", + "@rollup/rollup-win32-arm64-msvc": "4.57.0", + "@rollup/rollup-win32-ia32-msvc": "4.57.0", + "@rollup/rollup-win32-x64-gnu": "4.57.0", + "@rollup/rollup-win32-x64-msvc": "4.57.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5406,6 +8480,26 @@ "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.25.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", @@ -5613,6 +8707,13 @@ "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/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -5639,6 +8740,20 @@ "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/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -5832,6 +8947,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/tailwind-merge": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", @@ -5859,15 +8981,32 @@ "node": ">=6" } }, + "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.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "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.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -5877,11 +9016,14 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "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" }, @@ -5892,9 +9034,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -5904,6 +9046,36 @@ "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", @@ -5917,6 +9089,32 @@ "node": ">=8.0" } }, + "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", @@ -6123,6 +9321,37 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.7.2" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": 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", @@ -6133,6 +9362,49 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", @@ -6142,6 +9414,264 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_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", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "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.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "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.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "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==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "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", @@ -6247,6 +9777,23 @@ "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", @@ -6257,6 +9804,52 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "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==", + "dev": true, + "license": "ISC" + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 78c0c64..8765c4f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@radix-ui/react-avatar": "^1.1.7", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.4", "@radix-ui/react-slider": "^1.3.2", "@radix-ui/react-slot": "^1.2.0", @@ -25,13 +26,20 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.2", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@vitejs/plugin-react": "^4.7.0", "eslint": "^9", "eslint-config-next": "15.3.1", + "fast-check": "^4.5.3", + "happy-dom": "^20.4.0", + "jsdom": "^27.0.1", "tailwindcss": "^4", "tw-animate-css": "^1.2.8", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.0.18" } } diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..60c7c7a --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'happy-dom', + globals: true, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, '.'), + }, + }, +}); diff --git a/frontend/vitest.setup.ts b/frontend/vitest.setup.ts new file mode 100644 index 0000000..2be4857 --- /dev/null +++ b/frontend/vitest.setup.ts @@ -0,0 +1 @@ +import '@testing-library/dom';