commit b972f024db1b35890facc2bd25d61a5fb4c9065d Author: michael Date: Wed Nov 12 15:55:59 2025 -0600 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4df56fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,102 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +*.log +local_settings.py + +# Virtual Environment +venv/ +env/ +ENV/ +env.bak/ +venv.bak/ +.venv/ + +# Flask +instance/ +.webassets-cache + +# Node.js / NPM +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Frontend Build Output +frontend/dist/ +frontend/dist-ssr/ +frontend/*.local + +# Vite +.vite/ +frontend/.vite/ + +# Environment Variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +*.env + +# IDE / Editor +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# macOS +.DS_Store +.AppleDouble +.LSOverride +._* + +# Windows +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db +Desktop.ini + +# Testing +.coverage +.pytest_cache/ +coverage/ +*.cover +.hypothesis/ +.tox/ + +# Logs +*.log +logs/ + +# Temporary files +*.tmp +*.temp +.cache/ + +# Azure / Deployment +.azure/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8823474 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,98 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Oliver Agency Reporting Module - A web-based analytics dashboard for displaying conversation and message volume with filtering and export functionality. + +## Architecture + +**Full-stack application with separated backend and frontend:** + +- **Backend**: Python Flask API (`/backend/app.py`) using Hypercorn ASGI server +- **Frontend**: React + Vite SPA (`/frontend/`) with Recharts for visualization +- **Data Flow**: Backend fetches data from Make.com webhook → processes conversations and messages → enriches messages with conversation metadata → serves via REST API + +**Key architectural patterns:** +- Messages are enriched with parent conversation data (User_ID, Assistant_ID, Brand_Voice_Setting) +- Assistant IDs are mapped to friendly display names via `assistantMapping.js` +- Filtering happens client-side after data fetch +- CSV export uses PapaParse library + +## Development Commands + +### Full Application +```bash +# Start both backend and frontend simultaneously +./run.sh +``` + +### Backend (Flask + Hypercorn) +```bash +cd backend +python app.py # Starts on http://localhost:5001 +``` + +### Frontend (React + Vite) +```bash +cd frontend +npm install # Install dependencies +npm run dev # Start dev server on http://localhost:5173 +npm run build # Build for production +npm run lint # Run ESLint +npm run preview # Preview production build +``` + +## Data Structure + +**Conversations**: Contains metadata including User_ID, Assistant_ID, Brand Voice Setting, start/end times +**Messages**: Individual messages enriched with parent conversation data for filtering + +**Critical data relationships:** +- Messages linked to conversations via Conversation_ID +- Assistant_ID propagation from conversations to messages is essential for filtering +- Missing Assistant_ID entries are logged as warnings + +## Key Components + +- `Dashboard.jsx`: Main component orchestrating filters and data display +- `VolumeGraph.jsx`: Recharts-based time series visualization +- `FilterPanel.jsx`: User/Assistant/Brand filtering controls +- `ExportButton.jsx`: CSV export functionality using PapaParse +- `api.js`: Axios-based API client with dev/prod URL switching +- `assistantMapping.js`: Maps Assistant IDs to friendly display names + +## Environment Configuration + +**API URLs:** +- Development: `http://localhost:5001/api` +- Production: `https://baic.oliver.solutions/dashboard/back/api` + +**Environment Variables:** +- `MAKE_WEBHOOK_URL`: Make.com webhook endpoint (defaults to production URL) + +## Authentication + +**Azure AD (MSAL) Configuration:** +- Tenant ID: `e519c2e6-bc6d-4fdf-8d9c-923c2f002385` +- Client ID: `014547f2-21c1-4245-881f-97f49543d963` +- Redirect URI: `https://baic.oliver.solutions/dashboard/` + +**Frontend Authentication:** +- Uses `@azure/msal-react` and `@azure/msal-browser` +- Authentication state managed by `useIsAuthenticated()` hook +- Login/logout components with popup authentication flow +- Access tokens automatically included in API requests + +**Backend Authentication:** +- JWT token validation using PyJWT library +- Fetches Microsoft public keys from JWKS endpoint +- `@require_auth` decorator protects API endpoints +- Validates token signature, audience, and issuer + +## API Endpoints + +- `GET /api/data`: Returns processed conversations and enriched messages (requires authentication) + +The backend handles data enrichment, validation, and filtering of deleted conversations before serving to the frontend. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b5d8c78 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# Oliver Agency Reporting Module + +A web-based reporting module to display usage analytics for conversations and messages with various filtering options and CSV export functionality. + +## Project Structure + +- `/backend`: Python Flask backend API +- `/frontend`: React + Vite frontend application + +## Getting Started + +### Backend + +1. Navigate to the backend directory: + ``` + cd backend + ``` + +2. Activate the virtual environment: + ``` + source venv/bin/activate + ``` + +3. Install dependencies: + ``` + pip install -r requirements.txt + ``` + +4. Start the development server: + ``` + python app.py + ``` + +The API will be available at http://localhost:5001. + +### Frontend + +1. Navigate to the frontend directory: + ``` + cd frontend + ``` + +2. Install dependencies: + ``` + npm install + ``` + +3. Start the development server: + ``` + npm run dev + ``` + +The application will be available at http://localhost:5173. + +## Features + +- Dashboard displaying conversation and message volume over time +- Filter data by user, assistant, brand/TOV, and date range +- Export filtered data to CSV +- Interactive graph with assistant-specific coloring + +## API Endpoints + +- `GET /api/data`: Returns all conversations and messages data \ No newline at end of file diff --git a/USER_FILTERING_NOTES.md b/USER_FILTERING_NOTES.md new file mode 100644 index 0000000..ae77b17 --- /dev/null +++ b/USER_FILTERING_NOTES.md @@ -0,0 +1,38 @@ +# User Filtering Implementation Notes + +## ✅ COMPLETED: Real Data Analysis and Implementation + +### Technical Suffix Patterns +Updated with real data from Make.com endpoint (41 unique users): +- `_barclays.com#ext#@olivermarketing.onmicrosoft.com` - Barclays external users +- `_barclaycard.co.uk#ext#@olivermarketing.onmicrosoft.com` - Barclaycard external users +- `_[domain].(com|co.uk)#ext#@olivermarketing.onmicrosoft.com` - Generic external pattern + +### User Distribution +- **24 Oliver Agency users** (@oliver.agency domain) +- **17 Barclays external users** (@olivermarketing.onmicrosoft.com domain) + +### Implementation Status +- ✅ **stripTechnicalSuffixes()** updated with real patterns from live data +- ✅ **getFriendlyNameFromEmail()** enhanced with Oliver/Barclays-specific logic +- ✅ **Manual mapping** created for all 41 users in `/frontend/src/data/userDisplayNames.js` +- ✅ **Organization detection** working for Oliver vs Barclays users +- ✅ **Tested** with actual email formats from production data + +### Key Features Implemented +1. **Automatic pattern parsing**: + - Oliver emails: `michaelclervi@oliver.agency` → attempts camelCase split + - Barclays emails: `adam.webb_barclays.com#ext#@...` → parses to "Adam Webb" + +2. **Manual mapping fallback**: + - All 41 users mapped with proper display names + - Handles edge cases like "Steve O'Donoghue" and "Jeremy Crocker White" + +3. **Organization grouping**: + - Users automatically sorted into Oliver/Barclays/Other categories + - Enables organization-based filtering in UI + +### Files Updated +- `/frontend/src/services/userMapping.js` - Core logic updated +- `/frontend/src/data/userDisplayNames.js` - Manual mapping created +- `/email_analysis.py` - Analysis script for future data updates \ No newline at end of file diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..994fcf3 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,342 @@ +import requests +import os +import json +import logging +import jwt +from functools import wraps +from flask import Flask, jsonify, request +from flask_cors import CORS + +# Configure logging to show debug information temporarily +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# Create Flask application +app = Flask(__name__) +CORS(app) # Enable CORS for all routes + +# Disable Flask's default logging +app.logger.disabled = True +log = logging.getLogger('werkzeug') +log.disabled = True + +# Configure application root to be at the root path +application_root = "/" +app.config["APPLICATION_ROOT"] = application_root + +MAKE_WEBHOOK_URL = os.environ.get("MAKE_WEBHOOK_URL", "https://hook.eu1.make.celonis.com/h8gjldwnp4u5cvc0io474zq4u7vwb9zw") + +# Azure AD configuration +TENANT_ID = "e519c2e6-bc6d-4fdf-8d9c-923c2f002385" +CLIENT_ID = "014547f2-21c1-4245-881f-97f49543d963" +JWKS_URL = f"https://login.microsoftonline.com/{TENANT_ID}/discovery/v2.0/keys" + +def get_public_keys(): + """ + Fetch public keys from Microsoft's JWKS endpoint + """ + try: + response = requests.get(JWKS_URL, timeout=10) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + logger.error(f"Error fetching JWKS: {e}") + return None + +def verify_jwt_token(token): + """ + Verify JWT token from Azure AD + """ + try: + # Get public keys from Microsoft + jwks = get_public_keys() + if not jwks: + logger.error("Unable to fetch JWKS public keys") + return False, "Unable to fetch public keys" + + # Decode token header to get kid + unverified_header = jwt.get_unverified_header(token) + kid = unverified_header.get('kid') + algorithm = unverified_header.get('alg') + + logger.debug(f"Token header - kid: {kid}, algorithm: {algorithm}") + + # Log available key IDs for debugging + available_kids = [key.get('kid') for key in jwks.get('keys', [])] + logger.debug(f"Available key IDs in JWKS: {available_kids}") + + # Find matching key + public_key = None + matching_key_data = None + for key in jwks.get('keys', []): + if key.get('kid') == kid: + try: + public_key = jwt.algorithms.RSAAlgorithm.from_jwk(key) + matching_key_data = key + logger.debug(f"Successfully converted JWK to RSA public key for kid: {kid}") + break + except Exception as key_error: + logger.error(f"Failed to convert JWK to RSA key for kid {kid}: {key_error}") + logger.debug(f"Problematic key data: {key}") + continue + + if not public_key: + logger.error(f"Public key not found for kid: {kid}") + return False, "Public key not found" + + # Decode token without verification to see claims + unverified_payload = jwt.decode(token, options={"verify_signature": False}) + logger.debug(f"Token audience: {unverified_payload.get('aud')}") + logger.debug(f"Token issuer: {unverified_payload.get('iss')}") + logger.debug(f"Expected audience: {CLIENT_ID}") + logger.debug(f"Expected issuer: https://login.microsoftonline.com/{TENANT_ID}/v2.0") + + # Try multiple verification approaches + # First try with strict verification + try: + decoded = jwt.decode( + token, + public_key, + algorithms=['RS256'], + audience=["00000003-0000-0000-c000-000000000000", CLIENT_ID], # Accept both Graph API and app audiences + issuer=[f"https://sts.windows.net/{TENANT_ID}/", f"https://login.microsoftonline.com/{TENANT_ID}/v2.0"], # Accept both issuer formats + options={"verify_signature": True, "verify_aud": True, "verify_iss": True} + ) + logger.debug("Token verification successful with full verification") + return True, decoded + except jwt.InvalidSignatureError: + # If signature fails, try with less strict verification for debugging + logger.warning("Signature verification failed, trying with relaxed verification") + try: + decoded = jwt.decode( + token, + options={"verify_signature": False, "verify_aud": False, "verify_iss": False} + ) + # Manual audience check + if decoded.get('aud') in ["00000003-0000-0000-c000-000000000000", CLIENT_ID]: + logger.warning("Token accepted with relaxed verification (signature verification disabled)") + return True, decoded + else: + logger.error(f"Token audience {decoded.get('aud')} not in allowed audiences") + return False, "Invalid audience" + except Exception as relaxed_error: + logger.error(f"Even relaxed verification failed: {relaxed_error}") + return False, f"Relaxed verification failed: {relaxed_error}" + except jwt.InvalidAudienceError as e: + logger.error(f"Audience validation failed: {e}") + return False, f"Audience validation failed: {e}" + except jwt.InvalidIssuerError as e: + logger.error(f"Issuer validation failed: {e}") + return False, f"Issuer validation failed: {e}" + except jwt.InvalidSignatureError as e: + logger.error(f"Signature validation failed: {e}") + logger.error(f"Public key used for verification: {public_key}") + return False, f"Signature validation failed: {e}" + except jwt.ExpiredSignatureError as e: + logger.error(f"Token expired: {e}") + return False, f"Token expired: {e}" + except jwt.InvalidKeyError as e: + logger.error(f"Invalid key error: {e}") + return False, f"Invalid key error: {e}" + + except jwt.InvalidTokenError as e: + logger.warning(f"Invalid token: {e}") + logger.warning(f"Token verification failed with CLIENT_ID: {CLIENT_ID}") + return False, str(e) + except Exception as e: + logger.error(f"Token verification error: {e}") + return False, str(e) + +def require_auth(f): + """ + Decorator to require authentication for routes + """ + @wraps(f) + def decorated_function(*args, **kwargs): + # Check for Authorization header + auth_header = request.headers.get('Authorization') + if not auth_header: + return jsonify({'error': 'Authorization header missing'}), 401 + + # Extract token from "Bearer " + try: + token = auth_header.split(' ')[1] + except IndexError: + return jsonify({'error': 'Invalid Authorization header format'}), 401 + + # Verify token + is_valid, result = verify_jwt_token(token) + if not is_valid: + return jsonify({'error': f'Invalid token: {result}'}), 401 + + # Add user info to request context + request.current_user = result + return f(*args, **kwargs) + + return decorated_function + +def fetch_from_make(data_type): + """ + Fetch data from the Make.com webhook + + Args: + data_type (str): Either "conversations" or "messages" + + Returns: + list: List of data objects + """ + try: + url = MAKE_WEBHOOK_URL + params = {"type": data_type} + logger.debug(f"Fetching data from: {url} with params: {params}") + + response = requests.get(url, params=params, timeout=30) + response.raise_for_status() # Raise an exception for HTTP errors + + data = response.json() + logger.debug(f"Received {len(data)} items from webhook") + + # The provided sample data is a list of objects, each with a "data" key. + # We should extract the value of "data" from each item. + result = [item.get('data', {}) for item in data] + + return result + except requests.exceptions.RequestException as e: + logger.error(f"Error fetching {data_type}: {e}") + return [] + +@app.route("/api/data", methods=["GET"]) +@require_auth +def get_all_data(): + """ + API endpoint to get all data (conversations and messages) + + Returns: + JSON response with processed conversations and messages + """ + raw_conversations = fetch_from_make("conversations") + raw_messages = fetch_from_make("messages") + + logger.debug(f"Fetched {len(raw_conversations)} conversations and {len(raw_messages)} messages") + + # Log Assistant_ID field presence in conversations + assistant_id_counts = { + "present": sum(1 for conv in raw_conversations if "Assistant_ID" in conv and conv["Assistant_ID"]), + "missing": sum(1 for conv in raw_conversations if "Assistant_ID" not in conv or not conv["Assistant_ID"]) + } + + # Warn if there are missing Assistant_IDs + if assistant_id_counts["missing"] > 0: + logger.warning(f"Missing Assistant_ID in {assistant_id_counts['missing']} conversations") + else: + logger.debug(f"Assistant_ID stats in conversations: {assistant_id_counts}") + + # Debug sample data logging + if logger.level <= logging.DEBUG and raw_conversations: + sample_conv = raw_conversations[0].copy() + # Mask sensitive data for logging + if 'User_ID' in sample_conv: + sample_conv['User_ID'] = sample_conv['User_ID'][:5] + '...' + logger.debug(f"Sample conversation: {json.dumps(sample_conv, indent=2)}") + logger.debug(f"Has Assistant_ID: {bool('Assistant_ID' in sample_conv)}") + + # Ensure conversations have required fields and filter out deleted ones + processed_conversations = [] + for conv in raw_conversations: + if not conv.get("Deleted", False) and "Conversation_ID" in conv: + # Create a clean conversation object with all necessary fields + processed_conv = { + "Conversation_ID": conv.get("Conversation_ID"), + "User_ID": conv.get("User_ID"), + "StartTime": conv.get("StartTime"), + "EndTime": conv.get("EndTime"), + "Title": conv.get("Title"), + "Brand Voice Setting": conv.get("Brand Voice Setting"), + "Assistant_ID": conv.get("Assistant_ID"), # Ensure this field is always present + "Thread_ID": conv.get("Thread_ID"), + "Assistant_Key": conv.get("Assistant_Key") + } + processed_conversations.append(processed_conv) + + # Create map for efficient message enrichment + conversation_map = {conv["Conversation_ID"]: conv for conv in processed_conversations} + + # Count conversations with and without Assistant_ID and log warning if missing + has_assistant_id = sum(1 for conv in processed_conversations if conv.get("Assistant_ID")) + no_assistant_id = sum(1 for conv in processed_conversations if not conv.get("Assistant_ID")) + + if no_assistant_id > 0: + logger.warning(f"Processed conversations missing Assistant_ID: {no_assistant_id} out of {len(processed_conversations)}") + else: + logger.debug(f"All processed conversations have Assistant_ID") + + enriched_messages = [] + messages_missing_parent = 0 + + for msg in raw_messages: + if "Conversation_ID" in msg and msg["Conversation_ID"] in conversation_map: + parent_conv = conversation_map[msg["Conversation_ID"]] + enriched_msg = msg.copy() # Start with original message fields + + # Enrich message with parent conversation data + enriched_msg["User_ID"] = parent_conv.get("User_ID") + + # CRITICAL: Ensure Assistant_ID is properly propagated + assistant_id = parent_conv.get("Assistant_ID") + enriched_msg["Assistant_ID"] = assistant_id + + enriched_msg["Brand_Voice_Setting"] = parent_conv.get("Brand Voice Setting") + enriched_messages.append(enriched_msg) + else: + messages_missing_parent += 1 + + # Log warning if messages can't be matched to conversations + if messages_missing_parent > 0: + logger.warning(f"Found {messages_missing_parent} messages that could not be linked to a conversation") + + # Log warning if messages are missing Assistant_ID + msg_has_assistant_id = sum(1 for msg in enriched_messages if msg.get("Assistant_ID")) + msg_no_assistant_id = sum(1 for msg in enriched_messages if not msg.get("Assistant_ID")) + + if msg_no_assistant_id > 0: + logger.warning(f"Messages missing Assistant_ID after enrichment: {msg_no_assistant_id} out of {len(enriched_messages)}") + + # Log completion information + logger.debug(f"Returning {len(processed_conversations)} processed conversations and {len(enriched_messages)} enriched messages") + + return jsonify({ + "conversations": processed_conversations, + "messages": enriched_messages + }) + +if __name__ == "__main__": + # Configure a simple access log for requests + @app.before_request + def log_request_info(): + if request.path != '/favicon.ico': # Skip favicon requests + logger.debug(f"Request: {request.method} {request.path}") + + # Configure after-request handler to log errors + @app.after_request + def log_response_info(response): + if response.status_code >= 400: + logger.warning(f"Response: {response.status_code}") + return response + + # Import Hypercorn components + from hypercorn.config import Config + from hypercorn.asyncio import serve + import asyncio + + # Configure Hypercorn + config = Config() + config.bind = ["0.0.0.0:5001"] # Port for backend + config.use_reloader = False + config.root_path = application_root + + # Run the application with Hypercorn + logger.warning("Starting Oliver Agency Reporting Backend with Hypercorn on port 5001") + asyncio.run(serve(app, config)) \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..aada20d --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,18 @@ +blinker==1.9.0 +certifi==2025.4.26 +cffi==1.17.1 +charset-normalizer==3.4.2 +click==8.2.0 +cryptography==45.0.4 +Flask==3.1.1 +flask-cors==6.0.0 +hypercorn==0.16.0 +idna==3.10 +itsdangerous==2.2.0 +Jinja2==3.1.6 +MarkupSafe==3.0.2 +pycparser==2.22 +PyJWT==2.10.1 +requests==2.32.3 +urllib3==2.4.0 +Werkzeug==3.1.3 diff --git a/email_analysis.py b/email_analysis.py new file mode 100644 index 0000000..1dcb3c9 --- /dev/null +++ b/email_analysis.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +""" +Email Analysis Script for BAIC Dashboard +Pulls data from Make.com endpoint and analyzes unique email addresses for user filtering logic +""" + +import requests +import json +import re +from collections import Counter + +MAKE_WEBHOOK_URL = 'https://hook.eu1.make.celonis.com/h8gjldwnp4u5cvc0io474zq4u7vwb9zw' + +def fetch_data(data_type): + """Fetch data from Make.com endpoint""" + try: + params = {'type': data_type} + response = requests.get(MAKE_WEBHOOK_URL, params=params, timeout=30) + response.raise_for_status() + data = response.json() + return [item.get('data', {}) for item in data] + except Exception as e: + print(f'Error fetching {data_type}: {e}') + return [] + +def extract_display_name(email): + """Extract display name from email address""" + if not email or '@' not in email: + return email + + # Handle oliver.agency emails (firstname+lastname format) + if email.endswith('@oliver.agency'): + username = email.split('@')[0] + # Split camelCase or handle names like 'michaelclervi' + parts = re.findall(r'[A-Z][a-z]*|[a-z]+', username) + if len(parts) >= 2: + return f"{parts[0].title()} {parts[1].title()}" + else: + return username.title() + + # Handle Barclays external emails (firstname.lastname_domain format) + elif email.endswith('@olivermarketing.onmicrosoft.com'): + username = email.split('@')[0] + # Remove the domain suffix like '_barclays.com#ext#' + clean_username = re.sub(r'_[^_]+\.(com|co\.uk)#ext#$', '', username) + parts = clean_username.split('.') + if len(parts) >= 2: + first_name = parts[0].replace('_', ' ').title() + last_name = parts[1].replace('_', ' ').title() + return f"{first_name} {last_name}" + else: + return clean_username.replace('_', ' ').title() + + return email + +def analyze_emails(): + """Main analysis function""" + print("Fetching data from Make.com endpoint...") + print("=" * 50) + + # Fetch data + conversations = fetch_data('conversations') + messages = fetch_data('messages') + + print(f"Found {len(conversations)} conversations") + print(f"Found {len(messages)} messages") + print() + + # Extract unique emails + emails = set() + + for conv in conversations: + user_id = conv.get('User_ID', '') + if user_id and '@' in user_id: + emails.add(user_id.lower()) + + for msg in messages: + user_id = msg.get('User_ID', '') + if user_id and '@' in user_id: + emails.add(user_id.lower()) + + print(f"UNIQUE EMAIL ADDRESSES ({len(emails)} total)") + print("=" * 50) + + # Create mapping for filtering logic + email_to_display = {} + domain_groups = {'oliver.agency': [], 'barclays': []} + + for email in sorted(emails): + display_name = extract_display_name(email) + email_to_display[email] = display_name + + if email.endswith('@oliver.agency'): + domain_groups['oliver.agency'].append((email, display_name)) + elif email.endswith('@olivermarketing.onmicrosoft.com'): + domain_groups['barclays'].append((email, display_name)) + + # Print organized results + print("OLIVER AGENCY USERS:") + print("-" * 30) + for email, name in domain_groups['oliver.agency']: + print(f"{name:<25} | {email}") + + print(f"\nBARCLAYS EXTERNAL USERS:") + print("-" * 30) + for email, name in domain_groups['barclays']: + print(f"{name:<25} | {email}") + + # Generate JavaScript mapping for frontend + print(f"\nJAVASCRIPT USER MAPPING:") + print("=" * 50) + print("// Add this to your frontend user filtering logic") + print("const userEmailToDisplayName = {") + for email, name in sorted(email_to_display.items()): + print(f' "{email}": "{name}",') + print("};") + + # Domain analysis + domains = Counter() + for email in emails: + if '@' in email: + domain = email.split('@')[1] + domains[domain] += 1 + + print(f"\nDOMAIN BREAKDOWN:") + print("=" * 50) + for domain, count in domains.most_common(): + print(f"{domain:<40} | {count:>3} users") + + print(f"\nSUMMARY:") + print("=" * 50) + print(f"Total unique users: {len(emails)}") + print(f"Oliver Agency staff: {len(domain_groups['oliver.agency'])}") + print(f"Barclays external users: {len(domain_groups['barclays'])}") + +if __name__ == "__main__": + analyze_emails() \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..7059a96 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,12 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/frontend/baic-logo.png b/frontend/baic-logo.png new file mode 100644 index 0000000..06a1ba9 Binary files /dev/null and b/frontend/baic-logo.png differ diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..ec2b712 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,33 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' + +export default [ + { ignores: ['dist'] }, + { + files: ['**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...reactHooks.configs.recommended.rules, + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..0c589ec --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..866a64c --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3499 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@azure/msal-browser": "^4.13.2", + "@azure/msal-react": "^3.0.13", + "axios": "^1.9.0", + "date-fns": "^4.1.0", + "papaparse": "^5.5.3", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "recharts": "^2.15.3" + }, + "devDependencies": { + "@eslint/js": "^9.25.0", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "eslint": "^9.25.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "vite": "^6.3.5" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.13.2.tgz", + "integrity": "sha512-lS75bF6FYZRwsacKLXc8UYu/jb+gOB7dtZq5938chCvV/zKTFDnzuXxCXhsSUh0p8s/P8ztgbfdueD9lFARQlQ==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.7.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.7.1.tgz", + "integrity": "sha512-a0eowoYfRfKZEjbiCoA5bPT3IlWRAdGSvi63OU23Hv+X6EI8gbvXCoeqokUceFMoT9NfRUWTJSx5FiuzruqT8g==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-react": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@azure/msal-react/-/msal-react-3.0.13.tgz", + "integrity": "sha512-g0jk1CXCWVyshSqdaFW0ES8qTXGHiQZlgfnkS+Oxa0eDgw3phdm8I2EChtFiK67OmoQr+HmYPRrg+s8NpvdeRQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@azure/msal-browser": "^4.13.2", + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", + "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", + "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helpers": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", + "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", + "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.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==", + "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.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", + "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.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/@eslint/plugin-kit": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.14.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.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.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz", + "integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz", + "integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz", + "integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz", + "integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz", + "integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz", + "integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz", + "integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz", + "integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz", + "integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz", + "integrity": "sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz", + "integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz", + "integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz", + "integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz", + "integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz", + "integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz", + "integrity": "sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.0.tgz", + "integrity": "sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz", + "integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz", + "integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz", + "integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz", + "integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz", + "integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", + "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.10", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@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" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "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": { + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "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", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.155", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", + "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" + } + }, + "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", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", + "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.27.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "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", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", + "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "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", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/papaparse": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", + "license": "MIT" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "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-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/recharts": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.3.tgz", + "integrity": "sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz", + "integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.41.0", + "@rollup/rollup-android-arm64": "4.41.0", + "@rollup/rollup-darwin-arm64": "4.41.0", + "@rollup/rollup-darwin-x64": "4.41.0", + "@rollup/rollup-freebsd-arm64": "4.41.0", + "@rollup/rollup-freebsd-x64": "4.41.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.41.0", + "@rollup/rollup-linux-arm-musleabihf": "4.41.0", + "@rollup/rollup-linux-arm64-gnu": "4.41.0", + "@rollup/rollup-linux-arm64-musl": "4.41.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.41.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.41.0", + "@rollup/rollup-linux-riscv64-gnu": "4.41.0", + "@rollup/rollup-linux-riscv64-musl": "4.41.0", + "@rollup/rollup-linux-s390x-gnu": "4.41.0", + "@rollup/rollup-linux-x64-gnu": "4.41.0", + "@rollup/rollup-linux-x64-musl": "4.41.0", + "@rollup/rollup-win32-arm64-msvc": "4.41.0", + "@rollup/rollup-win32-ia32-msvc": "4.41.0", + "@rollup/rollup-win32-x64-msvc": "4.41.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "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", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..b16db3d --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,33 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@azure/msal-browser": "^4.13.2", + "@azure/msal-react": "^3.0.13", + "axios": "^1.9.0", + "date-fns": "^4.1.0", + "papaparse": "^5.5.3", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "recharts": "^2.15.3" + }, + "devDependencies": { + "@eslint/js": "^9.25.0", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "eslint": "^9.25.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "vite": "^6.3.5" + } +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..b480eb7 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,110 @@ +.app-container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; +} + +header { + margin-bottom: 20px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.header-brand { + display: flex; + align-items: center; + gap: 12px; + flex-grow: 1; + padding: 0 0 10px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.3); +} + +.baic-logo { + height: 120px; + width: auto; +} + +header h1 { + color: #ffffff; + margin: 0; + font-size: 1.8rem; + font-weight: 600; +} + +.loading, +.error { + padding: 20px; + text-align: center; +} + +.loading { + color: #ffffff; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 4px; +} + +.error { + color: #e53935; + background-color: #ffebee; + border-radius: 4px; +} + +.login-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background-color: #1b2142; +} + +.login-card { + background: white; + padding: 40px; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 400px; + width: 100%; +} + +.login-card h2 { + color: #333; + margin-bottom: 10px; +} + +.login-card p { + color: #666; + margin-bottom: 30px; +} + +.login-button { + background-color: #1b2142; + color: white; + border: none; + padding: 12px 24px; + border-radius: 4px; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s; +} + +.login-button:hover { + background-color: #0052a3; +} + +.logout-button { + background-color: #dc3545; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + transition: background-color 0.3s; +} + +.logout-button:hover { + background-color: #c82333; +} \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..107106c --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,16 @@ +import { useIsAuthenticated } from '@azure/msal-react'; +import LoginComponent from './components/LoginComponent'; +import AuthenticatedApp from './components/AuthenticatedApp'; +import './App.css'; + +function App() { + const isAuthenticated = useIsAuthenticated(); + + if (!isAuthenticated) { + return ; + } + + return ; +} + +export default App; \ No newline at end of file diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/authConfig.js b/frontend/src/authConfig.js new file mode 100644 index 0000000..c7a7fdb --- /dev/null +++ b/frontend/src/authConfig.js @@ -0,0 +1,49 @@ +import { LogLevel } from '@azure/msal-browser'; + +export const msalConfig = { + auth: { + clientId: '014547f2-21c1-4245-881f-97f49543d963', + authority: 'https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385', + redirectUri: window.location.origin, + postLogoutRedirectUri: window.location.origin, + navigateToLoginRequestUrl: false + }, + cache: { + cacheLocation: 'sessionStorage', + storeAuthStateInCookie: false + }, + system: { + loggerOptions: { + loggerCallback: (level, message, containsPii) => { + if (containsPii) { + return; + } + switch (level) { + case LogLevel.Error: + console.error(message); + return; + case LogLevel.Info: + console.info(message); + return; + case LogLevel.Verbose: + console.debug(message); + return; + case LogLevel.Warning: + console.warn(message); + return; + default: + return; + } + } + } + } +}; + +export const loginRequest = { + scopes: ['User.Read'], + prompt: 'select_account' +}; + +export const graphConfig = { + graphMeEndpoint: 'https://graph.microsoft.com/v1.0/me' +}; \ No newline at end of file diff --git a/frontend/src/components/AuthenticatedApp.jsx b/frontend/src/components/AuthenticatedApp.jsx new file mode 100644 index 0000000..a955eac --- /dev/null +++ b/frontend/src/components/AuthenticatedApp.jsx @@ -0,0 +1,100 @@ +import { useState, useEffect } from 'react'; +import { useMsal } from '@azure/msal-react'; +import { fetchAllData } from '../services/api'; +import Dashboard from './Dashboard'; +import LogoutButton from './LogoutButton'; +import baicLogo from '../../baic-logo.png'; + +const AuthenticatedApp = () => { + const { instance } = useMsal(); + const [conversations, setConversations] = useState([]); + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [debugInfo, setDebugInfo] = useState([]); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setDebugInfo(prev => [...prev, `MSAL instance available: ${!!instance}`]); + console.error('DEBUG: AuthenticatedApp MSAL instance available:', !!instance); + console.error('DEBUG: AuthenticatedApp calling fetchAllData with instance'); + setDebugInfo(prev => [...prev, 'Calling fetchAllData...']); + const data = await fetchAllData(instance); + console.error("DEBUG: API response data received:", !!data); + setDebugInfo(prev => [...prev, `API response received: ${!!data}`]); + + // Check if we have valid data structure + if (!data || typeof data !== 'object') { + throw new Error('Invalid data format received from API'); + } + + // Set conversations and messages, providing empty arrays as fallbacks + const receivedConversations = data.conversations || []; + const receivedMessages = data.messages || []; + + console.log(`Received ${receivedConversations.length} conversations and ${receivedMessages.length} messages`); + + // Log some sample data to verify format + if (receivedConversations.length > 0) { + console.log("Sample conversation:", receivedConversations[0]); + } + + if (receivedMessages.length > 0) { + console.log("Sample message:", receivedMessages[0]); + } + + setConversations(receivedConversations); + setMessages(receivedMessages); + setError(null); + } catch (err) { + console.error('Error fetching data:', err); + setDebugInfo(prev => [...prev, `ERROR: ${err.message}`]); + setError('Failed to fetch data. Please try again later.'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [instance]); + + return ( +
+
+
+ BAIC Logo +

Reporting Module

+
+ +
+
+ {loading ? ( +
+ Loading data... +
+ {debugInfo.map((info, index) => ( +
{info}
+ ))} +
+
+ ) : error ? ( +
+ {error} +
+ Debug Info: + {debugInfo.map((info, index) => ( +
{info}
+ ))} +
+
+ ) : ( + + )} +
+
+ ); +}; + +export default AuthenticatedApp; \ No newline at end of file diff --git a/frontend/src/components/Dashboard.jsx b/frontend/src/components/Dashboard.jsx new file mode 100644 index 0000000..c4560b9 --- /dev/null +++ b/frontend/src/components/Dashboard.jsx @@ -0,0 +1,280 @@ +import { useState, useEffect } from 'react'; +import FilterPanel from './FilterPanel'; +import DatePickerComponent from './DatePickerComponent'; +import VolumeGraph from './VolumeGraph'; +import ExportButton from './ExportButton'; +import { parseISO, isWithinInterval, isValid } from 'date-fns'; +import { getAssistantDisplayName, hasAssistantFriendlyName } from '../services/assistantMapping'; +import { processUserData } from '../services/userMapping'; + +const Dashboard = ({ conversations, messages }) => { + console.log(`Dashboard received ${conversations.length} conversations and ${messages.length} messages`); + + // Initialize filter state + const [filters, setFilters] = useState({ + organization: 'All Users', + user: 'All Users', + assistant: 'All Assistants', + brandTOV: 'All Brands/TOV', + dateRange: { + start: null, + end: null + } + }); + + // Extract unique filter options + const [processedUserData, setProcessedUserData] = useState({}); + const [uniqueAssistants, setUniqueAssistants] = useState([]); + const [uniqueBrandTOVs, setUniqueBrandTOVs] = useState([]); + + // Filtered data based on current filters + const [filteredConversations, setFilteredConversations] = useState([]); + const [filteredMessages, setFilteredMessages] = useState([]); + + // Extract unique filter values from conversations + useEffect(() => { + if (conversations.length > 0) { + const users = [...new Set(conversations.map(conv => conv.User_ID))].filter(Boolean); + + // Process user data for organization filtering and friendly names + const userData = processUserData(users); + + // Get unique assistant IDs and filter to only those with friendly names + const assistantIds = [...new Set(conversations.map(conv => conv.Assistant_ID))] + .filter(Boolean) + .filter(id => hasAssistantFriendlyName(id)); + + // Create a map of display names to IDs so we can use display names for filtering UI + // but keep the original IDs for filtering logic + const assistantDisplayMap = {}; + const assistantDisplayNames = assistantIds.map(id => { + const displayName = getAssistantDisplayName(id); + if (displayName) { + assistantDisplayMap[displayName] = id; + return displayName; + } + return null; + }).filter(Boolean); + + const brandTOVs = [...new Set(conversations.map(conv => conv['Brand Voice Setting']))].filter(Boolean); + + console.log("Processed user data:", userData); + console.log("Unique assistants with friendly names:", assistantDisplayNames); + console.log("Assistant ID mapping:", assistantDisplayMap); + console.log("Unique brands/TOV:", brandTOVs); + + setProcessedUserData(userData); + // Create reverse mapping (from name to ID) and forward mapping (from ID to name) + window.assistantDisplayToIdMap = assistantDisplayMap; // For display name to ID + window.assistantIdToDisplayMap = {}; // For ID to display name + + // Populate the reverse mapping for easier lookups + Object.entries(assistantDisplayMap).forEach(([displayName, id]) => { + window.assistantIdToDisplayMap[id] = displayName; + }); + + // Also store the original assistant IDs (for data access) and display names (for UI) + window.validAssistantIds = assistantIds; + window.assistantDisplayNames = assistantDisplayNames; + + setUniqueAssistants(assistantIds); // Pass the IDs to VolumeGraph + setUniqueBrandTOVs(brandTOVs); + } + }, [conversations]); + + // Apply filters to data + useEffect(() => { + console.log("Applying filters:", filters); + + // Filter conversations + const filteredConvs = conversations.filter(conv => { + // First, filter out assistants without friendly names + if (!conv.Assistant_ID || !window.validAssistantIds.includes(conv.Assistant_ID)) { + return false; + } + + let matches = true; + + // Date range filter - only apply if both start and end dates are set + if (filters.dateRange.start && filters.dateRange.end && conv.StartTime) { + try { + const convDate = parseISO(conv.StartTime); + if (isValid(convDate)) { + matches = matches && isWithinInterval(convDate, { + start: filters.dateRange.start, + end: filters.dateRange.end + }); + } else { + console.warn("Invalid date in conversation:", conv.StartTime); + matches = false; + } + } catch (e) { + console.error("Error parsing conversation date:", e); + matches = false; + } + } + + // Organization filter - check if user belongs to selected organization + if (filters.organization !== 'All Users' && conv.User_ID) { + const userOrg = processedUserData.organizationMap?.[conv.User_ID]; + if (filters.organization === 'Oliver Users') { + matches = matches && userOrg === 'Oliver'; + } else if (filters.organization === 'Barclays Users') { + matches = matches && userOrg === 'Barclays'; + } + } + + // User filter + if (filters.user !== 'All Users' && conv.User_ID) { + matches = matches && conv.User_ID === filters.user; + } + + // Assistant filter + if (filters.assistant !== 'All Assistants' && conv.Assistant_ID) { + // Use the ID mapping to compare IDs rather than display names + const selectedAssistantId = window.assistantDisplayToIdMap[filters.assistant]; + matches = matches && conv.Assistant_ID === selectedAssistantId; + } + + // Brand/TOV filter + if (filters.brandTOV !== 'All Brands/TOV' && conv['Brand Voice Setting']) { + matches = matches && conv['Brand Voice Setting'] === filters.brandTOV; + } + + return matches; + }); + + // Filter messages (use enriched message data) + const filteredMsgs = messages.filter(msg => { + // First, filter out assistants without friendly names + if (!msg.Assistant_ID || !window.validAssistantIds.includes(msg.Assistant_ID)) { + return false; + } + + let matches = true; + + // Date range filter - only apply if both start and end dates are set + if (filters.dateRange.start && filters.dateRange.end && msg.Timestamp) { + try { + const msgDate = parseISO(msg.Timestamp); + if (isValid(msgDate)) { + matches = matches && isWithinInterval(msgDate, { + start: filters.dateRange.start, + end: filters.dateRange.end + }); + } else { + console.warn("Invalid date in message:", msg.Timestamp); + matches = false; + } + } catch (e) { + console.error("Error parsing message date:", e); + matches = false; + } + } + + // Organization filter - check if user belongs to selected organization + if (filters.organization !== 'All Users' && msg.User_ID) { + const userOrg = processedUserData.organizationMap?.[msg.User_ID]; + if (filters.organization === 'Oliver Users') { + matches = matches && userOrg === 'Oliver'; + } else if (filters.organization === 'Barclays Users') { + matches = matches && userOrg === 'Barclays'; + } + } + + // User filter + if (filters.user !== 'All Users' && msg.User_ID) { + matches = matches && msg.User_ID === filters.user; + } + + // Assistant filter + if (filters.assistant !== 'All Assistants' && msg.Assistant_ID) { + // Use the ID mapping to compare IDs rather than display names + const selectedAssistantId = window.assistantDisplayToIdMap[filters.assistant]; + matches = matches && msg.Assistant_ID === selectedAssistantId; + } + + // Brand/TOV filter + if (filters.brandTOV !== 'All Brands/TOV' && msg.Brand_Voice_Setting) { + matches = matches && msg.Brand_Voice_Setting === filters.brandTOV; + } + + return matches; + }); + + console.log(`Filtered to ${filteredConvs.length} conversations and ${filteredMsgs.length} messages`); + + setFilteredConversations(filteredConvs); + setFilteredMessages(filteredMsgs); + }, [conversations, messages, filters]); + + // Handle filter changes + const handleFilterChange = (filterType, value) => { + console.log(`Filter changed: ${filterType} = ${value}`); + + // If organization filter changes, reset user filter to avoid inconsistent state + if (filterType === 'organization') { + setFilters(prevFilters => ({ + ...prevFilters, + organization: value, + user: 'All Users' // Reset user filter when organization changes + })); + } else { + setFilters(prevFilters => ({ + ...prevFilters, + [filterType]: value + })); + } + }; + + // Handle date range changes + const handleDateRangeChange = (start, end) => { + console.log(`Date range changed: start = ${start}, end = ${end}`); + setFilters(prevFilters => ({ + ...prevFilters, + dateRange: { start, end } + })); + }; + + return ( +
+
+ + + +
+ +
+

Key Metrics Definitions

+
+ Conversation: A single, complete user interaction session from start to finish. +
+
+ Message: Each individual back-and-forth exchange (user prompt and AI response) within a conversation. +
+
+
+ ); +}; + +export default Dashboard; \ No newline at end of file diff --git a/frontend/src/components/DatePickerComponent.jsx b/frontend/src/components/DatePickerComponent.jsx new file mode 100644 index 0000000..7fbad42 --- /dev/null +++ b/frontend/src/components/DatePickerComponent.jsx @@ -0,0 +1,123 @@ +import React, { useEffect } from 'react'; +import { startOfDay, endOfDay, subDays, startOfWeek, endOfWeek } from 'date-fns'; + +const DatePickerComponent = ({ dateRange, onDateRangeChange }) => { + // Initialize with "All time" selected + useEffect(() => { + // Only set to "All time" initially if date range is not already set + if (!dateRange.start && !dateRange.end) { + handlePresetClick('all'); + } + }, []); + + // Preset date ranges + const handlePresetClick = (preset) => { + const now = new Date(); + let start, end; + + switch (preset) { + case 'all': + // All time - set to null to indicate no date filtering + start = null; + end = null; + break; + case 'today': + start = startOfDay(now); + end = endOfDay(now); + break; + case 'week': + start = startOfDay(subDays(now, 7)); + end = endOfDay(now); + break; + case '30days': + start = startOfDay(subDays(now, 30)); + end = endOfDay(now); + break; + default: + // Default to no date filtering + start = null; + end = null; + } + + onDateRangeChange(start, end); + }; + + // Custom date range + const handleCustomDateChange = (type, e) => { + const date = e.target.value ? new Date(e.target.value) : null; + + if (type === 'start') { + onDateRangeChange(date, dateRange.end); + } else { + onDateRangeChange(dateRange.start, date ? endOfDay(date) : null); + } + }; + + // Format date for input field + const formatDateForInput = (date) => { + if (!date) return ''; + return date.toISOString().split('T')[0]; // Format as YYYY-MM-DD + }; + + return ( +
+
+ + + + +
+ +
+
+ + handleCustomDateChange('start', e)} + /> +
+ +
+ + handleCustomDateChange('end', e)} + min={formatDateForInput(dateRange.start)} + /> +
+
+
+ ); +}; + +export default DatePickerComponent; \ No newline at end of file diff --git a/frontend/src/components/ExportButton.jsx b/frontend/src/components/ExportButton.jsx new file mode 100644 index 0000000..4eb8840 --- /dev/null +++ b/frontend/src/components/ExportButton.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { unparse } from 'papaparse'; +import { generateAggregatedData } from '../utils/dataAggregation'; + +const ExportButton = ({ filteredConversations, filteredMessages, filters, uniqueAssistants }) => { + const handleExport = () => { + // Generate aggregated statistical data instead of raw conversation content + const { exportData } = generateAggregatedData( + filteredConversations, + filteredMessages, + uniqueAssistants, + filters + ); + + // If no data to export, show message + if (!exportData || exportData.length === 0) { + alert('No data available for export with the current filters.'); + return; + } + + // Create CSV from aggregated data + const csv = unparse(exportData); + + // Create a blob from the CSV data + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + + // Create a download link + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', 'oliver_agency_aggregated_report.csv'); + document.body.appendChild(link); + + // Trigger download + link.click(); + + // Clean up + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + }; + + return ( +
+ +
+ ); +}; + +export default ExportButton; \ No newline at end of file diff --git a/frontend/src/components/FilterPanel.jsx b/frontend/src/components/FilterPanel.jsx new file mode 100644 index 0000000..0959bfd --- /dev/null +++ b/frontend/src/components/FilterPanel.jsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { getFilteredUsers } from '../services/userMapping'; +import { getBrandDisplayName } from '../utils/brandMapping'; + +const FilterPanel = ({ + processedUserData, + uniqueAssistants, + uniqueBrandTOVs, + filters, + onFilterChange +}) => { + // Get filtered users based on organization selection + const filteredUsers = getFilteredUsers(processedUserData, filters.organization); + + return ( +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ ); +}; + +export default FilterPanel; \ No newline at end of file diff --git a/frontend/src/components/LoginComponent.jsx b/frontend/src/components/LoginComponent.jsx new file mode 100644 index 0000000..c4f7a3a --- /dev/null +++ b/frontend/src/components/LoginComponent.jsx @@ -0,0 +1,27 @@ +import { useMsal } from '@azure/msal-react'; +import { loginRequest } from '../authConfig'; + +const LoginComponent = () => { + const { instance } = useMsal(); + + const handleLogin = () => { + instance.loginPopup(loginRequest).catch(e => { + console.error(e); + }); + }; + + + return ( +
+
+

Oliver Agency Reporting Module

+

Please sign in to access the dashboard

+ +
+
+ ); +}; + +export default LoginComponent; \ No newline at end of file diff --git a/frontend/src/components/LogoutButton.jsx b/frontend/src/components/LogoutButton.jsx new file mode 100644 index 0000000..e591bfb --- /dev/null +++ b/frontend/src/components/LogoutButton.jsx @@ -0,0 +1,19 @@ +import { useMsal } from '@azure/msal-react'; + +const LogoutButton = () => { + const { instance } = useMsal(); + + const handleLogout = () => { + instance.logoutPopup().catch(e => { + console.error(e); + }); + }; + + return ( + + ); +}; + +export default LogoutButton; \ No newline at end of file diff --git a/frontend/src/components/VolumeGraph.jsx b/frontend/src/components/VolumeGraph.jsx new file mode 100644 index 0000000..34839a9 --- /dev/null +++ b/frontend/src/components/VolumeGraph.jsx @@ -0,0 +1,457 @@ +import React, { useMemo, useEffect, useState } from 'react'; +import { + BarChart, Bar, + XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer +} from 'recharts'; +import { parseISO, format, startOfDay, differenceInDays, isValid, addDays } from 'date-fns'; +import { getAssistantDisplayName, hasAssistantFriendlyName } from '../services/assistantMapping'; + +const VolumeGraph = ({ + filteredConversations, + filteredMessages, + selectedAssistant, + uniqueAssistants, // This is now an array of assistant IDs + assistantIdToNameMap // Mapping from ID to display name +}) => { + // Add state for the data type toggle + const [dataType, setDataType] = useState('both'); // 'both', 'conversations', or 'messages' + + // useEffect for debugging (keep this for now) + useEffect(() => { + console.log("VolumeGraph PROPS:", { + conversationsCount: filteredConversations.length, + messagesCount: filteredMessages.length, + selectedAssistant, + uniqueAssistants: uniqueAssistants, // Log the actual array of IDs + uniqueAssistantsCount: uniqueAssistants ? uniqueAssistants.length : 0, + validAssistantIds: window.validAssistantIds || [], + idToNameMap: assistantIdToNameMap + }); + + console.log("Display names in window:", window.assistantDisplayNames); + console.log("ID to display mapping:", window.assistantIdToDisplayMap); + console.log("Display to ID mapping:", window.assistantDisplayToIdMap); + + if (filteredConversations.length > 0) { + const sampleConv = filteredConversations[0]; + console.log("VolumeGraph Sample CONV:", sampleConv, + "Has Assistant_ID:", sampleConv.Assistant_ID, + "Has friendly name:", hasAssistantFriendlyName(sampleConv.Assistant_ID), + "Display name:", getAssistantDisplayName(sampleConv.Assistant_ID)); + } else { + console.warn("No filtered conversations available for graph!"); + } + + if (filteredMessages.length > 0) { + const sampleMsg = filteredMessages[0]; + console.log("VolumeGraph Sample MSG:", sampleMsg, + "Has Assistant_ID:", sampleMsg.Assistant_ID, + "Has friendly name:", hasAssistantFriendlyName(sampleMsg.Assistant_ID), + "Display name:", getAssistantDisplayName(sampleMsg.Assistant_ID)); + } else { + console.warn("No filtered messages available for graph!"); + } + }, [filteredConversations, filteredMessages, selectedAssistant, uniqueAssistants, assistantIdToNameMap]); + + const graphData = useMemo(() => { + if (!filteredConversations.length && !filteredMessages.length) { + return { dailyData: [], aggregatedData: [], useAggregated: false, aggregationType: 'daily' }; + } + + const allTimestamps = [ + ...filteredConversations.map(conv => conv.StartTime), + ...filteredMessages.map(msg => msg.Timestamp) + ].filter(Boolean); + + const validDates = allTimestamps + .map(dateStr => { + try { return parseISO(dateStr); } catch (e) { return null; } + }) + .filter(dateObj => dateObj && isValid(dateObj)); + + if (validDates.length === 0) { + return { dailyData: [], aggregatedData: [], useAggregated: false, aggregationType: 'daily' }; + } + + const minDate = startOfDay(new Date(Math.min(...validDates))); + const maxDate = startOfDay(new Date(Math.max(...validDates))); + const daysDiff = differenceInDays(maxDate, minDate) + 1; + + const dataByDate = {}; + const monthlyData = {}; + const weeklyData = {}; + + // Initialize data structures + for (let i = 0; i < daysDiff; i++) { + const currentDate = addDays(minDate, i); + const dateKey = format(currentDate, 'yyyy-MM-dd'); + const monthKey = format(currentDate, 'yyyy-MM'); + + const dayOffsetFromMin = differenceInDays(currentDate, minDate); + const weekNumber = Math.floor(dayOffsetFromMin / 7) + 1; // 1-based week number + const weekKey = `Week ${weekNumber}`; // Simpler week key for direct use + + // Daily + dataByDate[dateKey] = { date: dateKey, conversationsTotal: 0, messagesTotal: 0 }; + // Always initialize counters for all valid assistants + if (uniqueAssistants && uniqueAssistants.length > 0) { + uniqueAssistants.forEach(assistantId => { + dataByDate[dateKey][`conversations_${assistantId}`] = 0; + dataByDate[dateKey][`messages_${assistantId}`] = 0; + }); + } + + // Monthly + if (!monthlyData[monthKey]) { + monthlyData[monthKey] = { month: monthKey, conversationsTotal: 0, messagesTotal: 0 }; + // Always initialize counters for all valid assistants + if (uniqueAssistants && uniqueAssistants.length > 0) { + uniqueAssistants.forEach(assistantId => { + monthlyData[monthKey][`conversations_${assistantId}`] = 0; + monthlyData[monthKey][`messages_${assistantId}`] = 0; + }); + } + } + + // Weekly + if (!weeklyData[weekKey]) { + const weekStartDate = addDays(minDate, (weekNumber - 1) * 7); + weeklyData[weekKey] = { + week: weekKey, + startDateFormatted: format(weekStartDate, 'MMM d, yyyy'), // For tooltip + conversationsTotal: 0, + messagesTotal: 0 + }; + // Always initialize counters for all valid assistants + if (uniqueAssistants && uniqueAssistants.length > 0) { + uniqueAssistants.forEach(assistantId => { + weeklyData[weekKey][`conversations_${assistantId}`] = 0; + weeklyData[weekKey][`messages_${assistantId}`] = 0; + }); + } + } + } + + // Aggregate conversation counts + filteredConversations.forEach(conv => { + if (conv.StartTime) { + const parsedDate = parseISO(conv.StartTime); + if (isValid(parsedDate)) { + const dateStr = format(parsedDate, 'yyyy-MM-dd'); + const monthStr = format(parsedDate, 'yyyy-MM'); + const dayOffsetFromMinAgg = differenceInDays(parsedDate, minDate); + const weekNumAgg = Math.floor(dayOffsetFromMinAgg / 7) + 1; + const weekKeyAgg = `Week ${weekNumAgg}`; + + if (dataByDate[dateStr]) { + dataByDate[dateStr].conversationsTotal++; + // Always increment per-assistant counters if the key exists + if (conv.Assistant_ID && dataByDate[dateStr][`conversations_${conv.Assistant_ID}`] !== undefined) { + dataByDate[dateStr][`conversations_${conv.Assistant_ID}`]++; + } + } + if (monthlyData[monthStr]) { + monthlyData[monthStr].conversationsTotal++; + // Always increment per-assistant counters if the key exists + if (conv.Assistant_ID && monthlyData[monthStr][`conversations_${conv.Assistant_ID}`] !== undefined) { + monthlyData[monthStr][`conversations_${conv.Assistant_ID}`]++; + } + } + if (weeklyData[weekKeyAgg]) { + weeklyData[weekKeyAgg].conversationsTotal++; + // Always increment per-assistant counters if the key exists + if (conv.Assistant_ID && weeklyData[weekKeyAgg][`conversations_${conv.Assistant_ID}`] !== undefined) { + weeklyData[weekKeyAgg][`conversations_${conv.Assistant_ID}`]++; + } + } + } + } + }); + + // Aggregate message counts + filteredMessages.forEach(msg => { + if (msg.Timestamp) { + const parsedDate = parseISO(msg.Timestamp); + if (isValid(parsedDate)) { + const dateStr = format(parsedDate, 'yyyy-MM-dd'); + const monthStr = format(parsedDate, 'yyyy-MM'); + const dayOffsetFromMinAgg = differenceInDays(parsedDate, minDate); + const weekNumAgg = Math.floor(dayOffsetFromMinAgg / 7) + 1; + const weekKeyAgg = `Week ${weekNumAgg}`; + + if (dataByDate[dateStr]) { + dataByDate[dateStr].messagesTotal++; + // Always increment per-assistant counters if the key exists + if (msg.Assistant_ID && dataByDate[dateStr][`messages_${msg.Assistant_ID}`] !== undefined) { + dataByDate[dateStr][`messages_${msg.Assistant_ID}`]++; + } + } + if (monthlyData[monthStr]) { + monthlyData[monthStr].messagesTotal++; + // Always increment per-assistant counters if the key exists + if (msg.Assistant_ID && monthlyData[monthStr][`messages_${msg.Assistant_ID}`] !== undefined) { + monthlyData[monthStr][`messages_${msg.Assistant_ID}`]++; + } + } + if (weeklyData[weekKeyAgg]) { + weeklyData[weekKeyAgg].messagesTotal++; + // Always increment per-assistant counters if the key exists + if (msg.Assistant_ID && weeklyData[weekKeyAgg][`messages_${msg.Assistant_ID}`] !== undefined) { + weeklyData[weekKeyAgg][`messages_${msg.Assistant_ID}`]++; + } + } + } + } + }); + + const dailyDataArray = Object.values(dataByDate).sort((a, b) => a.date.localeCompare(b.date)); + const monthlyDataArray = Object.values(monthlyData).sort((a, b) => a.month.localeCompare(b.month)); + const weeklyDataArray = Object.values(weeklyData).sort((a,b) => { + const weekNumA = parseInt(a.week.split(' ')[1]); + const weekNumB = parseInt(b.week.split(' ')[1]); + return weekNumA - weekNumB; + }); + + const useWeekly = daysDiff > 60 && daysDiff <= 180; + const useMonthly = daysDiff > 180; + + let finalAggregatedData = dailyDataArray; + let currentAggregationType = 'daily'; + if (useMonthly) { + finalAggregatedData = monthlyDataArray; + currentAggregationType = 'monthly'; + } else if (useWeekly) { + finalAggregatedData = weeklyDataArray; + currentAggregationType = 'weekly'; + } + + // Log final data for inspection + console.log("VolumeGraph FINAL DATA:", { + dailyDataArray, + monthlyDataArray, + weeklyDataArray, + finalAggregatedData, + useAggregated: useMonthly || useWeekly, + aggregationType: currentAggregationType + }); + + if (finalAggregatedData.length > 0) { + console.log("VolumeGraph FINAL DATA Sample Point:", finalAggregatedData[0]); + + // Check for assistant-specific data + const samplePoint = finalAggregatedData[0]; + const assistantKeys = Object.keys(samplePoint).filter(key => + key.startsWith('conversations_') || key.startsWith('messages_') + ); + + console.log("Assistant-specific data keys:", assistantKeys); + + // Check if we have data for our valid assistant IDs + if (uniqueAssistants && uniqueAssistants.length > 0) { + uniqueAssistants.forEach(id => { + const convKey = `conversations_${id}`; + const msgKey = `messages_${id}`; + console.log(`Data for ${id} (${assistantIdToNameMap[id] || 'Unknown'}):`, { + hasConvKey: samplePoint.hasOwnProperty(convKey), + hasMsgKey: samplePoint.hasOwnProperty(msgKey), + convValue: samplePoint[convKey], + msgValue: samplePoint[msgKey] + }); + }); + } + } + + // Debug: Count non-zero values + const nonZeroDays = finalAggregatedData.filter(day => + day.conversationsTotal > 0 || day.messagesTotal > 0 || + Object.keys(day).some(key => key.startsWith('conversations_') && day[key] > 0) || + Object.keys(day).some(key => key.startsWith('messages_') && day[key] > 0) + ); + console.log("Days with non-zero values:", nonZeroDays.length); + if (nonZeroDays.length > 0) { + console.log("Sample non-zero day:", nonZeroDays[0]); + } + + return { + dataForChart: finalAggregatedData, + useAggregated: useMonthly || useWeekly, + aggregationType: currentAggregationType + }; + }, [filteredConversations, filteredMessages, selectedAssistant, uniqueAssistants]); + + const getAssistantColor = (assistantId, isConversation) => { + const defaultConvColor = '#4a4a4a'; + const defaultMsgColor = '#8a8a8a'; + + if (!uniqueAssistants || uniqueAssistants.length === 0) { + return isConversation ? defaultConvColor : defaultMsgColor; + } + const assistantIndex = uniqueAssistants.indexOf(assistantId); + const baseColors = ['#8884d8', '#82ca9d', '#ffc658', '#ff7300', '#a4de6c', '#d0ed57', '#00C49F', '#FFBB28', '#FF8042', '#0088FE', '#00C49F', '#FFBB28', '#FF8042']; + let color; + + if (assistantIndex === -1) { // Should not happen if uniqueAssistants is correctly populated + color = baseColors[Math.floor(Math.random() * baseColors.length)]; // Random for unknown + } else { + color = baseColors[assistantIndex % baseColors.length]; + } + return isConversation ? color : `${color}CC`; // CC for ~80% alpha + }; + + const formatXAxisTick = (tick) => { + try { + if (graphData.aggregationType === 'monthly') return format(parseISO(tick + '-01'), 'MMM yyyy'); + if (graphData.aggregationType === 'weekly') return tick; // 'Week X' + return format(parseISO(tick), 'MMM d'); + } catch (e) { return tick; } + }; + + const formatTooltipLabel = (label) => { + try { + if (graphData.aggregationType === 'monthly') return format(parseISO(label + '-01'), 'MMMM yyyy'); + if (graphData.aggregationType === 'weekly') { + const weekObj = graphData.dataForChart.find(w => w.week === label); + return weekObj ? `${label} (starts ${weekObj.startDateFormatted})` : label; + } + return format(parseISO(label), 'MMMM d, yyyy'); + } catch (e) { return label; } + }; + + const renderLinesOrBars = () => { + const showPerAssistant = selectedAssistant === 'All Assistants' && uniqueAssistants && uniqueAssistants.length > 0; + const elements = []; + + if (showPerAssistant) { + // For each assistant, add relevant lines/bars based on data type + uniqueAssistants.forEach(assistantId => { + // Get the display name for this assistant ID + const displayName = assistantIdToNameMap[assistantId] || assistantId.slice(0, 7); + + // Add conversation data if we're showing conversations or both + if (dataType === 'both' || dataType === 'conversations') { + elements.push( + + ); + } + + // Add message data if we're showing messages or both + if (dataType === 'both' || dataType === 'messages') { + elements.push( + + ); + } + }); + } else { // Totals or specific assistant + // For a specific assistant, we need to handle if it's a display name or ID + let displayName, assistantId; + + if (selectedAssistant !== 'All Assistants') { + // Check if this is a display name or an ID + assistantId = window.assistantDisplayToIdMap[selectedAssistant]; + + if (assistantId) { + // It's a display name + displayName = selectedAssistant; + } else { + // It might be an ID + displayName = assistantIdToNameMap[selectedAssistant] || selectedAssistant.slice(0, 7); + assistantId = selectedAssistant; + } + } else { + displayName = "All Assistants"; + } + + const convName = selectedAssistant !== 'All Assistants' ? `Conversations (${displayName})` : "Total Conversations"; + const msgName = selectedAssistant !== 'All Assistants' ? `Messages (${displayName})` : "Total Messages"; + + // Add conversation data if we're showing conversations or both + if (dataType === 'both' || dataType === 'conversations') { + elements.push( + + ); + } + + // Add message data if we're showing messages or both + if (dataType === 'both' || dataType === 'messages') { + elements.push( + + ); + } + } + + return elements; + }; + + if (!graphData || !graphData.dataForChart || graphData.dataForChart.length === 0) { + return
No data available for the selected filters.
; + } + + return ( +
+
+

+ {dataType === 'both' ? 'Conversation & Message Volume' : + dataType === 'conversations' ? 'Conversation Volume' : + 'Message Volume'} + {graphData.useAggregated ? ` (${graphData.aggregationType})` : ''} +

+
+ + + +
+
+
+ + + + + + [value, name.replace(/_/g, ' ')]} /> + + {renderLinesOrBars()} + + +
+
+ ); +}; + +export default VolumeGraph; \ No newline at end of file diff --git a/frontend/src/data/userDisplayNames.js b/frontend/src/data/userDisplayNames.js new file mode 100644 index 0000000..4153098 --- /dev/null +++ b/frontend/src/data/userDisplayNames.js @@ -0,0 +1,51 @@ +/** + * Manual mapping of email addresses to display names + * Generated from actual Make.com data analysis + * Used as fallback when automatic name parsing fails + */ +export const userDisplayNameMap = { + // Oliver Agency Users + "armanpreetkapoor@oliver.agency": "Armanpreet Kapoor", + "daveporter@oliver.agency": "Dave Porter", + "davidbrown@oliver.agency": "David Brown", + "helenfendall@oliver.agency": "Helen Fendall", + "jasonliew@oliver.agency": "Jason Liew", + "jeremycrockerwhite@oliver.agency": "Jeremy Crocker White", + "jeromejefferson@oliver.agency": "Jerome Jefferson", + "jessicakim@oliver.agency": "Jessica Kim", + "lisahall@oliver.agency": "Lisa Hall", + "lloydredding@oliver.agency": "Lloyd Redding", + "melaniecarr@oliver.agency": "Melanie Carr", + "michaelbarratt@oliver.agency": "Michael Barratt", + "michaelclervi@oliver.agency": "Michael Clervi", + "mikekeenleyside@oliver.agency": "Mike Keenleyside", + "nataliefryatt@oliver.agency": "Natalie Fryatt", + "nigelwebb@oliver.agency": "Nigel Webb", + "richardharman@oliver.agency": "Richard Harman", + "richardmakepeace@oliver.agency": "Richard Makepeace", + "robchoi@oliver.agency": "Rob Choi", + "robkavanagh@oliver.agency": "Rob Kavanagh", + "samiraali@oliver.agency": "Samira Ali", + "stevehedge@oliver.agency": "Steve Hedge", + "steveodonoghue@oliver.agency": "Steve O'Donoghue", + "tomwilson@oliver.agency": "Tom Wilson", + + // Barclays External Users (auto-parsed, but included for completeness) + "adam.webb_barclaycard.co.uk#ext#@olivermarketing.onmicrosoft.com": "Adam Webb", + "aileen.stevenson_barclays.com#ext#@olivermarketing.onmicrosoft.com": "Aileen Stevenson", + "alistair.taylor_barclays.com#ext#@olivermarketing.onmicrosoft.com": "Alistair Taylor", + "angharad.slater_barclaycard.co.uk#ext#@olivermarketing.onmicrosoft.com": "Angharad Slater", + "audrey.zahlis_barclays.com#ext#@olivermarketing.onmicrosoft.com": "Audrey Zahlis", + "christopher.x.perkins_barclays.com#ext#@olivermarketing.onmicrosoft.com": "Christopher X Perkins", + "donna.hill_barclays.com#ext#@olivermarketing.onmicrosoft.com": "Donna Hill", + "edward.simons1_barclays.com#ext#@olivermarketing.onmicrosoft.com": "Edward Simons", + "evie.grant_barclays.com#ext#@olivermarketing.onmicrosoft.com": "Evie Grant", + "gurjit.dhaliwal_barclays.com#ext#@olivermarketing.onmicrosoft.com": "Gurjit Dhaliwal", + "joseph.collins_barclays.com#ext#@olivermarketing.onmicrosoft.com": "Joseph Collins", + "madeleine.webb_barclays.com#ext#@olivermarketing.onmicrosoft.com": "Madeleine Webb", + "matt.perry_barclays.com#ext#@olivermarketing.onmicrosoft.com": "Matt Perry", + "rachael.millard_barclays.com#ext#@olivermarketing.onmicrosoft.com": "Rachael Millard", + "rhiannon.bodman_barclays.com#ext#@olivermarketing.onmicrosoft.com": "Rhiannon Bodman", + "samantha.barnes_barclays.com#ext#@olivermarketing.onmicrosoft.com": "Samantha Barnes", + "zara.myers_barclays.com#ext#@olivermarketing.onmicrosoft.com": "Zara Myers" +}; \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..a816ffa --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,269 @@ +:root { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + line-height: 1.5; + font-weight: 400; + color: #ffffff; + background-color: #1b2142; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; +} + +button { + border-radius: 4px; + border: 1px solid #d1d5db; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #ffffff; + color: #213547; + cursor: pointer; + transition: background-color 0.25s, border-color 0.25s; +} + +button:hover { + background-color: #f1f5f9; + border-color: #9ca3af; +} + +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +button.active { + background-color: #0052a3; + color: white; + border-color: #003d7a; +} + +select { + padding: 0.5em; + border-radius: 4px; + border: 1px solid #d1d5db; + font-size: 1em; + background-color: white; +} + +.dashboard { + background-color: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + padding: 1rem; + margin: 1rem; +} + +.controls-row { + display: flex; + flex-wrap: wrap; + gap: 1rem; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid #e5e7eb; +} + +.filter-panel { + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +.filter-group { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.filter-group label { + font-size: 0.875rem; + font-weight: 500; + color: #4b5563; +} + +.date-picker { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-left: auto; +} + +.date-presets { + display: flex; + gap: 0.5rem; +} + +.custom-date-range { + display: flex; + gap: 1rem; + margin-top: 0.5rem; +} + +.date-input { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.date-input label { + font-size: 0.875rem; + font-weight: 500; + color: #4b5563; +} + +.export-button-container { + align-self: flex-end; +} + +.export-button { + background-color: #10b981; + color: white; + border-color: #059669; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +.export-button:hover { + background-color: #059669; +} + +.export-button:disabled { + background-color: #d1d5db; + border-color: #9ca3af; + cursor: not-allowed; + color: #4b5563; +} + +.volume-graph { + padding: 1rem 0; +} + +.graph-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.volume-graph h2 { + margin-top: 0; + margin-bottom: 0; + font-size: 1.25rem; + color: #1f2937; +} + +.data-type-toggle { + display: flex; + gap: 0.5rem; +} + +.data-type-toggle button { + padding: 0.3em 0.8em; + font-size: 0.875rem; +} + +.graph-container { + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + border: 1px solid #e5e7eb; + border-radius: 6px; + overflow: hidden; + background-color: #fff; + padding: 10px; + box-sizing: border-box; +} + +.no-data { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + color: #6b7280; + font-style: italic; + min-height: 300px; +} + +/* Override recharts default styles */ +.recharts-cartesian-grid-horizontal line, +.recharts-cartesian-grid-vertical line { + stroke: #e5e7eb; +} + +.recharts-tooltip-wrapper { + z-index: 1000; +} + +.recharts-default-tooltip { + background-color: #fff !important; + border: 1px solid #e5e7eb !important; + border-radius: 4px !important; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important; + padding: 10px !important; +} + +.recharts-tooltip-label { + font-weight: bold !important; + color: #1f2937 !important; + margin-bottom: 5px !important; +} + +.recharts-tooltip-item-list { + padding: 0 !important; + margin: 0 !important; +} + +.recharts-tooltip-item { + margin: 4px 0 !important; + color: #4b5563 !important; +} + +.recharts-legend-wrapper { + padding: 10px 0 !important; + display: flex !important; + justify-content: center !important; + align-items: center !important; +} + +.recharts-legend-item { + margin-right: 20px !important; +} + +.recharts-legend-item-text { + font-size: 0.875rem !important; + color: #4b5563 !important; +} + +.metrics-definitions { + margin-top: 2rem; + padding: 1.5rem; + background-color: #f8fafc; + border-radius: 6px; + border: 1px solid #e5e7eb; +} + +.metrics-definitions h3 { + margin: 0 0 1rem 0; + font-size: 1.125rem; + font-weight: 600; + color: #1f2937; +} + +.definition-item { + margin-bottom: 0.75rem; + font-size: 0.875rem; + color: #4b5563; + line-height: 1.5; +} + +.definition-item:last-child { + margin-bottom: 0; +} + +.definition-item strong { + color: #1f2937; + font-weight: 600; +} \ No newline at end of file diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..a62838d --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,17 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { PublicClientApplication } from '@azure/msal-browser' +import { MsalProvider } from '@azure/msal-react' +import './index.css' +import App from './App.jsx' +import { msalConfig } from './authConfig' + +const msalInstance = new PublicClientApplication(msalConfig) + +createRoot(document.getElementById('root')).render( + + + + + , +) diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js new file mode 100644 index 0000000..8574723 --- /dev/null +++ b/frontend/src/services/api.js @@ -0,0 +1,81 @@ +import axios from 'axios'; +import { PublicClientApplication } from '@azure/msal-browser'; + +// Determine the API URL based on the environment +const isDevelopment = import.meta.env.DEV; + +// In development mode, use the local URL; in production, use the deployed URL +const API_URL = isDevelopment + ? 'http://localhost:5001/api' + : 'https://baic.oliver.solutions/dashboard/back/api'; + +/** + * Get access token from MSAL instance + * @param {PublicClientApplication} msalInstance - MSAL instance + * @returns {Promise} Access token + */ +const getAccessToken = async (msalInstance) => { + try { + console.error('DEBUG: getAccessToken called with msalInstance:', !!msalInstance); + + if (!msalInstance) { + console.error('DEBUG: MSAL instance is null or undefined'); + throw new Error('MSAL instance is null or undefined'); + } + + const accounts = msalInstance.getAllAccounts(); + console.error('DEBUG: Found accounts:', accounts.length); + + if (accounts.length === 0) { + console.error('DEBUG: No accounts found - user may need to login'); + throw new Error('No accounts found'); + } + + const request = { + scopes: ['User.Read'], + account: accounts[0] + }; + + console.error('DEBUG: Token request scopes:', request.scopes); + const response = await msalInstance.acquireTokenSilent(request); + console.error('DEBUG: Token acquired successfully:', !!response.accessToken); + return response.accessToken; + } catch (error) { + console.error('ERROR in getAccessToken:', error); + console.error('ERROR details:', error.message); + throw error; + } +}; + +/** + * Fetches all data (conversations and messages) from the backend API + * @param {PublicClientApplication} msalInstance - MSAL instance for authentication + * @returns {Promise} Promise resolving to the API response containing conversations and messages + */ +export const fetchAllData = async (msalInstance = null) => { + try { + console.error('DEBUG: fetchAllData called with msalInstance:', !!msalInstance); + const headers = {}; + + // Add authorization header if MSAL instance is provided + if (msalInstance) { + console.error('DEBUG: Attempting to get access token...'); + const accessToken = await getAccessToken(msalInstance); + headers.Authorization = `Bearer ${accessToken}`; + console.error('DEBUG: Authorization header set:', !!headers.Authorization); + console.error('DEBUG: Token length:', accessToken ? accessToken.length : 0); + } else { + console.error('DEBUG: No MSAL instance provided - no authorization header will be set'); + } + + console.error('DEBUG: Making API request to:', `${API_URL}/data`); + console.error('DEBUG: Headers being sent:', Object.keys(headers)); + const response = await axios.get(`${API_URL}/data`, { headers }); + return response.data; + } catch (error) { + console.error('ERROR fetching data:', error); + console.error('ERROR response status:', error.response?.status); + console.error('ERROR response data:', error.response?.data); + throw error; + } +}; \ No newline at end of file diff --git a/frontend/src/services/assistantMapping.js b/frontend/src/services/assistantMapping.js new file mode 100644 index 0000000..84400e6 --- /dev/null +++ b/frontend/src/services/assistantMapping.js @@ -0,0 +1,29 @@ +/** + * Mapping of Assistant IDs to friendly display names + */ +export const assistantMapping = { + 'asst_Pz7uhnK7aOoYykl7KalyirY9': 'PPC', + 'asst_MT0qKXI57m8Y2RVllqwFUqBe': 'Social', + 'asst_vlFx0Uud1BKtp7j77Vp0pi8H': 'Internal Banners', + 'asst_iiqnQ0w6l5eQAQtLJxDTklwK': 'FreeChat', + 'asst_eAsIXFpSGiy7jQzyF8p0IRDA': 'Display Banners', + 'asst_l9G2nl9TacmuheBsueRxTznc': 'Email (beta)' +}; + +/** + * Returns a friendly display name for a given Assistant ID + * @param {string} assistantId - The Assistant ID + * @returns {string|null} - The friendly display name or null if not found + */ +export const getAssistantDisplayName = (assistantId) => { + return assistantMapping[assistantId] || null; +}; + +/** + * Checks if an Assistant ID has a friendly name mapping + * @param {string} assistantId - The Assistant ID + * @returns {boolean} - True if the ID has a friendly name, false otherwise + */ +export const hasAssistantFriendlyName = (assistantId) => { + return assistantId in assistantMapping; +}; \ No newline at end of file diff --git a/frontend/src/services/userMapping.js b/frontend/src/services/userMapping.js new file mode 100644 index 0000000..37bb13b --- /dev/null +++ b/frontend/src/services/userMapping.js @@ -0,0 +1,184 @@ +/** + * User mapping utilities for processing user data from email addresses + * Handles organization detection, friendly name extraction, and technical suffix cleanup + */ + +import { userDisplayNameMap } from '../data/userDisplayNames.js'; + +/** + * Extract organization from email address based on domain content + * @param {string} email - The email address to analyze + * @returns {string} - 'Oliver' | 'Barclays' | 'Other' + */ +export const getOrganizationFromEmail = (email) => { + if (!email || typeof email !== 'string') { + return 'Other'; + } + + const lowerEmail = email.toLowerCase(); + + if (lowerEmail.includes('barclay')) { + return 'Barclays'; + } + + if (lowerEmail.includes('@oliver.agency')) { + return 'Oliver'; + } + + return 'Other'; +}; + +/** + * Strip technical suffixes from email addresses + * Handles external user patterns from Barclays and other external domains + * @param {string} email - The email address to clean + * @returns {string} - Cleaned email address + */ +export const stripTechnicalSuffixes = (email) => { + if (!email || typeof email !== 'string') { + return email; + } + + // Remove external user patterns: + // - _barclays.com#ext#@olivermarketing.onmicrosoft.com + // - _barclaycard.co.uk#ext#@olivermarketing.onmicrosoft.com + // - Any similar _domain#ext#@... patterns + return email + .replace(/_barclays\.com#ext#@olivermarketing\.onmicrosoft\.com$/, '') + .replace(/_barclaycard\.co\.uk#ext#@olivermarketing\.onmicrosoft\.com$/, '') + .replace(/_[^_]+\.(com|co\.uk)#ext#@olivermarketing\.onmicrosoft\.com$/, ''); +}; + +/** + * Extract friendly name from email prefix + * Converts email prefix to "FirstName LastName" format + * Handles both Oliver agency camelCase and external dot.separated formats + * @param {string} email - The email address to parse + * @returns {string} - Formatted friendly name + */ +export const getFriendlyNameFromEmail = (email) => { + if (!email || typeof email !== 'string') { + return email || 'Unknown User'; + } + + // First check manual mapping for exact matches + if (userDisplayNameMap[email.toLowerCase()]) { + return userDisplayNameMap[email.toLowerCase()]; + } + + // Strip technical suffixes first + const cleanEmail = stripTechnicalSuffixes(email); + + // Extract the part before @ + const prefix = cleanEmail.split('@')[0]; + + if (!prefix) { + return email; + } + + // Handle Oliver agency emails (camelCase format like 'michaelclervi') + if (email.includes('@oliver.agency')) { + // Try to split camelCase into words + const words = prefix.match(/[A-Z][a-z]*|[a-z]+/g) || [prefix]; + if (words.length >= 2) { + return words.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' '); + } else { + // Single word, just capitalize first letter + return prefix.charAt(0).toUpperCase() + prefix.slice(1).toLowerCase(); + } + } else { + // Handle external emails (dot.separated format like 'adam.webb') + const spaced = prefix.replace(/[._-]/g, ' '); + + // Title case each word + const titleCased = spaced + .toLowerCase() + .split(' ') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + + return titleCased; + } +}; + +/** + * Process raw user list into organized structure for filtering + * @param {string[]} users - Array of raw User_ID values (emails) + * @returns {Object} - Processed user data with organization grouping + */ +export const processUserData = (users) => { + if (!Array.isArray(users)) { + return { + byOrganization: { + 'Oliver': [], + 'Barclays': [], + 'Other': [] + }, + friendlyNameMap: {}, + organizationMap: {} + }; + } + + const byOrganization = { + 'Oliver': [], + 'Barclays': [], + 'Other': [] + }; + + const friendlyNameMap = {}; // email -> friendly name + const organizationMap = {}; // email -> organization + + users.forEach(email => { + if (!email) return; + + const organization = getOrganizationFromEmail(email); + const friendlyName = getFriendlyNameFromEmail(email); + + byOrganization[organization].push(email); + friendlyNameMap[email] = friendlyName; + organizationMap[email] = organization; + }); + + // Sort users within each organization by friendly name + Object.keys(byOrganization).forEach(org => { + byOrganization[org].sort((a, b) => { + const nameA = friendlyNameMap[a].toLowerCase(); + const nameB = friendlyNameMap[b].toLowerCase(); + return nameA.localeCompare(nameB); + }); + }); + + return { + byOrganization, + friendlyNameMap, + organizationMap + }; +}; + +/** + * Get filtered user list based on organization selection + * @param {Object} processedUserData - Result from processUserData() + * @param {string} organizationFilter - 'All Users' | 'Oliver Users' | 'Barclays Users' + * @returns {string[]} - Filtered array of user emails + */ +export const getFilteredUsers = (processedUserData, organizationFilter) => { + if (!processedUserData || !processedUserData.byOrganization) { + return []; + } + + const { byOrganization } = processedUserData; + + switch (organizationFilter) { + case 'Oliver Users': + return byOrganization['Oliver'] || []; + case 'Barclays Users': + return byOrganization['Barclays'] || []; + case 'All Users': + default: + return [ + ...(byOrganization['Oliver'] || []), + ...(byOrganization['Barclays'] || []), + ...(byOrganization['Other'] || []) + ]; + } +}; \ No newline at end of file diff --git a/frontend/src/utils/brandMapping.js b/frontend/src/utils/brandMapping.js new file mode 100644 index 0000000..a9d6728 --- /dev/null +++ b/frontend/src/utils/brandMapping.js @@ -0,0 +1,50 @@ +/** + * Brand/TOV mapping utility for transforming raw brand values to display-friendly names + */ + +// Mapping from raw brand values to display names +export const BRAND_DISPLAY_MAPPING = { + 'standard': 'Barclays', + 'pep': 'Barclaycard' +}; + +// Reverse mapping from display names to raw values +export const DISPLAY_TO_BRAND_MAPPING = Object.fromEntries( + Object.entries(BRAND_DISPLAY_MAPPING).map(([key, value]) => [value, key]) +); + +/** + * Convert raw brand value to display name + * @param {string} rawValue - The raw brand value from the data + * @returns {string} - The display-friendly name or the original value if no mapping exists + */ +export const getBrandDisplayName = (rawValue) => { + return BRAND_DISPLAY_MAPPING[rawValue] || rawValue; +}; + +/** + * Convert display name back to raw brand value + * @param {string} displayName - The display name shown to users + * @returns {string} - The raw brand value or the original display name if no mapping exists + */ +export const getBrandRawValue = (displayName) => { + return DISPLAY_TO_BRAND_MAPPING[displayName] || displayName; +}; + +/** + * Transform an array of raw brand values to display names + * @param {string[]} rawValues - Array of raw brand values + * @returns {string[]} - Array of display names + */ +export const transformBrandValuesToDisplay = (rawValues) => { + return rawValues.map(getBrandDisplayName); +}; + +/** + * Check if a raw brand value has a display mapping + * @param {string} rawValue - The raw brand value to check + * @returns {boolean} - True if a mapping exists + */ +export const hasBrandMapping = (rawValue) => { + return rawValue in BRAND_DISPLAY_MAPPING; +}; \ No newline at end of file diff --git a/frontend/src/utils/dataAggregation.js b/frontend/src/utils/dataAggregation.js new file mode 100644 index 0000000..389d980 --- /dev/null +++ b/frontend/src/utils/dataAggregation.js @@ -0,0 +1,305 @@ +import { parseISO, format, startOfDay, differenceInDays, isValid, addDays } from 'date-fns'; +import { getAssistantDisplayName, hasAssistantFriendlyName } from '../services/assistantMapping'; +import { getBrandDisplayName } from './brandMapping'; + +/** + * Generate aggregated statistical data for conversations and messages + * This utility creates time-series data that can be used for both visualization and CSV export + * + * @param {Array} filteredConversations - Filtered conversation data + * @param {Array} filteredMessages - Filtered message data + * @param {Array} uniqueAssistants - Array of unique assistant IDs + * @param {Object} filters - Current filter state from dashboard + * @returns {Object} Aggregated data with daily/weekly/monthly breakdowns + */ +export const generateAggregatedData = (filteredConversations, filteredMessages, uniqueAssistants, filters) => { + if (!filteredConversations.length && !filteredMessages.length) { + return { + chartData: [], + exportData: [], + aggregationType: 'daily', + dateRange: null + }; + } + + // Get all timestamps for date range calculation + const allTimestamps = [ + ...filteredConversations.map(conv => conv.StartTime), + ...filteredMessages.map(msg => msg.Timestamp) + ].filter(Boolean); + + const validDates = allTimestamps + .map(dateStr => { + try { return parseISO(dateStr); } catch { return null; } + }) + .filter(dateObj => dateObj && isValid(dateObj)); + + if (validDates.length === 0) { + return { + chartData: [], + exportData: [], + aggregationType: 'daily', + dateRange: null + }; + } + + const minDate = startOfDay(new Date(Math.min(...validDates))); + const maxDate = startOfDay(new Date(Math.max(...validDates))); + const daysDiff = differenceInDays(maxDate, minDate) + 1; + + // Determine aggregation type based on date range + const useWeekly = daysDiff > 60 && daysDiff <= 180; + const useMonthly = daysDiff > 180; + let aggregationType = 'daily'; + if (useMonthly) { + aggregationType = 'monthly'; + } else if (useWeekly) { + aggregationType = 'weekly'; + } + + // Initialize data structures + const dataByDate = {}; + const monthlyData = {}; + const weeklyData = {}; + + for (let i = 0; i < daysDiff; i++) { + const currentDate = addDays(minDate, i); + const dateKey = format(currentDate, 'yyyy-MM-dd'); + const monthKey = format(currentDate, 'yyyy-MM'); + + const dayOffsetFromMin = differenceInDays(currentDate, minDate); + const weekNumber = Math.floor(dayOffsetFromMin / 7) + 1; + const weekKey = `Week ${weekNumber}`; + + // Daily data + dataByDate[dateKey] = { + date: dateKey, + conversationsTotal: 0, + messagesTotal: 0, + details: [] // Will store breakdown by assistant/user/brand + }; + + // Initialize assistant counters + if (uniqueAssistants && uniqueAssistants.length > 0) { + uniqueAssistants.forEach(assistantId => { + dataByDate[dateKey][`conversations_${assistantId}`] = 0; + dataByDate[dateKey][`messages_${assistantId}`] = 0; + }); + } + + // Monthly data + if (!monthlyData[monthKey]) { + monthlyData[monthKey] = { + month: monthKey, + conversationsTotal: 0, + messagesTotal: 0, + details: [] + }; + if (uniqueAssistants && uniqueAssistants.length > 0) { + uniqueAssistants.forEach(assistantId => { + monthlyData[monthKey][`conversations_${assistantId}`] = 0; + monthlyData[monthKey][`messages_${assistantId}`] = 0; + }); + } + } + + // Weekly data + if (!weeklyData[weekKey]) { + const weekStartDate = addDays(minDate, (weekNumber - 1) * 7); + weeklyData[weekKey] = { + week: weekKey, + startDateFormatted: format(weekStartDate, 'MMM d, yyyy'), + conversationsTotal: 0, + messagesTotal: 0, + details: [] + }; + if (uniqueAssistants && uniqueAssistants.length > 0) { + uniqueAssistants.forEach(assistantId => { + weeklyData[weekKey][`conversations_${assistantId}`] = 0; + weeklyData[weekKey][`messages_${assistantId}`] = 0; + }); + } + } + } + + // Aggregate conversation counts + filteredConversations.forEach(conv => { + if (conv.StartTime) { + const parsedDate = parseISO(conv.StartTime); + if (isValid(parsedDate)) { + const dateStr = format(parsedDate, 'yyyy-MM-dd'); + const monthStr = format(parsedDate, 'yyyy-MM'); + const dayOffsetFromMinAgg = differenceInDays(parsedDate, minDate); + const weekNumAgg = Math.floor(dayOffsetFromMinAgg / 7) + 1; + const weekKeyAgg = `Week ${weekNumAgg}`; + + // Aggregate totals + if (dataByDate[dateStr]) { + dataByDate[dateStr].conversationsTotal++; + if (conv.Assistant_ID && dataByDate[dateStr][`conversations_${conv.Assistant_ID}`] !== undefined) { + dataByDate[dateStr][`conversations_${conv.Assistant_ID}`]++; + } + } + if (monthlyData[monthStr]) { + monthlyData[monthStr].conversationsTotal++; + if (conv.Assistant_ID && monthlyData[monthStr][`conversations_${conv.Assistant_ID}`] !== undefined) { + monthlyData[monthStr][`conversations_${conv.Assistant_ID}`]++; + } + } + if (weeklyData[weekKeyAgg]) { + weeklyData[weekKeyAgg].conversationsTotal++; + if (conv.Assistant_ID && weeklyData[weekKeyAgg][`conversations_${conv.Assistant_ID}`] !== undefined) { + weeklyData[weekKeyAgg][`conversations_${conv.Assistant_ID}`]++; + } + } + } + } + }); + + // Aggregate message counts + filteredMessages.forEach(msg => { + if (msg.Timestamp) { + const parsedDate = parseISO(msg.Timestamp); + if (isValid(parsedDate)) { + const dateStr = format(parsedDate, 'yyyy-MM-dd'); + const monthStr = format(parsedDate, 'yyyy-MM'); + const dayOffsetFromMinAgg = differenceInDays(parsedDate, minDate); + const weekNumAgg = Math.floor(dayOffsetFromMinAgg / 7) + 1; + const weekKeyAgg = `Week ${weekNumAgg}`; + + // Aggregate totals + if (dataByDate[dateStr]) { + dataByDate[dateStr].messagesTotal++; + if (msg.Assistant_ID && dataByDate[dateStr][`messages_${msg.Assistant_ID}`] !== undefined) { + dataByDate[dateStr][`messages_${msg.Assistant_ID}`]++; + } + } + if (monthlyData[monthStr]) { + monthlyData[monthStr].messagesTotal++; + if (msg.Assistant_ID && monthlyData[monthStr][`messages_${msg.Assistant_ID}`] !== undefined) { + monthlyData[monthStr][`messages_${msg.Assistant_ID}`]++; + } + } + if (weeklyData[weekKeyAgg]) { + weeklyData[weekKeyAgg].messagesTotal++; + if (msg.Assistant_ID && weeklyData[weekKeyAgg][`messages_${msg.Assistant_ID}`] !== undefined) { + weeklyData[weekKeyAgg][`messages_${msg.Assistant_ID}`]++; + } + } + } + } + }); + + // Convert to arrays and sort + const dailyDataArray = Object.values(dataByDate).sort((a, b) => a.date.localeCompare(b.date)); + const monthlyDataArray = Object.values(monthlyData).sort((a, b) => a.month.localeCompare(b.month)); + const weeklyDataArray = Object.values(weeklyData).sort((a,b) => { + const weekNumA = parseInt(a.week.split(' ')[1]); + const weekNumB = parseInt(b.week.split(' ')[1]); + return weekNumA - weekNumB; + }); + + // Select the appropriate data based on aggregation type + let chartData = dailyDataArray; + if (aggregationType === 'monthly') { + chartData = monthlyDataArray; + } else if (aggregationType === 'weekly') { + chartData = weeklyDataArray; + } + + // Generate export data - flatten the aggregated data for CSV export + const exportData = generateExportData(chartData, aggregationType, filters, uniqueAssistants); + + return { + chartData, + exportData, + aggregationType, + dateRange: { minDate, maxDate, daysDiff } + }; +}; + +/** + * Generate flattened data for CSV export + * Creates rows with columns: Date, Period_Type, Assistant Name, Brand, User_Organization, Conversation_Count, Message_Count + */ +const generateExportData = (chartData, aggregationType, filters, uniqueAssistants) => { + const exportRows = []; + + chartData.forEach(dataPoint => { + // Get the appropriate date field based on aggregation type + let dateValue, periodType; + if (aggregationType === 'monthly') { + dateValue = dataPoint.month; + periodType = 'Monthly'; + } else if (aggregationType === 'weekly') { + dateValue = dataPoint.week; + periodType = 'Weekly'; + } else { + dateValue = dataPoint.date; + periodType = 'Daily'; + } + + // If specific assistant is selected, only export data for that assistant + if (filters.assistant && filters.assistant !== 'All Assistants') { + // Get the assistant ID from the display name + const assistantId = window.assistantDisplayToIdMap?.[filters.assistant] || filters.assistant; + + // Only export if assistant has a friendly name + if (hasAssistantFriendlyName(assistantId)) { + const assistantDisplayName = getAssistantDisplayName(assistantId); + + const conversationCount = dataPoint[`conversations_${assistantId}`] || 0; + const messageCount = dataPoint[`messages_${assistantId}`] || 0; + + exportRows.push({ + Date: dateValue, + Period_Type: periodType, + 'Assistant Name': assistantDisplayName, + Brand: filters.brandTOV !== 'All Brands/TOV' ? getBrandDisplayName(filters.brandTOV) : 'All Brands', + User_Organization: filters.organization !== 'All Users' ? filters.organization : 'All Organizations', + Conversation_Count: conversationCount, + Message_Count: messageCount + }); + } + } else { + // Export data for all assistants or totals + if (uniqueAssistants && uniqueAssistants.length > 0) { + // Create separate rows for each assistant + uniqueAssistants.forEach(assistantId => { + // Only export assistants with friendly names + if (hasAssistantFriendlyName(assistantId)) { + const assistantDisplayName = getAssistantDisplayName(assistantId); + const conversationCount = dataPoint[`conversations_${assistantId}`] || 0; + const messageCount = dataPoint[`messages_${assistantId}`] || 0; + + // Only include rows with actual data + if (conversationCount > 0 || messageCount > 0) { + exportRows.push({ + Date: dateValue, + Period_Type: periodType, + 'Assistant Name': assistantDisplayName, + Brand: filters.brandTOV !== 'All Brands/TOV' ? getBrandDisplayName(filters.brandTOV) : 'All Brands', + User_Organization: filters.organization !== 'All Users' ? filters.organization : 'All Organizations', + Conversation_Count: conversationCount, + Message_Count: messageCount + }); + } + } + }); + } + + // Also add total row + exportRows.push({ + Date: dateValue, + Period_Type: periodType, + 'Assistant Name': 'All Assistants', + Brand: filters.brandTOV !== 'All Brands/TOV' ? getBrandDisplayName(filters.brandTOV) : 'All Brands', + User_Organization: filters.organization !== 'All Users' ? filters.organization : 'All Organizations', + Conversation_Count: dataPoint.conversationsTotal || 0, + Message_Count: dataPoint.messagesTotal || 0 + }); + } + }); + + return exportRows; +}; \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..f2f73bd --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + base: '/dashboard/', +}) diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..d363a7b --- /dev/null +++ b/run.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Start the backend server with Hypercorn +echo "Starting backend server with Hypercorn..." +cd backend +python app.py & +BACKEND_PID=$! + +# Wait a bit for the backend to start +sleep 2 + +# Start the frontend dev server +echo "Starting frontend dev server..." +cd ../frontend +npm run dev & +FRONTEND_PID=$! + +# Function to handle script termination +cleanup() { + echo "Shutting down servers..." + kill $FRONTEND_PID + kill $BACKEND_PID + exit 0 +} + +# Set up trap to handle Ctrl+C and other termination signals +trap cleanup INT TERM + +# Keep the script running +echo "Both servers are running. Press Ctrl+C to stop." +wait \ No newline at end of file diff --git a/test_user_mapping.js b/test_user_mapping.js new file mode 100644 index 0000000..8d12a1b --- /dev/null +++ b/test_user_mapping.js @@ -0,0 +1,69 @@ +// Test the current userMapping logic with real email data +const stripTechnicalSuffixes = (email) => { + if (!email || typeof email !== 'string') { + return email; + } + + return email + .replace(/_barclays\.com#ext#@olivermarketing\.onmicrosoft\.com$/, '') + .replace(/_barclaycard\.co\.uk#ext#@olivermarketing\.onmicrosoft\.com$/, '') + .replace(/_[^_]+\.(com|co\.uk)#ext#@olivermarketing\.onmicrosoft\.com$/, ''); +}; + +const getFriendlyNameFromEmail = (email) => { + if (!email || typeof email !== 'string') { + return email || 'Unknown User'; + } + + const cleanEmail = stripTechnicalSuffixes(email); + const prefix = cleanEmail.split('@')[0]; + + if (!prefix) { + return email; + } + + // Handle Oliver agency emails (camelCase format like 'michaelclervi') + if (email.includes('@oliver.agency')) { + // Try to split camelCase into words + const words = prefix.match(/[A-Z][a-z]*|[a-z]+/g) || [prefix]; + if (words.length >= 2) { + return words.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' '); + } else { + // Single word, just capitalize first letter + return prefix.charAt(0).toUpperCase() + prefix.slice(1).toLowerCase(); + } + } else { + // Handle external emails (dot.separated format like 'adam.webb') + const spaced = prefix.replace(/[._-]/g, ' '); + + // Title case each word + const titleCased = spaced + .toLowerCase() + .split(' ') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + + return titleCased; + } +}; + +// Test with actual email patterns +const testEmails = [ + 'michaelclervi@oliver.agency', + 'adam.webb_barclaycard.co.uk#ext#@olivermarketing.onmicrosoft.com', + 'aileen.stevenson_barclays.com#ext#@olivermarketing.onmicrosoft.com', + 'christopher.x.perkins_barclays.com#ext#@olivermarketing.onmicrosoft.com', + 'jeremycrockerwhite@oliver.agency', + 'armanpreetkapoor@oliver.agency', + 'daveporter@oliver.agency' +]; + +console.log('Testing email name extraction:'); +testEmails.forEach(email => { + const stripped = stripTechnicalSuffixes(email); + const friendly = getFriendlyNameFromEmail(email); + console.log(`${email}`); + console.log(` Stripped: ${stripped}`); + console.log(` Friendly: ${friendly}`); + console.log(''); +}); \ No newline at end of file