diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..49de1405422bee03b1fbec3d966d4d45d20c36c4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,96 @@ +# Models directory - downloaded at runtime +server/models/ +models/ + +# Node modules - installed during build +frontend/node_modules/ +**/node_modules/ + +# Frontend build artifacts - built during Docker build +frontend/dist/ +frontend/build/ + +# Development files +.git/ +.gitignore +*.md +README* + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Python cache and artifacts +__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 + +# Virtual environments +venv/ +env/ +ENV/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pytest +.pytest_cache/ + +# Coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Logs +*.log +logs/ + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Docker files (optional - uncomment if you don't want to include docker files) +# Dockerfile* +# docker-compose* +# .dockerignore + +# Temporary files +tmp/ +temp/ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..f6b1f326ca4ab7cf0c8798856f8fe0020ff82d58 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,36 @@ +*.7z filter=lfs diff=lfs merge=lfs -text +*.arrow filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -text +*.ftz filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.h5 filter=lfs diff=lfs merge=lfs -text +*.joblib filter=lfs diff=lfs merge=lfs -text +*.lfs.* filter=lfs diff=lfs merge=lfs -text +*.mlmodel filter=lfs diff=lfs merge=lfs -text +*.model filter=lfs diff=lfs merge=lfs -text +*.msgpack filter=lfs diff=lfs merge=lfs -text +*.npy filter=lfs diff=lfs merge=lfs -text +*.npz filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text +*.ot filter=lfs diff=lfs merge=lfs -text +*.parquet filter=lfs diff=lfs merge=lfs -text +*.pb filter=lfs diff=lfs merge=lfs -text +*.pickle filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.safetensors filter=lfs diff=lfs merge=lfs -text +saved_model/**/* filter=lfs diff=lfs merge=lfs -text +*.tar.* filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.tflite filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.wasm filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text +*tfevents* filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..bb610c1220f17d608cd9b6f612eb4a347b13caea --- /dev/null +++ b/.gitignore @@ -0,0 +1,158 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +# wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +*.log +logs/ + +# Temporary files +*.tmp +*.temp + +# Git backup directories +.git.backup + +# Models directory (only at root level) +/models/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..a879e83b1a3277f54918bad89e9a15f4cea431bf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,151 @@ +# Frontend build stage +FROM node:18-alpine AS frontend-build + +# Google Analytics build args +ARG VITE_ENABLE_ANALYTICS +ARG VITE_REACT_APP_GOOGLE_ANALYTICS_ID +ARG VITE_ALLOW_ALL_LANGUAGES + +# Make build args available as environment variables during build +ENV VITE_ENABLE_ANALYTICS=${VITE_ENABLE_ANALYTICS} +ENV VITE_REACT_APP_GOOGLE_ANALYTICS_ID=${VITE_REACT_APP_GOOGLE_ANALYTICS_ID} +ENV VITE_ALLOW_ALL_LANGUAGES=${VITE_ALLOW_ALL_LANGUAGES} + +WORKDIR /app/frontend +COPY frontend/package.json frontend/package-lock.json* ./ +RUN npm install +COPY frontend/ ./ +RUN npm run build + +# Dockerfile to support Translations API Build - works locally and on Hugging Face Spaces +FROM nvidia/cuda:12.4.0-runtime-ubuntu22.04 as base + +ENV PYTHON_VERSION=3.10 \ + PYTHON_VERSION_SHORT=310 + +RUN apt-get update && apt-get upgrade -y + +# Install system packages including audio processing libraries +RUN apt-get install -y \ + build-essential \ + wget \ + python${PYTHON_VERSION} \ + python3-pip \ + libpq-dev + +#Constants +ENV PYTHONUNBUFFERED TRUE + +ARG DEBIAN_FRONTEND=noninteractive + +# Set up user with UID 1000 for HF Spaces compatibility +RUN useradd -m -u 1000 user + +# Install base utilities, linux packages, and audio processing libraries +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \ + fakeroot \ + ca-certificates \ + curl \ + vim \ + ssh \ + wget \ + gcc \ + git \ + ffmpeg \ + libsndfile1 \ + libsox-fmt-all \ + sox \ + libavcodec-extra && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Install miniconda +ENV CONDA_DIR /opt/conda +# Put conda in path and install +ENV PATH=$CONDA_DIR/bin:$PATH +RUN wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh \ + && /bin/bash ~/miniconda.sh -b -p /opt/conda + +RUN conda config --set auto_activate_base false && \ + conda config --set channel_priority flexible && \ + mkdir -p ~/.conda && \ + echo "channel_priority: flexible" > ~/.condarc && \ + conda config --add channels conda-forge && \ + conda config --set remote_max_retries 5 && \ + conda config --set remote_connect_timeout_secs 30 && \ + conda config --set remote_read_timeout_secs 30 && \ + conda config --set show_channel_urls True && \ + conda config --set auto_update_conda False && \ + conda config --set notify_outdated_conda False && \ + conda config --set report_errors False && \ + conda config --set always_yes True && \ + conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main && \ + conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r && \ + conda clean -afy + +RUN conda config --set channel_priority false && \ + conda create -n transcriptions-api python=${PYTHON_VERSION} -y && \ + conda install -n transcriptions-api -c conda-forge \ + libsndfile=1.0.31 \ + numpy \ + scipy \ + -y + +# Enable conda +SHELL ["conda", "run", "-n", "transcriptions-api", "/bin/bash", "-c"] + +# Set up working directory and environment for user +ENV HOME=/home/user \ + PATH=/home/user/.local/bin:$PATH + +WORKDIR $HOME/app + +# Copy requirements.txt and wheel file before installing dependencies +COPY --chown=user server/requirements.txt ./ +COPY --chown=user server/wheels/omnilingual_asr-0.1.0-py3-none-any.whl ./ + +# Install MMS library from local wheel file +RUN pip install omnilingual_asr-0.1.0-py3-none-any.whl + +# Install Python dependencies with proper conda activation +RUN pip install -r requirements.txt + +# Install debugpy for development debugging +RUN pip install debugpy + +# Copy server code into the image with proper ownership +COPY --chown=user ./server $HOME/app/server + +# Copy frontend build from the frontend-build stage +COPY --from=frontend-build --chown=user /app/frontend/dist $HOME/app/frontend/dist + +# Make scripts executable and create directories with proper ownership +RUN chmod +x $HOME/app/server/run.sh $HOME/app/server/download_models.sh && \ + mkdir -p $HOME/app/models && \ + chown -R user:user $HOME/app && \ + chmod -R 755 $HOME/app + +# Switch to user for runtime +USER user + +# Create /data/models and if possible (for HF Spaces) +RUN mkdir -p /data/models 2>/dev/null || true + +# Set working directory to server +WORKDIR $HOME/app/server + +# Expose port 7860 for HF Spaces (also works locally) +EXPOSE 7860 + +# For production: pre-download models into the image (optional) +# Uncomment the following lines if you want models baked into the production image +# RUN mkdir -p $HOME/app/models +# RUN cd $HOME/app/models && \ +# wget -O ctc_alignment_mling_uroman_model_dict.txt https://dl.fbaipublicfiles.com/mms/torchaudio/ctc_alignment_mling_uroman/dictionary.txt && \ +# wget -O ctc_alignment_mling_uroman_model.pt https://dl.fbaipublicfiles.com/mms/torchaudio/ctc_alignment_mling_uroman/model.pt && \ +# wget https://dl.fbaipublicfiles.com/mms/mms_1143_langs_tokenizer_spm.model && \ +# wget https://dl.fbaipublicfiles.com/mms/mms_XRI.pt + +# Default command - works for both local and HF Spaces +CMD ["conda", "run", "--no-capture-output", "-n", "transcriptions-api", "./run.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8b702c183e13300ecf9eadb0b88ae60cf9198e60 --- /dev/null +++ b/README.md @@ -0,0 +1,160 @@ +--- +title: Omnilingual ASR Media Transcription +emoji: 🌍 +colorFrom: blue +colorTo: green +sdk: docker +app_port: 7860 +pinned: false +license: mit +suggested_hardware: a100-large +--- + +# Experimental Omnilingual ASR Media Transcription Demo + +A media transcription tool with a web interface for multilingual audio and video transcription using Meta's Omnilingual ASR model. Transcriptions are supported for 1600+ languages. + +This application is designed primarily as a **web-based media transcription tool** with an intuitive frontend interface. While you can interact directly with the API endpoints, the recommended usage is through the web interface at `http://localhost:7860`. + +## HuggingFace Space Configuration + +This application is configured to run as a HuggingFace Space, however has resource limitations as it is a public. In order to have your own dedicated space, please clone with the following recommended specifications: + +- **Hardware**: A100 GPU (80GB) - Required for loading the 7B parameter Omnilingual ASR model + - _Alternative_: Machines with lower GPU memory can use smaller models by setting the `MODEL_NAME` environment variable in HuggingFace Space settings, e.g. `omniASR_LLM_300M` (requires ~8GB GPU memory) +- **Persistent Storage**: Enabled for model caching and improved loading times. Medium (150GB) +- **Docker Runtime**: Uses custom Dockerfile for fairseq2 and PyTorch integration +- **Port**: 7860 (HuggingFace standard) + +The A100 machine is specifically chosen to accommodate the large Omnilingual ASR model (~14GB) in GPU memory, ensuring fast inference and real-time transcription capabilities. + +## Running Outside HuggingFace + +While this application is designed for HuggingFace Spaces, **it can be run on any machine with Docker and GPU support** with similar hardware requirements as the machines on HuggingFace. + +## Getting Started + +### Running with Docker + +1. Build and run the container: + +```bash +docker build -t omnilingual-asr-transcriptions . +docker run --rm -p 7860:7860 --gpus all \ + -e MODEL_NAME=omniASR_LLM_300M \ + -v {your cache directory}:/home/user/app/models \ + omnilingual-asr-transcriptions +``` + +The media transcription app will be available at `http://localhost:7860` + +#### Docker Run Parameters Explained: + +- `--rm`: Automatically remove the container when it exits +- `-p 7860:7860`: Map host port 7860 to container port 7860 +- `--gpus all`: Enable GPU access for CUDA acceleration +- `-e MODEL_NAME=omniASR_LLM_300M`: Set the Omnilingual ASR model variant to use + - Options: `omniASR_LLM_1B` (default, 1B parameters), `omniASR_LLM_300M` (300M parameters, faster) +- `-e ENABLE_TOXIC_FILTERING=true`: Enable filtering of toxic words from transcription results (optional) +- `-v {your cache directory}:/home/user/app/models`: Mount local models directory + - **Purpose**: Persist downloaded models between container runs (14GB+ cache) + - **Benefits**: Avoid re-downloading models on each container restart + - **Path**: Adjust `{your cache directory}` to your local models directory + +### Available API Endpoints + +#### Core Transcription Routes + +- `GET /health` - Comprehensive health check with GPU/CUDA status, FFmpeg availability, and transcription status +- `GET /status` - Get current transcription status (busy/idle, progress, operation type) +- `POST /transcribe` - Audio transcription with automatic chunking for files of any length + +#### Additional Routes + +- `POST /combine-video-subtitles` - Combine video files with subtitle tracks +- `GET /` - Serve the web application frontend +- `GET /assets/` - Serve frontend static assets + +### Usage Recommendations + +**Primary Usage**: Access the web interface at `http://localhost:7860` for an intuitive media transcription experience with drag-and-drop file upload, real-time progress tracking, and downloadable results. + +**API Usage**: For programmatic access or integration with other tools, you can call the API endpoints directly as shown in the examples below. + +### Environment Variables + +You are free to change these if you clone the space and set them in the Huggingface space settings or in your own server environment. In the public shared demo these are controled for an optimal experience. + +#### Server Environment Variables + +- `API_LOG_LEVEL` - Set logging level (DEBUG, INFO, WARNING, ERROR) +- `MODEL_NAME` - Omnilingual ASR model to use (default: omniASR_LLM_1B) +- `USE_CHUNKING` - Enable/disable audio chunking (default: true) +- `ENABLE_TOXIC_FILTERING` - Enable toxic word filtering from transcription results (default: false) + +#### Frontend Environment Variables + +- `VITE_ALLOW_ALL_LANGUAGES` - Set to `true` to show all 1,400+ supported languages in the language selector, or `false` to only show languages with error rates < 10% for public demo (default: false) +- `VITE_ENABLE_ANALYTICS` - Set to `true` to enable Google Analytics tracking, or `false` to disable analytics (default: false) +- `VITE_REACT_APP_GOOGLE_ANALYTICS_ID` - Your Google Analytics measurement ID (e.g., `G-XXXXXXXXXX`) for tracking usage when analytics are enabled + +### API Examples (For Developers) + +For programmatic access or integration with other tools, you can call the API endpoints directly: + +```bash +# Health check +curl http://localhost:7860/health + +# Get transcription status +curl http://localhost:7860/status + +# Transcribe audio file +curl -X POST http://localhost:7860/transcribe \ + -F "audio=@path/to/your/audio.wav" +``` + +## Project Structure + +``` +omnilingual-asr-transcriptions/ +├── Dockerfile # Multi-stage build with frontend + backend +├── README.md +├── requirements.txt # Python dependencies +├── deploy.sh # Deployment script +├── run_docker.sh # Local Docker run script +├── frontend/ # Web interface (React/Vite) +│ ├── package.json +│ ├── src/ +│ └── dist/ # Built frontend (served by Flask) +├── models/ # Model files (automatically downloaded) +│ ├── ctc_alignment_mling_uroman_model.pt +│ ├── ctc_alignment_mling_uroman_model_dict.txt +│ └── [Additional model files downloaded at runtime] +└── server/ # Flask API backend + ├── server.py # Main Flask application + ├── transcriptions_blueprint.py # API routes + ├── audio_transcription.py # Core transcription logic + ├── media_transcription_processor.py # Media processing + ├── transcription_status.py # Status tracking + ├── env_vars.py # Environment configuration + ├── run.sh # Production startup script + ├── download_models.sh # Model download script + ├── wheels/ # Pre-built Omnilingual ASR wheel packages + └── inference/ # Model inference components + ├── mms_model_pipeline.py # Omnilingual ASR model wrapper + ├── audio_chunker.py # Audio chunking logic + └── audio_sentence_alignment.py # Forced alignment +``` + +### Key Features + +- **Simplified Architecture**: Single Docker container with built-in model management +- **Auto Model Download**: Models are downloaded automatically during container startup +- **Omnilingual ASR Integration**: Uses the latest Omnilingual ASR library with 1600+ language support +- **GPU Acceleration**: CUDA-enabled inference with automatic device detection +- **Web Interface**: Modern React frontend for easy testing and usage +- **Smart Transcription**: Single endpoint handles files of any length with automatic chunking +- **Intelligent Processing**: Automatic audio format detection and conversion + +**Note**: Model files are large (14GB+ total) and are downloaded automatically when the container starts. The first run may take longer due to model downloads. diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000000000000000000000000000000000000..3fa7c53d168fa828049fcbf0fef88eb8b220c4d4 --- /dev/null +++ b/frontend/.env @@ -0,0 +1,5 @@ +VITE_SERVER_URL='' + +# Set to 'true' to show all languages, 'false' to only show accurate languages for demo +VITE_ALLOW_ALL_LANGUAGES=true +VITE_ENABLE_ANALYTICS=false diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a613909220a6e6eb026f87cba165c57858a0b786 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,38 @@ +# Vite/React/TypeScript frontend +node_modules/ +dist/ +.vite/ +.env.local +.env.* +.DS_Store +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +.eslintcache +.parcel-cache +.turbo/ +.next/ +.vercel/ +.cache/ +.storybook/ +.swc/ +.coverage/ +coverage/ +.sass-cache/ +.nuxt/ +.output/ +.firebase/ +.firebaserc +.netlify/ +netlify.toml +.envrc +.env.test.local +.env.production.local +.env.development.local + +# Editor directories and files +.idea/ +.vscode/ +*.sublime-workspace +*.sublime-project diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e67e2d24c928e65fc00d5bfbd76548c02d6658ca --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,60 @@ +# Project Title + +A brief description of your project and its purpose. + +## Getting Started + +These instructions will help you set up the project on your local machine for development and testing. + +### Prerequisites + +- Node.js (version X.X.X or later) +- npm (version X.X.X or later) + +### Installation + +1. Clone the repository: + ``` + git clone + ``` + +2. Navigate to the project directory: + ``` + cd frontend + ``` + +3. Install the dependencies: + ``` + npm install + ``` + +### Running the Application + +To start the development server, run: +``` +npm run dev +``` + +Open your browser and go to `http://localhost:3000` to see the application in action. + +### Building for Production + +To create a production build, run: +``` +npm run build +``` + +The built files will be generated in the `dist` directory. + +## Usage + +- Upload audio or video files to transcribe. +- View synchronized transcriptions and download subtitles in various formats. + +## Contributing + +If you would like to contribute to this project, please fork the repository and submit a pull request. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000000000000000000000000000000000000..72f56320de5e673042f2395e04cb987ffd073567 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + Omnilingual ASR Media Transcription + + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..9797fa072eb89a1afa34853255ae58d615f8ac0f --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3438 @@ +{ + "name": "frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "1.0.0", + "dependencies": { + "@heroicons/react": "^2.2.0", + "daisyui": "^4.12.23", + "debounce": "^2.2.0", + "match-sorter": "^8.1.0", + "react": "^18.2.0", + "react-cookie-consent": "^9.0.0", + "react-dom": "^18.2.0", + "react-select": "^5.10.2", + "react-use": "^17.6.0", + "tailwindcss": "^3.0.0", + "zustand": "^5.0.7" + }, + "devDependencies": { + "@types/react": "^18.2.15", + "@types/react-dom": "^18.2.7", + "@vitejs/plugin-react": "^4.0.3", + "autoprefixer": "^10.0.0", + "postcss": "^8.0.0", + "typescript": "^5.1.6", + "vite": "^4.4.5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/@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==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "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-compilation-targets/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/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "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.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "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==", + "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.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", + "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "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==", + "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.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/js-cookie": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", + "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==", + "license": "MIT" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@xobotyi/scrollbar-width": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz", + "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "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.30001726", + "electron-to-chromium": "^1.5.173", + "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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "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/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "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==", + "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==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-in-js-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", + "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", + "license": "MIT", + "dependencies": { + "hyphenate-style-name": "^1.0.3" + } + }, + "node_modules/css-selector-tokenizer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", + "integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "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/culori": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz", + "integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/daisyui": { + "version": "4.12.24", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.24.tgz", + "integrity": "sha512-JYg9fhQHOfXyLadrBrEqCDM6D5dWCSSiM6eTNCRrBRzx/VlOCrLS8eDfIw9RVvs64v2mJdLooKXY8EwQzoszAA==", + "license": "MIT", + "dependencies": { + "css-selector-tokenizer": "^0.8", + "culori": "^3", + "picocolors": "^1", + "postcss-js": "^4" + }, + "engines": { + "node": ">=16.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/daisyui" + } + }, + "node_modules/debounce": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz", + "integrity": "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "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/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.195", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.195.tgz", + "integrity": "sha512-URclP0iIaDUzqcAyV1v2PgduJ9N0IdXmWsnPzPfelvBmjmZzEy6xJcjb1cXj+TbYqXgtLrjHEoaSIdTYhw4ezg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", + "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" + }, + "node_modules/fastest-stable-stringify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", + "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==", + "license": "MIT" + }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "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/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hyphenate-style-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", + "license": "BSD-3-Clause" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "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/inline-style-prefixer": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz", + "integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==", + "license": "MIT", + "dependencies": { + "css-in-js-utils": "^3.1.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==", + "license": "MIT" + }, + "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/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "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/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "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": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/match-sorter": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-8.1.0.tgz", + "integrity": "sha512-0HX3BHPixkbECX+Vt7nS1vJ6P2twPgGTU3PMXjWrl1eyVCL24tFHeyYN1FN5RKLzve0TyzNI9qntqQGbebnfPQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0" + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nano-css": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.2.tgz", + "integrity": "sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==", + "license": "Unlicense", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "css-tree": "^1.1.2", + "csstype": "^3.1.2", + "fastest-stable-stringify": "^2.0.2", + "inline-style-prefixer": "^7.0.1", + "rtl-css-js": "^1.16.1", + "stacktrace-js": "^2.0.2", + "stylis": "^4.3.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/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/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "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==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "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==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-cookie-consent": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/react-cookie-consent/-/react-cookie-consent-9.0.0.tgz", + "integrity": "sha512-Blyj+m+Zz7SFHYqT18p16EANgnSg2sIyU6Yp3vk83AnOnSW7qnehPkUe4+8+qxztJrNmCH5GP+VHsWzAKVOoZA==", + "license": "MIT", + "dependencies": { + "js-cookie": "^2.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "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/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-select": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz", + "integrity": "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.2.0" + }, + "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/react-universal-interface": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", + "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==", + "peerDependencies": { + "react": "*", + "tslib": "*" + } + }, + "node_modules/react-use": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.6.0.tgz", + "integrity": "sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==", + "license": "Unlicense", + "dependencies": { + "@types/js-cookie": "^2.2.6", + "@xobotyi/scrollbar-width": "^1.9.5", + "copy-to-clipboard": "^3.3.1", + "fast-deep-equal": "^3.1.3", + "fast-shallow-equal": "^1.0.0", + "js-cookie": "^2.2.1", + "nano-css": "^5.6.2", + "react-universal-interface": "^0.6.2", + "resize-observer-polyfill": "^1.5.1", + "screenfull": "^5.1.0", + "set-harmonic-interval": "^1.0.1", + "throttle-debounce": "^3.0.1", + "ts-easing": "^0.2.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", + "license": "MIT" + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/screenfull": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", + "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/set-harmonic-interval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz", + "integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==", + "license": "Unlicense", + "engines": { + "node": ">=6.9" + } + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-generator": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", + "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT" + }, + "node_modules/stacktrace-gps": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", + "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", + "license": "MIT", + "dependencies": { + "source-map": "0.5.6", + "stackframe": "^1.3.4" + } + }, + "node_modules/stacktrace-gps/node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "license": "MIT", + "dependencies": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, + "node_modules/ts-easing": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", + "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==", + "license": "Unlicense" + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "4.5.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", + "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "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==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/zustand": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.7.tgz", + "integrity": "sha512-Ot6uqHDW/O2VdYsKLLU8GQu8sCOM1LcoE8RwvLv9uuRT9s6SOHCKs0ZEOhxg+I1Ld+A1Q5lwx+UlKXXUoCZITg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000000000000000000000000000000000000..9fa62b02cd654bff3d7c48d9a9cd1996d5838c2e --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,32 @@ +{ + "name": "frontend", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview" + }, + "dependencies": { + "@heroicons/react": "^2.2.0", + "daisyui": "^4.12.23", + "debounce": "^2.2.0", + "match-sorter": "^8.1.0", + "react": "^18.2.0", + "react-cookie-consent": "^9.0.0", + "react-dom": "^18.2.0", + "react-select": "^5.10.2", + "react-use": "^17.6.0", + "tailwindcss": "^3.0.0", + "zustand": "^5.0.7" + }, + "devDependencies": { + "@types/react": "^18.2.15", + "@types/react-dom": "^18.2.7", + "@vitejs/plugin-react": "^4.0.3", + "autoprefixer": "^10.0.0", + "postcss": "^8.0.0", + "typescript": "^5.1.6", + "vite": "^4.4.5" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000000000000000000000000000000000000..8567b4c40cce463b404f90dad254b05adc29527f --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; \ No newline at end of file diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000000000000000000000000000000000000..61954e2a40b206d8d8460d20c42581fbd36bf924 --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e19d57f87d96a1e5b20a9b603f2d25b95d28b1c2 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import TranscriptionPage from './pages/TranscriptionPage'; +import WelcomeModal from './components/WelcomeModal'; +import { useTranscriptionStore } from './stores/transcriptionStore'; +import { trackWelcomeModalClose } from './analytics/gaEvents'; +import Analytics from './analytics/Analytics'; + +const App: React.FC = () => { + const { showWelcomeModal, setShowWelcomeModal } = useTranscriptionStore(); + + const handleCloseWelcomeModal = () => { + trackWelcomeModalClose(); + setShowWelcomeModal(false); + }; + + return ( +
+ + + +
+ ); +}; + +export default App; diff --git a/frontend/src/analytics/Analytics.tsx b/frontend/src/analytics/Analytics.tsx new file mode 100644 index 0000000000000000000000000000000000000000..189b1c568328cc750587a933cd07398a8dee869c --- /dev/null +++ b/frontend/src/analytics/Analytics.tsx @@ -0,0 +1,181 @@ +import React from "react"; +import CookieBanner from "./CookieBanner"; + +export const CONSENT_COOKIE_NAME = "omniasr_transcription_consent"; + +// Declare gtag global function +declare global { + interface Window { + gtag: (...args: any[]) => void; + dataLayer: any[]; + } +} + +const Analytics = () => { + const [analyticsEnabled, setAnalyticsEnabled] = React.useState(false); + const [consentState, setConsentState] = React.useState(null); // In-memory fallback + const [showBanner, setShowBanner] = React.useState(false); // Control banner visibility + + // Check if we're in iframe (like HuggingFace Spaces) + const isInIframe = () => { + try { + return window.self !== window.top; + } catch (e) { + return true; + } + }; + + // Get consent with fallback chain: memory -> localStorage -> sessionStorage -> cookies + const getConsent = (): boolean => { + // First check in-memory state (for HF spaces that block all storage) + if (consentState !== null) { + return consentState; + } + + // Try localStorage + try { + const localValue = window.localStorage.getItem(CONSENT_COOKIE_NAME); + if (localValue !== null) { + return localValue === "true"; + } + } catch (e) {} + + // Try sessionStorage + try { + const sessionValue = window.sessionStorage.getItem(CONSENT_COOKIE_NAME); + if (sessionValue !== null) { + return sessionValue === "true"; + } + } catch (e) {} + + // Try cookies + try { + return document.cookie.includes(`${CONSENT_COOKIE_NAME}=true`); + } catch (e) {} + + return false; + }; + + // Set consent with fallback chain + const setConsent = (accepted: boolean) => { + // Always set in-memory state first (works in all environments) + setConsentState(accepted); + + // Try localStorage + try { + window.localStorage.setItem(CONSENT_COOKIE_NAME, accepted.toString()); + } catch (e) {} + + // Try sessionStorage as fallback + try { + window.sessionStorage.setItem(CONSENT_COOKIE_NAME, accepted.toString()); + } catch (e) {} + + // Try cookies (mainly for non-iframe environments) + if (!isInIframe()) { + try { + const expires = new Date(); + expires.setFullYear(expires.getFullYear() + 1); + document.cookie = `${CONSENT_COOKIE_NAME}=${accepted}; expires=${expires.toUTCString()}; path=/; SameSite=Lax`; + } catch (e) {} + } + }; + + // Load gtag script dynamically + const loadGtagScript = (gaId: string) => { + return new Promise((resolve, reject) => { + // Check if gtag is already loaded + if (window.gtag) { + resolve(); + return; + } + + // Create script element + const script = document.createElement('script'); + script.async = true; + script.src = `https://www.googletagmanager.com/gtag/js?id=${gaId}`; + + script.onload = () => { + // Initialize gtag + window.dataLayer = window.dataLayer || []; + window.gtag = function gtag() { + window.dataLayer.push(arguments); + }; + window.gtag('js', new Date()); + window.gtag('config', gaId, { + // Settings for iframe environments + send_page_view: false, // We'll send manually + cookie_flags: 'max-age=7200;secure;samesite=none', // For iframe support + }); + + console.log('GA: gtag script loaded'); + resolve(); + }; + + script.onerror = () => { + console.error('❌ Failed to load gtag script'); + reject(new Error('Failed to load gtag script')); + }; + + document.head.appendChild(script); + }); + }; + + // Enable analytics if consent given + const handleAcceptCookie = React.useCallback(() => { + console.log('User accepted analytics cookies'); + setConsent(true); + setShowBanner(false); // Hide banner after acceptance + + const gaId = import.meta.env.VITE_REACT_APP_GOOGLE_ANALYTICS_ID; + const analyticsEnabled = import.meta.env.VITE_ENABLE_ANALYTICS === 'true'; + + if (gaId && analyticsEnabled) { + loadGtagScript(gaId) + .then(() => { + setAnalyticsEnabled(true); + console.log('GA initialized successfully'); + + // Send initial pageview + const pathname = window.location.pathname; + window.gtag('event', 'page_view', { + page_title: document.title, + page_location: window.location.href, + page_path: pathname, + }); + }) + .catch((e) => { + console.error('GA initialization failed:', e); + }); + } + }, []); + + const handleDeclineCookie = React.useCallback(() => { + console.log('User declined analytics cookies'); + setConsent(false); + setShowBanner(false); // Hide banner after decline + }, []); + + // Check for existing consent on mount + React.useEffect(() => { + const existingConsent = getConsent(); + if (existingConsent) { + console.log('GA: Found existing consent, initializing...'); + handleAcceptCookie(); + setShowBanner(false); // Don't show banner if consent already exists + } else { + setShowBanner(true); // Show banner if no consent + } + }, [handleAcceptCookie]); + + // Note: pageview is now sent directly in handleAcceptCookie when gtag is loaded + + return showBanner ? ( + + ) : null; +}; + +export default Analytics; diff --git a/frontend/src/analytics/CookieBanner.tsx b/frontend/src/analytics/CookieBanner.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c2cd773b3e37e27874c48f5651bde9fa4533f5ba --- /dev/null +++ b/frontend/src/analytics/CookieBanner.tsx @@ -0,0 +1,53 @@ +import CookieConsent from "react-cookie-consent"; +import { CONSENT_COOKIE_NAME } from "./Analytics"; + +interface CookieBannerProps { + onAccept?: (acceptedByScrolling: boolean) => void; + onDecline?: () => void; +} + +const CookieBanner = ({ onAccept, onDecline }: CookieBannerProps) => { + return ( + + ); +}; + +export default CookieBanner; diff --git a/frontend/src/analytics/gaEvents.ts b/frontend/src/analytics/gaEvents.ts new file mode 100644 index 0000000000000000000000000000000000000000..146288fae5aaf2faea24fc1c27c878f07e178a66 --- /dev/null +++ b/frontend/src/analytics/gaEvents.ts @@ -0,0 +1,97 @@ +import { CONSENT_COOKIE_NAME } from "./Analytics"; + +// Declare gtag global function +declare global { + interface Window { + gtag: (...args: any[]) => void; + } +} + +// Check if analytics are enabled and user has consented +const isAnalyticsEnabled = (): boolean => { + if (import.meta.env.VITE_ENABLE_ANALYTICS !== 'true') { + return false; + } + + // Check localStorage first (works in iframes like HuggingFace), then cookies + try { + const localStorageValue = localStorage.getItem(CONSENT_COOKIE_NAME); + if (localStorageValue === "true") return true; + } catch (e) {} + + return document.cookie.includes(`${CONSENT_COOKIE_NAME}=true`); +}; + +// Send GA4 event using gtag +export const sendGAEvent = ( + eventCategory: string, + eventAction: string, + eventLabel?: string, + value?: number +) => { + if (!isAnalyticsEnabled() || !window.gtag) { + return; + } + + try { + // Use gtag event format for GA4 + const eventName = `${eventCategory.toLowerCase()}_${eventAction.toLowerCase()}`; + const eventParams: any = { + event_category: eventCategory, + event_label: eventLabel, + }; + + if (value !== undefined) { + eventParams.value = value; + } + + window.gtag('event', eventName, eventParams); + } catch (error) { + console.error("GA Event Error:", error); + } +}; + +// Predefined event functions for common actions +export const trackTranscriptionStart = (languageCode: string) => { + sendGAEvent("Transcription", "start", languageCode); +}; + +export const trackTranscriptionComplete = (languageCode: string, duration?: number) => { + sendGAEvent("Transcription", "complete", languageCode, duration); +}; + +export const trackTranscriptionError = (languageCode: string, errorMessage?: string) => { + sendGAEvent("Transcription", "error", `${languageCode}${errorMessage ? ` - ${errorMessage}` : ''}`); +}; + +export const trackFileUpload = (fileType: string, fileSizeMB?: number) => { + sendGAEvent("File", "upload", fileType, fileSizeMB); +}; + +export const trackLanguageChange = (languageCode: string) => { + sendGAEvent("Language", "select", languageCode); +}; + +export const trackDownloadSRT = (languageCode: string) => { + sendGAEvent("Download", "srt", languageCode); +}; + +export const trackDownloadVideoWithSubtitles = (languageCode: string) => { + sendGAEvent("Download", "video_with_subtitles", languageCode); +}; + +export const trackReset = () => { + sendGAEvent("App", "reset"); +}; + +export const trackWelcomeModalClose = () => { + sendGAEvent("Modal", "welcome_close"); +}; + +export const trackSegmentEdit = (languageCode: string, editType: "text" | "timing") => { + sendGAEvent("Edit", editType, languageCode); +}; + +export const trackSegmentDelete = (languageCode: string) => { + sendGAEvent("Edit", "delete_segment", languageCode); +}; diff --git a/frontend/src/components/CanvasTimeline.tsx b/frontend/src/components/CanvasTimeline.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d892095e8956b0f72c16630846dd1ebf275bbc3e --- /dev/null +++ b/frontend/src/components/CanvasTimeline.tsx @@ -0,0 +1,393 @@ +import React, { + useRef, + useEffect, + useState, + useCallback, + forwardRef, +} from "react"; +import {AlignedSegment} from "../services/transcriptionApi"; +import {useTranscriptionStore} from "../stores/transcriptionStore"; +import {formatTime} from "../utils/subtitleUtils"; +import {assignTracksToSegments, getMaxTrackCount} from "../utils/trackUtils"; +import {useTimelineGeometry} from "../hooks/useTimelineGeometry"; +import {useTimelineDragControls} from "../hooks/useTimelineDragControls"; +import {useTimelineRenderer} from "../hooks/useTimelineRenderer"; +import SegmentEditor from "./SegmentEditor"; +import MediaDownloadControls from "./MediaDownloadControls"; +import MediaEditControls from "./MediaEditControls"; + +interface CanvasTimelineProps { + audioRef: React.RefObject; + videoRef: React.RefObject; + onSeekToSegment: (segment: AlignedSegment) => void; + onTimeUpdate: () => void; + viewport?: {start: number; end: number}; +} + +const CanvasTimeline = forwardRef( + ({audioRef, videoRef}, ref) => { + const canvasRef = useRef(null); + const containerRef = useRef(null); + + // Combine the forwarded ref with our internal ref + const combinedRef = useCallback( + (node: HTMLDivElement | null) => { + // Use Object.defineProperty to safely assign to current + if (containerRef.current !== node) { + Object.defineProperty(containerRef, "current", { + value: node, + writable: true, + configurable: true, + }); + } + + if (typeof ref === "function") { + ref(node); + } else if (ref) { + // Type assertion to overcome readonly constraint + const mutableRef = ref as any; + mutableRef.current = node; + } + }, + [ref] + ); + const [canvasSize, setCanvasSize] = useState({width: 1200, height: 200}); + + const { + transcription, + currentTime, + activeSegmentIndex, + selectedSegmentIndex, + currentSegments, + setSelectedSegmentIndex, + updateSegmentText, + deleteSegment, + } = useTranscriptionStore(); + + // Constants + const constants = { + TRACK_HEIGHT: 32, + TRACK_PADDING: 4, + TIMELINE_PADDING: 0, + PIXELS_PER_SECOND: 300, // Increased from 200 to give segments more space + }; + + // Early return if no transcription + if (!transcription) { + return null; + } + + const displaySegments = currentSegments || transcription.aligned_segments; + const segmentsWithTracks = assignTracksToSegments(displaySegments); + const trackCount = getMaxTrackCount(segmentsWithTracks); + + // Get actual media duration from audio/video elements + const getMediaDuration = useCallback(() => { + const audioElement = audioRef.current; + const videoElement = videoRef.current; + + if (audioElement && !isNaN(audioElement.duration)) { + return audioElement.duration; + } + if (videoElement && !isNaN(videoElement.duration)) { + return videoElement.duration; + } + + // Fallback to transcription duration if media duration is not available + return transcription.total_duration; + }, [audioRef, videoRef, transcription.total_duration]); + + const mediaDuration = getMediaDuration(); + console.log({mediaDuration}); + + // Calculate canvas dimensions based on full media duration + const timelineWidth = mediaDuration * constants.PIXELS_PER_SECOND; + const timelineHeight = + constants.TIMELINE_PADDING * 2 + + trackCount * (constants.TRACK_HEIGHT + constants.TRACK_PADDING); + + // Update canvas size when needed + useEffect(() => { + setCanvasSize({ + width: timelineWidth, // Canvas internal resolution + height: Math.max(timelineHeight, 200), + }); + }, [timelineWidth, timelineHeight, trackCount]); + + // Initialize geometry utilities + const geometryUtils = useTimelineGeometry({ + mediaDuration, + constants, + }); + + // Initialize drag controls + const dragControls = useTimelineDragControls({ + segmentsWithTracks, + displaySegments, + geometryUtils, + canvasRef, + containerRef, + mediaDuration, + constants: { + TRACK_HEIGHT: constants.TRACK_HEIGHT, + TIMELINE_PADDING: constants.TIMELINE_PADDING, + }, + }); + + // Initialize renderer + const {draw} = useTimelineRenderer({ + canvasRef, + canvasSize, + segmentsWithTracks, + displaySegments, + currentTime, + activeSegmentIndex, + selectedSegmentIndex, + hoveredSegment: dragControls.hoveredSegment, + isDragging: dragControls.isDragging, + dragSegmentIndex: dragControls.dragSegmentIndex, + mediaDuration, + geometryUtils, + constants, + }); + + // State for smooth scrolling animation + const scrollAnimationRef = useRef(null); + + // Smooth scroll implementation using requestAnimationFrame + const smoothScrollTo = useCallback( + ( + container: HTMLDivElement, + targetScrollLeft: number, + duration = 500 + ): Promise => { + return new Promise((resolve) => { + const startScrollLeft = container.scrollLeft; + const scrollDistance = targetScrollLeft - startScrollLeft; + const startTime = Date.now(); + + const animate = () => { + const currentTime = Date.now(); + const elapsedTime = currentTime - startTime; + const progress = Math.min(elapsedTime / duration, 1); + + // Use easeOutQuart for smooth deceleration + const easeOutQuart = 1 - Math.pow(1 - progress, 4); + + container.scrollLeft = + startScrollLeft + scrollDistance * easeOutQuart; + + if (progress < 1) { + scrollAnimationRef.current = requestAnimationFrame(animate); + } else { + scrollAnimationRef.current = null; + resolve(); + } + }; + + // Cancel any existing animation + if (scrollAnimationRef.current) { + cancelAnimationFrame(scrollAnimationRef.current); + } + + animate(); + }); + }, + [] + ); + + // Cleanup animation on unmount + useEffect(() => { + return () => { + if (scrollAnimationRef.current) { + cancelAnimationFrame(scrollAnimationRef.current); + } + }; + }, []); + + // Determine if media is playing for auto-scroll behavior + const isMediaPlaying = useCallback(() => { + const audioElement = audioRef.current; + const videoElement = videoRef.current; + const mediaElement = audioElement || videoElement; + return mediaElement && !mediaElement.paused && !mediaElement.ended; + }, [audioRef, videoRef]); + + // Track if we're currently animating scroll to avoid re-triggering + const isScrollingRef = useRef(false); + const prevCurrentTimeRef = useRef(currentTime); + + // Auto-scroll during playback: only when playing and playhead gets near edges (20%) + useEffect(() => { + const container = containerRef.current; + if (!container || !isMediaPlaying() || isScrollingRef.current) return; + + const timeX = geometryUtils.timeToX(currentTime); + const containerWidth = container.clientWidth; + const currentScrollLeft = container.scrollLeft; + const maxScrollLeft = Math.max(0, container.scrollWidth - containerWidth); + + // Calculate 20% edge boundaries + const leftEdge = currentScrollLeft + containerWidth * 0.2; + const rightEdge = + currentScrollLeft + containerWidth - containerWidth * 0.2; + + // Only scroll if playhead is near edges + if (timeX < leftEdge || timeX > rightEdge) { + isScrollingRef.current = true; + + // Center the playhead position + const targetScrollLeft = Math.max( + 0, + Math.min(maxScrollLeft, timeX - containerWidth / 2) + ); + + smoothScrollTo(container, targetScrollLeft, 800).then(() => { + isScrollingRef.current = false; + }); + } + }, [currentTime, geometryUtils, isMediaPlaying, smoothScrollTo]); + + // Handle manual seeking (scrubbing, keyboard shortcuts, etc.) + useEffect(() => { + const container = containerRef.current; + if (!container || isScrollingRef.current) return; + + const timeDifference = Math.abs(currentTime - prevCurrentTimeRef.current); + const isSeekOperation = timeDifference > 0.5; // Significant time jump indicates seeking + + if (isSeekOperation) { + const timeX = geometryUtils.timeToX(currentTime); + const containerWidth = container.clientWidth; + const currentScrollLeft = container.scrollLeft; + const maxScrollLeft = Math.max( + 0, + container.scrollWidth - containerWidth + ); + + // Check if the seek position is outside the visible area + const visibleStart = currentScrollLeft; + const visibleEnd = currentScrollLeft + containerWidth; + + if (timeX < visibleStart || timeX > visibleEnd) { + isScrollingRef.current = true; + + // Center the seek position + const targetScrollLeft = Math.max( + 0, + Math.min(maxScrollLeft, timeX - containerWidth / 2) + ); + + smoothScrollTo(container, targetScrollLeft, 600).then(() => { + isScrollingRef.current = false; + }); + } + } + + prevCurrentTimeRef.current = currentTime; + }, [currentTime, geometryUtils, smoothScrollTo]); + + // Redraw on scroll + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const handleScroll = () => { + draw(); + }; + + container.addEventListener("scroll", handleScroll); + return () => container.removeEventListener("scroll", handleScroll); + }, [draw]); + + return ( +
+ {/* Header */} +
+ {/* Download Buttons - Centered above edit controls */} + {/*
+ +
*/} + + {/* Edit Controls */} + {/* */} +
+ + {/* Canvas Container */} +
+ +
+ + {/* Tooltip for hovered segment */} + {dragControls.hoveredSegment !== null && + !dragControls.isDragging && + !dragControls.isTimelineDragging && + (() => { + // Find the segment in segmentsWithTracks that corresponds to the hovered original segment + const originalSegment = + displaySegments[dragControls.hoveredSegment]; + + // Safety check: ensure the segment exists + if (!originalSegment) return null; + + const hoveredSegmentWithTrack = segmentsWithTracks.find( + (s) => + s.start === originalSegment.start && + s.end === originalSegment.end && + s.text === originalSegment.text + ); + + if (!hoveredSegmentWithTrack) return null; + + return ( +
+
+ {hoveredSegmentWithTrack.text} +
+
+ {formatTime(hoveredSegmentWithTrack.start)} -{" "} + {formatTime(hoveredSegmentWithTrack.end)} ( + {hoveredSegmentWithTrack.duration.toFixed(1)}s) +
+
+ Click to select â€ĸ Drag to move â€ĸ Drag edges to resize +
+
+ ); + })()} + + {/* Segment Editor at Bottom */} + {selectedSegmentIndex !== null && + displaySegments[selectedSegmentIndex] && ( + setSelectedSegmentIndex(null)} + /> + )} +
+ ); + } +); + +CanvasTimeline.displayName = "CanvasTimeline"; + +export default CanvasTimeline; diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ff53d56dbe0c48e9878fc2614b63c37a80e99b06 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.tsx @@ -0,0 +1,131 @@ +import React, { Component, ReactNode } from 'react'; + +interface Props { + children: ReactNode; + componentName?: string; +} + +interface State { + hasError: boolean; + error: Error | null; + errorInfo: React.ErrorInfo | null; +} + +class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false, error: null, errorInfo: null }; + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error, errorInfo: null }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('ErrorBoundary caught an error:', error, errorInfo); + this.setState({ + error, + errorInfo + }); + } + + handleRetry = () => { + this.setState({ hasError: false, error: null, errorInfo: null }); + }; + + handleCopyError = () => { + const { error, errorInfo } = this.state; + const { componentName } = this.props; + + const errorText = ` +Component: ${componentName || 'Unknown'} +Error: ${error?.message || 'Unknown error'} +Stack: ${error?.stack || 'No stack trace available'} +Component Stack: ${errorInfo?.componentStack || 'No component stack available'} +Timestamp: ${new Date().toISOString()} + `.trim(); + + navigator.clipboard.writeText(errorText).then(() => { + alert('Error details copied to clipboard!'); + }).catch(() => { + // Fallback for older browsers + const textArea = document.createElement('textarea'); + textArea.value = errorText; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + alert('Error details copied to clipboard!'); + }); + }; + + render() { + if (this.state.hasError) { + const { componentName } = this.props; + const { error, errorInfo } = this.state; + + return ( +
+
+

+ Sorry, something went wrong +

+

+ {componentName ? `An error occurred in the ${componentName} component.` : 'An unexpected error occurred.'} +

+ + +
+ +
+ + Show Error Details (for developers) + +
+
+ Error Message: +
+                  {error?.message || 'Unknown error'}
+                
+
+ +
+ Stack Trace: +
+                  {error?.stack || 'No stack trace available'}
+                
+
+ + {errorInfo?.componentStack && ( +
+ Component Stack: +
+                    {errorInfo.componentStack}
+                  
+
+ )} + +
+ Timestamp: {new Date().toISOString()} +
+
+
+
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/frontend/src/components/FeedbackCard.tsx b/frontend/src/components/FeedbackCard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..64393002a0785dffcffaf9cad84308c919ca6e09 --- /dev/null +++ b/frontend/src/components/FeedbackCard.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +const FeedbackCard: React.FC = () => { + return ( +
+
+

Help Us Improve

+
+ +
+ Your feedback is crucial for improving language coverage and model quality for low-resource languages. + Please{' '} + + provide feedback + + {' '}to share your experiences, suggestions, and any issues you encounter. +
+
+ ); +}; + +export default FeedbackCard; diff --git a/frontend/src/components/FullTranscription.tsx b/frontend/src/components/FullTranscription.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ec1f3e0572b2b59af74241d23df618c058b74c80 --- /dev/null +++ b/frontend/src/components/FullTranscription.tsx @@ -0,0 +1,152 @@ +import React, { useState } from 'react'; +import { formatTime } from '../utils/subtitleUtils'; +import { useTranscriptionStore } from '../stores/transcriptionStore'; +import { InformationCircleIcon } from '@heroicons/react/24/outline'; + +const FullTranscription: React.FC = () => { + const [showInfoTooltip, setShowInfoTooltip] = useState(false); + const [showExpandedChunks, setShowExpandedChunks] = useState(false); + const { transcription, selectedLanguage } = useTranscriptionStore(); + + const renderInfoTooltip = () => { + if (!transcription) return null; + + const chunks = transcription.chunks || []; + const maxVisibleChunks = 3; + const hasMoreChunks = chunks.length > maxVisibleChunks; + const visibleChunks = showExpandedChunks ? chunks : chunks.slice(0, maxVisibleChunks); + + // Calculate better positioning to avoid edge cuts + const tooltipStyle: React.CSSProperties = { + position: 'fixed', + left: '50%', + top: '50%', + transform: 'translate(-50%, -50%)', + maxHeight: '80vh', // Prevent tooltip from being taller than viewport + overflowY: 'auto' as const + }; + + return ( +
+ +
Transcription Details
+ +
+
+ Model: + {transcription.model} +
+ +
+ Language: + + {selectedLanguage || 'auto-detect'} + +
+ +
+ Segments: + {transcription.num_segments} +
+ +
+ Duration: + {formatTime(transcription.total_duration)} +
+ +
+ Device: + {transcription.device} +
+ + {/* Long-form specific info */} + {transcription.num_chunks && ( + <> +
+
Long-form Processing
+
+
+ Chunks: + {transcription.num_chunks} +
+ + )} +
+ + {/* Improved Chunk details */} + {chunks.length > 0 && ( +
+
+
Chunk Details
+ {hasMoreChunks && ( + + )} +
+ +
+ {visibleChunks.map((chunk, index) => ( +
+
+
+ Chunk #{index + 1} +
+
+ {chunk.duration.toFixed(1)}s +
+
+
+ {formatTime(chunk.start_time)} → {formatTime(chunk.end_time)} +
+
+ ))} + + {hasMoreChunks && !showExpandedChunks && ( +
+ +
+ )} +
+
+ )} +
+ ); + }; + + if (!transcription) return null; + + return ( +
+
+

Full Transcription

+ + {/* Info tooltip */} +
+ setShowInfoTooltip(true)} + onMouseLeave={() => setShowInfoTooltip(false)} + /> + {showInfoTooltip && renderInfoTooltip()} +
+
+ +
+ {transcription.transcription} +
+
+ ); +}; + +export default FullTranscription; diff --git a/frontend/src/components/LanguageSelector.tsx b/frontend/src/components/LanguageSelector.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2bf1136086ee30a6960b674e31dbab98440aff95 --- /dev/null +++ b/frontend/src/components/LanguageSelector.tsx @@ -0,0 +1,262 @@ +import React, { useMemo, useState, useEffect } from 'react'; +import Select, { components, StylesConfig, type SingleValue } from 'react-select'; +import { matchSorter } from 'match-sorter'; +import { LANGUAGE_MAP, ACCURATE_LANGUAGES } from '../utils/languages'; +import { getScriptName, getScriptDescription } from '../utils/scripts'; +import { getSupportedLanguages } from '../services/transcriptionApi'; + +interface LanguageSelectorProps { + selectedLanguage: string | null; + selectedScript: string | null; + onLanguageAndScriptSelect: (language: string | null, script: string | null) => void; + disabled?: boolean; +} + +interface OptionType { + value: string; // The full code_script combination + label: string; + languageName: string; + scriptName: string; + languageCode: string; + scriptCode: string; +} + +const parseLanguage = (languageString: string): OptionType | null => { + const parts = languageString.split('_'); + + // Always expect format: "eng_Latn" + if (parts.length === 2) { + const [languageCode, scriptCode] = parts; + const languageName = (LANGUAGE_MAP as Record)[languageCode] || languageCode; + const scriptName = getScriptName(scriptCode); + + return { + value: languageString, + label: `${languageName} ${scriptName} (${languageString})`, + languageName, + scriptName, + languageCode, + scriptCode, + }; + } + + return null; +}; + +// Custom Option component to show language, script, and code with tooltip +const Option = (props: any) => { + const scriptDescription = getScriptDescription(props.data.scriptCode); + + return ( + +
+
{props.data.languageName}
+
{props.data.scriptName} ({props.data.value})
+
+
+ ); +}; + +// Custom SingleValue component for selected value +const SingleValue = (props: any) => ( + +
+
{props.data.languageName}
+
{props.data.scriptName} ({props.data.value})
+
+
+); + +// Custom styles to match the dark theme +const customStyles: StylesConfig = { + control: (styles, { isDisabled, isFocused }) => ({ + ...styles, + backgroundColor: isDisabled ? '#374151' : '#374151', // gray-700 + borderColor: isFocused ? '#3b82f6' : '#4b5563', // blue-500 : gray-600 + borderRadius: '0.375rem', + minHeight: '40px', + boxShadow: isFocused ? '0 0 0 1px #3b82f6' : 'none', + '&:hover': { + borderColor: isDisabled ? '#4b5563' : '#6b7280', // gray-600 : gray-500 + backgroundColor: isDisabled ? '#374151' : '#4b5563', // gray-700 : gray-600 + }, + cursor: isDisabled ? 'not-allowed' : 'pointer', + }), + singleValue: (styles) => ({ + ...styles, + color: '#f9fafb', // gray-50 + }), + placeholder: (styles) => ({ + ...styles, + color: '#9ca3af', // gray-400 + }), + input: (styles) => ({ + ...styles, + color: '#f9fafb', // gray-50 + }), + menu: (styles) => ({ + ...styles, + backgroundColor: '#374151', // gray-700 + border: '1px solid #4b5563', // gray-600 + borderRadius: '0.5rem', + boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', + zIndex: 50, + }), + menuList: (styles) => ({ + ...styles, + maxHeight: '200px', + padding: 0, + }), + option: (styles, { isFocused, isSelected }) => ({ + ...styles, + backgroundColor: isSelected + ? '#2563eb' // blue-600 + : isFocused + ? '#4b5563' // gray-600 + : 'transparent', + color: '#f9fafb', // gray-50 + cursor: 'pointer', + padding: '8px 12px', + '&:hover': { + backgroundColor: isSelected ? '#2563eb' : '#4b5563', // blue-600 : gray-600 + }, + }), + indicatorSeparator: (styles) => ({ + ...styles, + backgroundColor: '#4b5563', // gray-600 + }), + dropdownIndicator: (styles, { isDisabled }) => ({ + ...styles, + color: isDisabled ? '#6b7280' : '#9ca3af', // gray-500 : gray-400 + '&:hover': { + color: isDisabled ? '#6b7280' : '#d1d5db', // gray-500 : gray-300 + }, + }), + clearIndicator: (styles) => ({ + ...styles, + color: '#9ca3af', // gray-400 + '&:hover': { + color: '#d1d5db', // gray-300 + }, + }), + noOptionsMessage: (styles) => ({ + ...styles, + color: '#9ca3af', // gray-400 + }), +}; + +const LanguageSelector: React.FC = ({ + selectedLanguage, + selectedScript, + onLanguageAndScriptSelect, + disabled = false +}) => { + const [supportedLanguages, setSupportedLanguages] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + // Fetch supported languages from API + useEffect(() => { + const fetchSupportedLanguages = async () => { + try { + setIsLoading(true); + const languages = await getSupportedLanguages(); + setSupportedLanguages(languages); + } catch (err) { + console.error('Failed to fetch supported languages:', err); + setError('Failed to load supported languages'); + } finally { + setIsLoading(false); + } + }; + + fetchSupportedLanguages(); + }, []); + + // Convert supported languages to options + const languageOptions = useMemo(() => { + const allowAllLanguages = import.meta.env.VITE_ALLOW_ALL_LANGUAGES === 'true'; + + return supportedLanguages + .map(parseLanguage) + .filter((option): option is OptionType => option !== null) + .filter((option) => { + if (allowAllLanguages) { + return true; + } + return ACCURATE_LANGUAGES.includes(option.languageCode); + }) + .sort((a, b) => a.languageName.localeCompare(b.languageName)); + }, [supportedLanguages]); + + // Find the selected option + const selectedOption = useMemo(() => { + if (!selectedLanguage || !selectedScript) return null; + const combinedValue = `${selectedLanguage}_${selectedScript}`; + return languageOptions.find(option => option.value === combinedValue) || null; + }, [selectedLanguage, selectedScript, languageOptions]); + + const handleChange = (newValue: SingleValue) => { + if (newValue) { + onLanguageAndScriptSelect(newValue.languageCode, newValue.scriptCode); + } else { + onLanguageAndScriptSelect(null, null); + } + }; + + // Custom filterOption function using match-sorter + const filterOptions = useMemo(() => { + return (option: { label: string; value: string; data: OptionType }, inputValue: string) => { + if (!inputValue.trim()) return true; + + // Use match-sorter to check if this individual option matches + const matches = matchSorter([option.data], inputValue, { + keys: [ + 'languageName', // Primary: language name + 'scriptName', // Secondary: script name + 'languageCode', // Tertiary: language code + 'scriptCode', // Quaternary: script code + 'label', // Fallback: full label + ], + threshold: matchSorter.rankings.CONTAINS, + }); + + return matches.length > 0; + }; + }, []); + + if (error) { + return ( +
+ {error} +
+ ); + } + + return ( + + value={selectedOption} + onChange={handleChange} + options={languageOptions} + placeholder={isLoading ? "Loading languages..." : "Select language..."} + isClearable + isDisabled={disabled || isLoading} + isSearchable + filterOption={(option, inputValue) => filterOptions(option, inputValue)} + components={{ Option, SingleValue }} + styles={customStyles} + menuPortalTarget={document.body} + menuPosition="fixed" + noOptionsMessage={({ inputValue }) => + `No languages found matching "${inputValue}"` + } + // Performance optimizations + menuIsOpen={undefined} // Let react-select manage this + blurInputOnSelect={true} + closeMenuOnSelect={true} + hideSelectedOptions={false} + /> + ); +}; + +export default LanguageSelector; diff --git a/frontend/src/components/MediaDownloadControls.tsx b/frontend/src/components/MediaDownloadControls.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5535540184ee8f597c9cb20ce2e75dbfbb7531e2 --- /dev/null +++ b/frontend/src/components/MediaDownloadControls.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; +import { useTranscriptionStore } from '../stores/transcriptionStore'; +import { generateSRT, generateWebVTT, downloadSubtitles } from '../utils/subtitleUtils'; +import { trackDownloadSRT, sendGAEvent } from '../analytics/gaEvents'; + +const MediaDownloadControls: React.FC = () => { + const { + file, + transcription, + selectedLanguage, + isVideoFile, + isDownloadingVideo, + handleDownloadVideoWithSubtitles, + } = useTranscriptionStore(); + + if (!transcription) { + return null; + } + + return ( +
+
+ +
+
+ +
+ {isVideoFile && ( +
+ +
+ )} +
+ ); +}; + +export default MediaDownloadControls; diff --git a/frontend/src/components/MediaEditControls.tsx b/frontend/src/components/MediaEditControls.tsx new file mode 100644 index 0000000000000000000000000000000000000000..33dcf107378422563f44ca4d29effd1775c11c57 --- /dev/null +++ b/frontend/src/components/MediaEditControls.tsx @@ -0,0 +1,113 @@ +import React, { useState, useEffect } from 'react'; +import { InformationCircleIcon } from '@heroicons/react/24/outline'; +import { useTranscriptionStore } from '../stores/transcriptionStore'; +import { formatTime } from '../utils/subtitleUtils'; + +const DEFAULT_MERGE_THRESHOLD = 2; + +const MediaEditControls: React.FC = () => { + const [mergeThreshold, setMergeThreshold] = useState(DEFAULT_MERGE_THRESHOLD); + + const { + transcription, + currentTime, + activeSegmentIndex, + currentSegments, + undo, + redo, + canUndo, + canRedo, + mergeSegmentsByProximity, + } = useTranscriptionStore(); + + const MAX_MERGE_INTERVAL_SECONDS = 30; + + if (!transcription) { + return null; + } + + const displaySegments = currentSegments || transcription.aligned_segments; + + // Handle merge threshold changes + useEffect(() => { + mergeSegmentsByProximity(mergeThreshold); + }, [mergeThreshold, mergeSegmentsByProximity]); + + // Keyboard shortcuts for undo/redo + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) { + e.preventDefault(); + undo(); + } else if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) { + e.preventDefault(); + redo(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [undo, redo]); + + return ( +
+
+ {/* Current Status Info */} +
+ + {formatTime(currentTime)} / {formatTime(transcription.total_duration)} + + + {activeSegmentIndex !== null + ? `Segment ${activeSegmentIndex + 1}/${displaySegments.length}` + : "No active segment"} + +
+ + {/* Combine Segments Slider */} +
+ + setMergeThreshold(Number(e.target.value))} + className="w-20 h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer slider" + style={{ + background: `linear-gradient(to right, #3B82F6 0%, #3B82F6 ${(mergeThreshold / MAX_MERGE_INTERVAL_SECONDS) * 100}%, #4B5563 ${(mergeThreshold / MAX_MERGE_INTERVAL_SECONDS) * 100}%, #4B5563 100%)` + }} + /> +
+ + {/* Undo/Redo Buttons */} +
+ + +
+
+
+ ); +}; + +export default MediaEditControls; diff --git a/frontend/src/components/MediaPlayer.tsx b/frontend/src/components/MediaPlayer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..295f586556a8a2e1242e4e0623109147cfe3a038 --- /dev/null +++ b/frontend/src/components/MediaPlayer.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import { useTranscriptionStore } from '../stores/transcriptionStore'; +import { generateWebVTT } from '../utils/subtitleUtils'; +import {LANGUAGE_MAP} from '../utils/languages'; + +interface MediaPlayerProps { + audioRef: React.RefObject; + videoRef: React.RefObject; + onTimeUpdate?: () => void; +} + +export default function MediaPlayer({ + audioRef, + videoRef, + onTimeUpdate, +}: MediaPlayerProps) { + const { + file, + mediaUrl, + isVideoFile, + currentSegments, + selectedLanguage, + setCurrentTime + } = useTranscriptionStore(); + + const handleSeeked = (event: React.SyntheticEvent) => { + const target = event.target as HTMLMediaElement; + setCurrentTime(target.currentTime); + // Call onTimeUpdate to trigger segment selection logic + if (onTimeUpdate) { + onTimeUpdate(); + } + }; + + const handleLoadedMetadata = (event: React.SyntheticEvent) => { + const target = event.target as HTMLMediaElement; + setCurrentTime(target.currentTime); + + // Call onTimeUpdate to trigger segment selection logic + if (onTimeUpdate) { + onTimeUpdate(); + } + }; + + // Helper function to encode UTF-8 string to base64 + const utf8ToBase64 = (str: string): string => { + // Convert string to UTF-8 bytes, then to base64 + const encoder = new TextEncoder(); + const bytes = encoder.encode(str); + let binary = ''; + bytes.forEach(byte => binary += String.fromCharCode(byte)); + return btoa(binary); + }; + + // Get language info for subtitles + const getLanguageInfo = () => { + if (!selectedLanguage) { + return { code: 'en', name: 'English' }; + } + const languageName = (LANGUAGE_MAP as Record)[selectedLanguage]; + return { + code: selectedLanguage, + name: languageName || 'Unknown' + }; + }; + + // Early return if no file is selected + if (!file) { + return null; + } + + // Early return if no media URL is available + if (!mediaUrl) { + return ( +
+
+ Loading media... +
+
+ ); + } + + return ( +
+
+ {isVideoFile ? ( + + ) : ( +
+
+ )} +
+
+ ); +} diff --git a/frontend/src/components/MediaRecorder.tsx b/frontend/src/components/MediaRecorder.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c71f89d756491f574f49b799b1ce6b74acc377e6 --- /dev/null +++ b/frontend/src/components/MediaRecorder.tsx @@ -0,0 +1,353 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { useTranscriptionStore } from '../stores/transcriptionStore'; +import { useAudioAnalyzer } from '../hooks/useAudioAnalyzer'; + +interface MediaRecorderProps { + onComplete: () => void; + onCancel: () => void; +} + +const MediaRecorder: React.FC = ({ onComplete, onCancel }) => { + const { recordingType, setRecordedBlob } = useTranscriptionStore(); + + const [isRecording, setIsRecording] = useState(false); + const [recordingTime, setRecordingTime] = useState(0); + const [stream, setStream] = useState(null); + const [error, setError] = useState(null); + const [permissionState, setPermissionState] = useState<'prompt' | 'granted' | 'denied'>('prompt'); + const [currentMicrophone, setCurrentMicrophone] = useState(null); + + const mediaRecorderRef = useRef(null); + const videoRef = useRef(null); + const chunksRef = useRef([]); + const timerRef = useRef(null); + + const isVideo = recordingType === 'video'; + + // Audio analyzer for real-time waveform + const { audioData, connectToStream, disconnect } = useAudioAnalyzer(256); + + // Get microphone device info + const getMicrophoneInfo = async (mediaStream: MediaStream) => { + try { + // Get all available audio input devices + const devices = await navigator.mediaDevices.enumerateDevices(); + const audioInputDevices = devices.filter(device => device.kind === 'audioinput'); + + // Get the audio track from the current stream + const audioTrack = mediaStream.getAudioTracks()[0]; + + if (audioTrack) { + // Get the device settings + const settings = audioTrack.getSettings(); + const deviceId = settings.deviceId; + + // Find the matching device in our list + const currentDevice = audioInputDevices.find(device => device.deviceId === deviceId); + + if (currentDevice && currentDevice.label) { + setCurrentMicrophone(currentDevice.label); + } else { + // Fallback to device ID if label is not available + setCurrentMicrophone(`Microphone (${deviceId?.substring(0, 8)}...)`); + } + } + } catch (err) { + console.error('Error getting microphone info:', err); + setCurrentMicrophone('Unknown microphone'); + } + }; + + // Request permissions and setup media stream + const requestPermissions = async () => { + try { + setError(null); + + const constraints: MediaStreamConstraints = { + audio: { + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true, + }, + video: isVideo ? { + width: { ideal: 1280 }, + height: { ideal: 720 }, + facingMode: 'user' + } : false + }; + + const mediaStream = await navigator.mediaDevices.getUserMedia(constraints); + setStream(mediaStream); + setPermissionState('granted'); + + // Get microphone device information + await getMicrophoneInfo(mediaStream); + + // Show video preview if recording video + if (isVideo && videoRef.current) { + videoRef.current.srcObject = mediaStream; + videoRef.current.play(); + } + + // Connect audio analyzer for waveform visualization + connectToStream(mediaStream); + + } catch (err) { + console.error('Error accessing media devices:', err); + setPermissionState('denied'); + + if (err instanceof DOMException) { + switch (err.name) { + case 'NotAllowedError': + setError('Permission denied. Please allow access to your microphone' + (isVideo ? ' and camera' : '') + '.'); + break; + case 'NotFoundError': + setError('No ' + (isVideo ? 'camera or ' : '') + 'microphone found.'); + break; + case 'NotReadableError': + setError('Media device is already in use by another application.'); + break; + default: + setError('Failed to access media devices: ' + err.message); + } + } else { + setError('An unexpected error occurred while accessing media devices.'); + } + } + }; + + // Start recording + const startRecording = () => { + if (!stream) return; + + try { + chunksRef.current = []; + + // Try different MIME types in order of preference + const mimeTypes = isVideo + ? ['video/webm;codecs=vp9,opus', 'video/webm;codecs=vp8,opus', 'video/webm'] + : ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4', '']; + + let selectedMimeType = ''; + for (const mimeType of mimeTypes) { + if (mimeType === '' || window.MediaRecorder.isTypeSupported(mimeType)) { + selectedMimeType = mimeType; + break; + } + } + + const options: MediaRecorderOptions = selectedMimeType ? { mimeType: selectedMimeType } : {}; + const mediaRecorder = new window.MediaRecorder(stream, options); + mediaRecorderRef.current = mediaRecorder; + + mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + chunksRef.current.push(event.data); + } + }; + + mediaRecorder.onstop = () => { + const blob = new Blob(chunksRef.current, { + type: isVideo ? 'video/webm' : 'audio/webm' + }); + + setRecordedBlob(blob); + onComplete(); + }; + + mediaRecorder.start(); + setIsRecording(true); + setRecordingTime(0); + + // Start timer + timerRef.current = setInterval(() => { + setRecordingTime(prev => prev + 1); + }, 1000); + + } catch (err) { + console.error('Error starting recording:', err); + setError('Failed to start recording: ' + (err instanceof Error ? err.message : 'Unknown error')); + } + }; + + // Stop recording + const stopRecording = () => { + if (mediaRecorderRef.current && isRecording) { + mediaRecorderRef.current.stop(); + setIsRecording(false); + + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + } + }; + + // Format recording time + const formatTime = (seconds: number) => { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + }; + + // Cleanup on unmount and when recording stops + useEffect(() => { + return () => { + if (stream) { + stream.getTracks().forEach(track => track.stop()); + } + if (timerRef.current) { + clearInterval(timerRef.current); + } + }; + }, [stream]); + + // Cleanup stream when recording stops externally + useEffect(() => { + if (!recordingType && stream) { + stream.getTracks().forEach(track => track.stop()); + setStream(null); + setCurrentMicrophone(null); // Clear microphone info + disconnect(); // Also disconnect audio analyzer + } + }, [recordingType, stream, disconnect]); + + // Auto-request permissions when component mounts + useEffect(() => { + if (permissionState === 'prompt') { + requestPermissions(); + } + }, []); + + return ( +
+ + {/* Header */} +
+

+ Record {isVideo ? 'Video' : 'Audio'} +

+

+ {permissionState === 'prompt' && 'Requesting permissions...'} + {permissionState === 'denied' && 'Permission required to record'} + {permissionState === 'granted' && !isRecording && 'Ready to record'} + {isRecording && `Recording... ${formatTime(recordingTime)}`} +

+ + {/* Microphone Device Info */} + {permissionState === 'granted' && currentMicrophone && ( +
+ + + + + {currentMicrophone} + +
+ )} +
+ + {/* Video Preview (only for video recording) */} + {isVideo && permissionState === 'granted' && ( +
+
+ )} + + {/* Audio Visualization */} + {permissionState === 'granted' && ( +
+
+
+ {/* Real-time audio visualization bars */} + {Array.from({ length: 32 }, (_, i) => { + // Use a wider frequency range for better distribution + // Map across 60% of the frequency spectrum for voice and some harmonics + const voiceRangeEnd = Math.floor(audioData.length * 0.6); + const dataIndex = Math.floor((i / 32) * voiceRangeEnd); + const amplitude = audioData[dataIndex] || 0; + + // Apply logarithmic scaling to prevent saturation and better distribute levels + const normalizedAmplitude = amplitude / 255; + const logScaled = Math.log10(1 + normalizedAmplitude * 9) / Math.log10(10); // Log scale 0-1 + const height = Math.max(4, logScaled * 60); // Scale to 4-60px + + return ( +
+ ); + })} +
+
+
+ )} + + {/* Error Display */} + {error && ( +
+

{error}

+
+ )} + + {/* Controls */} +
+ {permissionState === 'denied' && ( + + )} + + {permissionState === 'granted' && !isRecording && ( + + )} + + {isRecording && ( + + )} + + +
+ + {/* Tips */} +
+

+ Speak clearly and minimize background noise for best transcription results. +

+
+
+ ); +}; + +export default MediaRecorder; diff --git a/frontend/src/components/MinimapTimeline.tsx b/frontend/src/components/MinimapTimeline.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2e1114ee8b3b2a3288b1352bdb3a4ed15a3351a6 --- /dev/null +++ b/frontend/src/components/MinimapTimeline.tsx @@ -0,0 +1,509 @@ +import React, { useRef, useEffect, useState, useCallback } from 'react'; +import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; +import { useTranscriptionStore } from '../stores/transcriptionStore'; + +interface MinimapTimelineProps { + audioRef: React.RefObject; + videoRef: React.RefObject; + canvasTimelineRef: React.RefObject; // Container that scrolls +} + +export default function MinimapTimeline({ + audioRef, + videoRef, + canvasTimelineRef +}: MinimapTimelineProps) { + const canvasRef = useRef(null); + const containerRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const [dragStartX, setDragStartX] = useState(0); + const [dragStartScrollLeft, setDragStartScrollLeft] = useState(0); + const [waveformData, setWaveformData] = useState([]); + const [viewport, setViewport] = useState({ start: 0, end: 30, visible: false }); + + const { + transcription, + preprocessedAudio, + currentTime, + } = useTranscriptionStore(); + + // Constants + const MINIMAP_HEIGHT = 80; + const PIXELS_PER_SECOND = 300; // Match the CanvasTimeline scaling + + // Get media duration + const getMediaDuration = useCallback(() => { + const audioElement = audioRef.current; + const videoElement = videoRef.current; + + if (audioElement && !isNaN(audioElement.duration)) { + return audioElement.duration; + } + if (videoElement && !isNaN(videoElement.duration)) { + return videoElement.duration; + } + + return transcription?.total_duration || 0; + }, [audioRef, videoRef, transcription]); + + const mediaDuration = getMediaDuration(); + + // Canvas width based on container + const [canvasWidth, setCanvasWidth] = useState(800); + + // Update canvas width on resize + useEffect(() => { + const updateCanvasWidth = () => { + if (containerRef.current) { + setCanvasWidth(containerRef.current.clientWidth); + } + }; + + updateCanvasWidth(); + window.addEventListener('resize', updateCanvasWidth); + return () => window.removeEventListener('resize', updateCanvasWidth); + }, []); + + // Track Canvas Timeline scroll position and calculate viewport + const updateViewportFromScroll = useCallback(() => { + const canvasContainer = canvasTimelineRef.current; + if (!canvasContainer || mediaDuration === 0) return; + + const scrollLeft = canvasContainer.scrollLeft; + const containerWidth = canvasContainer.clientWidth; + const totalCanvasWidth = mediaDuration * PIXELS_PER_SECOND; + + // Calculate what time range is currently visible + const startTime = (scrollLeft / totalCanvasWidth) * mediaDuration; + const endTime = ((scrollLeft + containerWidth) / totalCanvasWidth) * mediaDuration; + + setViewport({ + start: Math.max(0, startTime), + end: Math.min(mediaDuration, endTime), + visible: true + }); + }, [canvasTimelineRef, mediaDuration]); + + // Listen for scroll events on the Canvas Timeline container + useEffect(() => { + const canvasContainer = canvasTimelineRef.current; + if (!canvasContainer) return; + + const handleScroll = () => { + updateViewportFromScroll(); + }; + + const handleLoadOrResize = () => { + // Update viewport when container size changes + updateViewportFromScroll(); + }; + + // Initial viewport calculation + updateViewportFromScroll(); + + canvasContainer.addEventListener('scroll', handleScroll); + window.addEventListener('resize', handleLoadOrResize); + + return () => { + canvasContainer.removeEventListener('scroll', handleScroll); + window.removeEventListener('resize', handleLoadOrResize); + }; + }, [updateViewportFromScroll]); + + // Generate waveform data from preprocessed audio + const generateWaveformFromPreprocessedAudio = useCallback(async () => { + if (!preprocessedAudio?.data) { + console.log('No preprocessed audio data available'); + return; + } + + try { + console.log('Generating waveform from preprocessed audio data'); + + // Decode base64 audio data + const audioBytes = atob(preprocessedAudio.data); + const audioArrayBuffer = new ArrayBuffer(audioBytes.length); + const audioUint8Array = new Uint8Array(audioArrayBuffer); + + for (let i = 0; i < audioBytes.length; i++) { + audioUint8Array[i] = audioBytes.charCodeAt(i); + } + + // Create audio context and decode the WAV data + const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); + const audioBuffer = await audioContext.decodeAudioData(audioArrayBuffer); + + // Extract audio data + const channelData = audioBuffer.getChannelData(0); + const samples = Math.min(800, canvasWidth); // Limit samples for performance + const blockSize = Math.floor(channelData.length / samples); + + const waveform: number[] = []; + for (let i = 0; i < samples; i++) { + const start = i * blockSize; + const end = Math.min(start + blockSize, channelData.length); + + let sum = 0; + for (let j = start; j < end; j++) { + sum += Math.abs(channelData[j]); + } + + waveform.push(sum / (end - start)); + } + + // Normalize waveform + const max = Math.max(...waveform); + const normalizedWaveform = max > 0 ? waveform.map(val => val / max) : waveform; + + setWaveformData(normalizedWaveform); + console.log(`Generated waveform with ${normalizedWaveform.length} samples from preprocessed audio`); + + } catch (error) { + console.error('Error generating waveform from preprocessed audio:', error); + // Fallback to segment-based visualization + generateFallbackWaveform(); + } + }, [preprocessedAudio, canvasWidth]); + + // Fallback waveform generation from segment data + const generateFallbackWaveform = useCallback(() => { + if (!transcription?.aligned_segments || mediaDuration === 0) return; + + console.log('Using fallback waveform generation from segments'); + const segments = transcription.aligned_segments; + const samples = Math.min(400, canvasWidth / 2); + const bars = new Array(samples).fill(0); + + // Create waveform based on speech activity in segments + segments.forEach(segment => { + const startIndex = Math.floor((segment.start / mediaDuration) * samples); + const endIndex = Math.ceil((segment.end / mediaDuration) * samples); + + for (let i = startIndex; i < Math.min(endIndex, samples); i++) { + // Use segment text length and duration to estimate intensity + const intensity = Math.min(1.0, segment.text.length / 50 + 0.3); + bars[i] = Math.max(bars[i], intensity * (0.7 + Math.random() * 0.3)); + } + }); + + setWaveformData(bars); + console.log(`Generated fallback waveform with ${bars.length} samples`); + }, [transcription, mediaDuration, canvasWidth]); + + // Generate waveform when preprocessed audio becomes available + useEffect(() => { + if (preprocessedAudio?.data) { + generateWaveformFromPreprocessedAudio(); + } else if (transcription?.aligned_segments) { + // Use fallback if we have segments but no preprocessed audio + generateFallbackWaveform(); + } + }, [preprocessedAudio, generateWaveformFromPreprocessedAudio, generateFallbackWaveform]); + + // Draw the minimap + const draw = useCallback(() => { + const canvas = canvasRef.current; + if (!canvas || mediaDuration === 0) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const { width, height } = canvas; + + // Clear canvas + ctx.clearRect(0, 0, width, height); + + // Draw background + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(0, 0, width, height); + + // Draw waveform + if (waveformData.length > 0) { + ctx.fillStyle = '#4a5568'; + const barWidth = width / waveformData.length; + + waveformData.forEach((amplitude, index) => { + const barHeight = amplitude * (height - 20); + const x = index * barWidth; + const y = (height - barHeight) / 2; + + ctx.fillRect(x, y, Math.max(1, barWidth - 1), barHeight); + }); + } + + // Draw segments as colored bars + if (transcription?.aligned_segments) { + transcription.aligned_segments.forEach((segment, index) => { + const startX = (segment.start / mediaDuration) * width; + const endX = (segment.end / mediaDuration) * width; + const segmentWidth = endX - startX; + + // Alternate colors for segments + ctx.fillStyle = index % 2 === 0 ? '#3182ce' : '#38a169'; + ctx.fillRect(startX, height - 4, segmentWidth, 4); + }); + } + + // Draw current time indicator + const currentTimeX = (currentTime / mediaDuration) * width; + ctx.strokeStyle = '#f56565'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(currentTimeX, 0); + ctx.lineTo(currentTimeX, height); + ctx.stroke(); + + // Draw viewport region (what's visible in Canvas Timeline) + if (viewport.visible) { + const viewportStartX = (viewport.start / mediaDuration) * width; + const viewportEndX = (viewport.end / mediaDuration) * width; + + // Draw viewport selection area (visible region highlight) + ctx.fillStyle = 'rgba(66, 153, 225, 0.3)'; + ctx.fillRect(viewportStartX, 0, viewportEndX - viewportStartX, height); + + // Draw left boundary line (start of visible area) + ctx.strokeStyle = '#4299e1'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.moveTo(viewportStartX, 0); + ctx.lineTo(viewportStartX, height); + ctx.stroke(); + + // Draw right boundary line (end of visible area) + ctx.beginPath(); + ctx.moveTo(viewportEndX, 0); + ctx.lineTo(viewportEndX, height); + ctx.stroke(); + + // Draw border around visible area + ctx.strokeStyle = '#4299e1'; + ctx.lineWidth = 1; + ctx.strokeRect(viewportStartX, 0, viewportEndX - viewportStartX, height); + } + }, [waveformData, transcription, currentTime, viewport, mediaDuration]); + + // Update canvas size and redraw + useEffect(() => { + const canvas = canvasRef.current; + if (canvas) { + canvas.width = canvasWidth; + canvas.height = MINIMAP_HEIGHT; + draw(); + } + }, [canvasWidth, draw]); + + // Redraw when dependencies change + useEffect(() => { + draw(); + }, [draw]); + + // Utility function to get time from X coordinate + const getTimeFromX = useCallback((x: number) => { + return (x / canvasWidth) * mediaDuration; + }, [canvasWidth, mediaDuration]); + + // Check if clicking inside the viewport region + const isClickingViewport = useCallback((x: number) => { + if (!viewport.visible) return false; + + const viewportStartX = (viewport.start / mediaDuration) * canvasWidth; + const viewportEndX = (viewport.end / mediaDuration) * canvasWidth; + + return x >= viewportStartX && x <= viewportEndX; + }, [viewport, mediaDuration, canvasWidth]); + + // Scroll Canvas Timeline to show specific time + const scrollToTime = useCallback((time: number) => { + const canvasContainer = canvasTimelineRef.current; + if (!canvasContainer) return; + + const totalCanvasWidth = mediaDuration * PIXELS_PER_SECOND; + const targetScrollLeft = Math.max(0, (time / mediaDuration) * totalCanvasWidth); + + canvasContainer.scrollLeft = targetScrollLeft; + }, [canvasTimelineRef, mediaDuration]); + + // Mouse event handlers + const handleMouseDown = useCallback((e: React.MouseEvent) => { + const rect = canvasRef.current?.getBoundingClientRect(); + if (!rect) return; + + const x = e.clientX - rect.left; + + if (isClickingViewport(x)) { + // Start dragging the viewport + setIsDragging(true); + setDragStartX(x); + const canvasContainer = canvasTimelineRef.current; + if (canvasContainer) { + setDragStartScrollLeft(canvasContainer.scrollLeft); + } + } else { + // Click outside viewport - jump to that position + const clickTime = getTimeFromX(x); + scrollToTime(clickTime); + } + }, [isClickingViewport, canvasTimelineRef, getTimeFromX, scrollToTime]); + + const handleMouseMove = useCallback((e: React.MouseEvent) => { + if (!isDragging) return; + + const rect = canvasRef.current?.getBoundingClientRect(); + if (!rect) return; + + const x = e.clientX - rect.left; + const deltaX = x - dragStartX; + + const canvasContainer = canvasTimelineRef.current; + if (!canvasContainer) return; + + // Convert deltaX to scroll delta + const totalCanvasWidth = mediaDuration * PIXELS_PER_SECOND; + const scrollDelta = (deltaX / canvasWidth) * totalCanvasWidth; + + const newScrollLeft = Math.max(0, Math.min( + dragStartScrollLeft + scrollDelta, + canvasContainer.scrollWidth - canvasContainer.clientWidth + )); + + canvasContainer.scrollLeft = newScrollLeft; + }, [isDragging, dragStartX, dragStartScrollLeft, canvasTimelineRef, mediaDuration, canvasWidth]); + + const handleMouseUp = useCallback(() => { + setIsDragging(false); + }, []); + + // Add global mouse event listeners when dragging + useEffect(() => { + if (isDragging) { + const handleGlobalMouseMove = (e: MouseEvent) => { + handleMouseMove(e as any); + }; + const handleGlobalMouseUp = () => { + handleMouseUp(); + }; + + document.addEventListener('mousemove', handleGlobalMouseMove); + document.addEventListener('mouseup', handleGlobalMouseUp); + + return () => { + document.removeEventListener('mousemove', handleGlobalMouseMove); + document.removeEventListener('mouseup', handleGlobalMouseUp); + }; + } + }, [isDragging, handleMouseMove, handleMouseUp]); + + // Change cursor based on hover position + const handleMouseHover = useCallback((e: React.MouseEvent) => { + if (isDragging) return; + + const rect = canvasRef.current?.getBoundingClientRect(); + if (!rect) return; + + const x = e.clientX - rect.left; + const canvas = canvasRef.current; + if (!canvas) return; + + if (isClickingViewport(x)) { + canvas.style.cursor = 'move'; + } else { + canvas.style.cursor = 'pointer'; + } + }, [isDragging, isClickingViewport]); + + // Download preprocessed audio as WAV file + const downloadPreprocessedAudio = useCallback(() => { + if (!preprocessedAudio?.data) { + console.error('No preprocessed audio data available'); + return; + } + + try { + // Decode base64 audio data + const audioBytes = atob(preprocessedAudio.data); + const audioArrayBuffer = new ArrayBuffer(audioBytes.length); + const audioUint8Array = new Uint8Array(audioArrayBuffer); + + for (let i = 0; i < audioBytes.length; i++) { + audioUint8Array[i] = audioBytes.charCodeAt(i); + } + + // Create blob and download + const blob = new Blob([audioUint8Array], { type: 'audio/wav' }); + const url = URL.createObjectURL(blob); + + // Get original filename without extension + const { file } = useTranscriptionStore.getState(); + const originalName = file?.name?.replace(/\.[^/.]+$/, '') || 'audio'; + const filename = `${originalName}_preprocessed_16khz_mono_normalized.wav`; + + // Create download link + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Clean up URL + URL.revokeObjectURL(url); + + console.log(`Downloaded preprocessed audio: ${filename}`); + } catch (error) { + console.error('Error downloading preprocessed audio:', error); + } + }, [preprocessedAudio]); + + if (!transcription || mediaDuration === 0) { + return null; + } + + return ( +
+
+
+
+ + Overview - Full Timeline ({Math.round(mediaDuration)}s) + {preprocessedAudio ? ' â€ĸ Preprocessed Waveform' : ' â€ĸ Segment-Based View'} + + {preprocessedAudio && ( +
+ +
+ )} +
+ {viewport.visible && ( + + Visible: {viewport.start.toFixed(1)}s - {viewport.end.toFixed(1)}s + ({Math.round(viewport.end - viewport.start)}s view) + + )} +
+
+ +
+
+
+ ); +} diff --git a/frontend/src/components/QuickGuide.tsx b/frontend/src/components/QuickGuide.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2b74b95d592bcb509d30acb793cc0d332e8e06d5 --- /dev/null +++ b/frontend/src/components/QuickGuide.tsx @@ -0,0 +1,176 @@ +import React from "react"; +import {useTranscriptionStore} from "../stores/transcriptionStore"; + +interface QuickGuideProps { + currentStep?: string; // Optional override for current step +} + +type GuideStep = { + id: string; + text: string; + icon?: string; + isActive: (state: any) => boolean; + isCompleted: (state: any) => boolean; +}; + +const QuickGuide: React.FC = ({currentStep}) => { + const { + file, + transcription, + isLoading, + selectedSegmentIndex, + currentSegments, + currentTime + } = useTranscriptionStore(); + + // Define all the steps with their conditions + const steps: GuideStep[] = [ + { + id: "upload", + text: "Upload or record audio", + icon: "📁", + isActive: (state) => !state.file, + isCompleted: (state) => !!state.file, + }, + { + id: "transcribe", + text: "Click transcribe to process", + icon: "đŸŽ¯", + isActive: (state) => + !!state.file && !state.transcription && !state.isLoading, + isCompleted: (state) => !!state.transcription || state.isLoading, + }, + { + id: "play", + text: "Play media", + icon: "â–ļī¸", + isActive: (state) => !!state.transcription, + isCompleted: () => (currentTime ?? 0) > 0, // Always in progress when transcription available + }, + // { + // id: "jump", + // text: "Click segments to jump", + // icon: "đŸŽĩ", + // isActive: (state) => !!state.transcription, + // isCompleted: () => false, // Always in progress when transcription available + // }, + // { + // id: "drag", + // text: "Drag segments to move/resize", + // icon: "â†”ī¸", + // isActive: (state) => !!state.transcription, + // isCompleted: () => false, // Always in progress when transcription available + // }, + // { + // id: "combine", + // text: "Use slider to combine segments", + // icon: "🔗", + // isActive: (state) => !!state.transcription, + // isCompleted: () => false, // Always in progress when transcription available + // }, + // { + // id: "download", + // text: "Download subtitles", + // icon: "💾", + // isActive: (state) => !!state.transcription, + // isCompleted: () => false, // Always in progress when transcription available + // }, + ]; + + // Create state object for condition checking + const storeState = { + file, + transcription, + isLoading, + selectedSegmentIndex, + currentSegments, + }; + + // Determine step states + const getStepState = (step: GuideStep) => { + // Override with currentStep prop if provided + if (currentStep) { + if (step.id === currentStep) return "active"; + if (step.isCompleted(storeState)) return "completed"; + return "inactive"; + } + + // Default logic based on store state + if (step.isCompleted(storeState)) return "completed"; + if (step.isActive(storeState)) return "active"; + return "inactive"; + }; + + // Get the appropriate CSS classes for each step state + const getStepClasses = (stepState: string) => { + switch (stepState) { + case "active": + return "text-blue-300 bg-blue-900/30 border-blue-500/50 font-medium"; + case "completed": + return "text-green-300 bg-green-900/20 border-green-500/30"; + default: + return "text-gray-400 bg-transparent border-transparent"; + } + }; + + // Get icon for step state + const getStepIcon = (step: GuideStep, stepState: string) => { + if (stepState === "completed") return "✓"; + if (stepState === "active") return "→"; + return step.icon || "â€ĸ"; + }; + + return ( +
+

Quick Guide

+
+ {steps.map((step) => { + const stepState = getStepState(step); + const stepClasses = getStepClasses(stepState); + const icon = getStepIcon(step, stepState); + + return ( +
+ + {icon} + + {step.text} +
+ ); + })} +
+ + {/* Progress indicator */} + {transcription && ( +
+
+ {selectedSegmentIndex !== null ? ( + âœī¸ Editing mode active + ) : ( + + ✓ Ready for playback & editing + + )} +
+
+ )} + + {/* Loading indicator */} + {isLoading && ( +
+
+ âŗ Processing... Please wait +
+
+ )} +
+ ); +}; + +export default QuickGuide; diff --git a/frontend/src/components/SegmentEditor.tsx b/frontend/src/components/SegmentEditor.tsx new file mode 100644 index 0000000000000000000000000000000000000000..42a255e390b95c88557f9aaeb94da9ea0b0144a0 --- /dev/null +++ b/frontend/src/components/SegmentEditor.tsx @@ -0,0 +1,92 @@ +import React, { useEffect, useRef } from 'react'; +import { AlignedSegment } from '../services/transcriptionApi'; +import { formatTime } from '../utils/subtitleUtils'; + +interface SegmentEditorProps { + segment: AlignedSegment; + segmentIndex: number; + onUpdateText: (index: number, text: string) => void; + onDeleteSegment: (index: number) => void; + onClose: () => void; +} + +export default function SegmentEditor({ + segment, + segmentIndex, + onUpdateText, + onDeleteSegment, + onClose, +}: SegmentEditorProps) { + const textareaRef = useRef(null); + + useEffect(() => { + // Focus the textarea when component mounts + if (textareaRef.current) { + textareaRef.current.focus(); + textareaRef.current.select(); + } + }, []); + + const handleTextChange = (e: React.ChangeEvent) => { + const newText = e.target.value; + onUpdateText(segmentIndex, newText); + }; + + const handleDelete = () => { + onDeleteSegment(segmentIndex); + onClose(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + onClose(); + } + }; + + return ( +
+
+
+

+ Edit Segment #{segmentIndex + 1} +

+
+ {formatTime(segment.start)} - {formatTime(segment.end)} ({segment.duration.toFixed(1)}s) +
+
+ +
+ +
+