diff --git a/.gitignore b/.gitignore index 850c823f..5d8a3c2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # dependencies /node_modules +/**/node_modules /.pnp .pnp.js @@ -8,6 +9,7 @@ # next.js /.next/ +/**/.next/ /out/ # production @@ -26,6 +28,10 @@ yarn-error.log* .env*.local .env +IDE Files +.idx +.idx/* */ + # vercel .vercel @@ -35,3 +41,5 @@ next-env.d.ts package-lock.json +# Dynamically generated files +cloudbuild.yaml diff --git a/README.md b/README.md index 93435229..fb844964 100644 --- a/README.md +++ b/README.md @@ -1,226 +1,133 @@ -# Infrastructure setup guide for ImgStudio - -## 0\\ Get access to **Imagen models** - -- **In general, for Vertex:** in the console - - Go to `Vertex AI` \> `Enable all recommended APIs` (they should include: **Vertex AI API, Cloud Storage API)** - - Make sure the Vertex Service Account exists in your project `service-PROJECT_NUMBER@gcp-sa-aiplatform.iam.gserviceaccount.com` -- **For Imagen Generation:** - - Models are now in public GA, **Imagen 3 Generate** (`imagen-3.0-generate-001`) and **Imagen 3 Generate Fast** (`imagen-3.0-fast-generate-001`) - - **For people generation** (adult and/ or children), you now need to fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSduBp9w84qgim6vLriQ9p7sdz62bMJaL-nNmIVoyiOwd84SMw/viewform) to get access. -- **For Imagen Editing & Customization** - - You can fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLScN9KOtbuwnEh6pV7xjxib5up5kG_uPqnBtJ8GcubZ6M3i5Cw/viewform) to get access to the Preview feature (name: `imagen-3.0-capability-001`) - - You will also need the **Vertex Image Segmentation model** when using editing, fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSdzIR1EeQGFcMsqd9nPip5e9ovDKSjfWRd58QVjo1zLpfdvEg/viewform?resourcekey=0-Pvqc66u-0Z1QmuzHq4wLKg&pli=1) to get access (name: `image-segmentation-001`) - -## 1\\ Create **Cloud Storage** buckets - -- **Specifications:** Regional in your desired region (ex: `europe-west9` in Paris) -- **Create 3 buckets** - - **Raw generated output content**: `YOUR_COMPANY-imgstudio-output` - - **Shared content**: `YOUR_COMPANY-imgstudio-library` - - **Configuration file bucket**: `YOUR_COMPANY-imgstudio-export-config` - - Here upload `export-fields-options.json` a **configuration file specific** to your usage that you can find an [exemple](https://github.com/aduboue/img-studio/blob/main/export-fields-options.json) of in the [repository](https://github.com/aduboue/img-studio), its purpose is to setup the desired **metadata** you want to set for your generated content - - In this file, for each **fields** (ex: contextAuthorTeam, contextTargetPlatform, contextAssociatedBrand, contextCollection), you can only change the **ID** of the field (ex: `“contextAuthorTeam”`), its **label** (ex: `“In which team are you?”`), its **name** (ex: `“Associated team(s)”`), its tag **isMandatory** (ex: `true`) and finally its **options** - - Attention\! The ID and the options’ values must only be letters, no spaces, no special characters, starting with a lowercase letter - -## 2\\ Setup **Cloud Build** trigger - -- Name: `YOUR_COMPANY-imgstudio` -- **Event:** `Manual invocation` -- **Source:** - - Go to the public repository [**https://github.com/aduboue/img-studio**](https://github.com/aduboue/img-studio) - - **Monitor new releases** to stay up-to-date: click on `Watch` \> `Custom` \> `Releases` \> Apply - - Use the top right button to setup a [**GitHub fork**](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) from the repository, which will **create a copy repository in your own GitHub account** - - Back in Cloud Build, **log into your GitHub account**, then select the newly created repository -- **Configuration:** `Cloud Build configuration file (yaml or json)` - - Cloud Build configuration file location: `/cloudbuild.yaml` -- Put in 7 **substitution variables:** - - `_NEXT_PUBLIC_EXPORT_FIELDS_OPTIONS_URI` - - The URI of the configuration JSON file in its bucket - - Ex: `gs://YOUR_COMPANY-imgstudio-export-config/export-fields-options.json` - - `_NEXT_PUBLIC_GCS_BUCKET_LOCATION` - - The region selected for your GCS buckets - - Ex: `europe-west9` - - `_NEXT_PUBLIC_VERTEX_API_LOCATION` - - The region you want to use for VertexAI APIs - - Ex: `europe-west9` - - `_NEXT_PUBLIC_GEMINI_MODEL` \= `gemini-1.5-flash-001` - - Don’t change this - - `_NEXT_PUBLIC_OUTPUT_BUCKET` - - The name of the raw generated output content bucket - - Ex: `YOUR_COMPANY-imgstudio-output` - - `_NEXT_PUBLIC_TEAM_BUCKET` - - The name of the shared content bucket - - Ex: `YOUR_COMPANY-imgstudio-library` - - `_NEXT_PUBLIC_PRINCIPAL_TO_USER_FILTERS` - - The sections of your users’ email address used to log in via IAP that will need to be removed in order to get their user ID, separated by commas - - Ex: my email address is ‘admin-jdupont@company.com’, the value to set would be `admin-,@company.com` so that the user ID jdupont can be extracted - - `_NEXT_PUBLIC_EDIT_ENABLED` - - Allow to enable edit features, **set it to `false` if you do not have access yet** - - `_NEXT_PUBLIC_SEG_MODEL` - - **Only mandatory if Edit is enabled** - - Service name for the Vertex Segmentation model, when you get access to it **(see Step 0\)** -- **Service account:** select the **default already existing Cloud Build service account** `PROJECT_NUMBER-compute@developer.gserviceaccount.com` - - You may want to **check in IAM it has the roles**: `Artifact Registry Writer` and `Logs Writer` -- \> Save -- **Manualy run your first build \!** - -## 3\\ Enable **IAP** & configure **oAuth Consent Screen** - -- Go to **Security \> Identity Aware Proxy** and enable the API -- \> **Configure consent screen** (oAuth) - - **User type** can be either - - `Internal` if you want to limit IAP users to your **GCP org domain** - - `External` if you have some users on a **different domain** than your GCP org’s - - \> Create - - Fill in - - App Name: `YOUR_COMPANY-imgstudio` - - User Support Email - - Authorized Domain: `YOUR_COMPANY_DOMAIN` - - Developer Contact Email - - \> Save and Continue -- Add any needed **scopes** or \> Save and Continue -- Review the **Summary** -- \> Back to Dashboard - -## 4\\ Create application **Service Account** - -- Go to IAM \> Service Accounts \> Create Service Account -- Provide name: `YOUR_COMPANY-imgstudio-sa` -- Give **roles**: - - `Cloud Datastore User` - - `Logs Writer` - - `Secret Manager Secret Accessor` - - `Service Account Token Creator` - - `Storage Object Creator` - - `Storage Object Viewer` - - `Vertex AI User` - -## 5\\ Deploy **Cloud Run** service - -- Deploy container \> Service -- `Deploy one revision from an existing container image` -- **Container image** \> Select from **Artifact registry** the `latest` image you just build in Cloud Build -- Name: `YOUR_COMPANY-imgstudio-app` -- Set your region (ex: `europe-west9`) -- **Authentication**: Require authentication -- **Ingress Control** \> `Internal` \> `Allow traffic from external Application Load Balancers` -- Container(s) **\> Container port:** `3000` -- Security **\> Service account:** `YOUR_COMPANY-imgstudio-sa` -- \> Create -- NB: if you try to access the published URL for the new service you should receive an error message stating “Error: Page Not Found”, this is due to the fact that we are only allowing ingress for external traffic from a Load Balancer - -## 6\\ Grant IAP **Permissions** on Cloud Run service - -- Create the **IAP service account address** - - Go to the top right of the console \> Shell icon “Activate Cloud Shell” - - Wait for machine to setup - - In the terminal, use this command and **copy the output** service account address - - `gcloud beta services identity create --service=iap.googleapis.com --project=PROJECT_ID` - - The format of the output you can copy should be `service-PROJECT_NUMBER@gcp-sa-iap.iam.gserviceaccount.com` -- Go to Cloud Run, in the Services list, select the checkbox next to the name of your service - - Click on **Permissions** \> **Add Principal** - - Grant the `Cloud Run Invoker` role to the previously created/ fetched **IAP service account** - -## 7\\ Create **DNS Zone** - -- Network Service \> Cloud DNS -- \> Create zone -- Complete the form - - Zone Name: `imgstudio` - - **DNS Name**: `imgstudio.YOUR_COMPANY_DOMAIN` - - DNSSEC: `Off` - - Cloud Logging: `Off` -- Once DNS propagation is completed, verify the name servers of the DNS managed zone using the command below (it could take several hours to complete) - - `dig imgstudio.YOUR_COMPANY_DOMAIN NS +short` - -## 8\\ Create **Load Balancer** & **SSL Certificate** - -- Network Service \> Load Balancing -- \> Create Load Balancer -- Select: - - **Type of Load Balancer**: `Application Load Balancer (HTTP/HTTPS)` - - **Public Facing or Internal**: `Public Facing (external)` - - **Global or single region**: `Global` - - **Load Balancer Generation**: `Global external Application Load Balancer` -- \> Configure -- Load balancer name: `YOUR_COMPANY-imgstudio-lb` -- Frontend configuration: - - **Protocol**: `HTTPS` - - **IP address** can be left `Ephemeral` (you could also configure a static IP) - - **Certificate** dropdown \> Create a New Certificate - - Name: `YOUR_COMPANY-imgstudio-cert` - - \> Create Google-managed certificate - - **Domain 1**: `imgstudio.YOUR_COMPANY_DOMAIN` - - \> Create - - \> Done -- Backend Configuration - - Backend Services & Backend Buckets \> Create a **Backend Service** - - Name: `YOUR_COMPANY-imgstudio-back` - - **Backend type**: `Serverless Network Endpoint Group` \> Done - - Backends \> New backend \> Serverless Network Endpoint Groups dropdown \> Create Serverless Network Endpoint Group - - Name: `YOUR_COMPANY-imgstudio-neg` - - Region: the region the Cloud Run service was deployed to (ex: `europe-west9`) - - Serverless network endpoint group type \> Cloud Run \> Select service \> `YOUR_COMPANY-imgstudio-app` \> Create - - Enable Cloud CDN \> `Off` - - \> Create -- Review the Load Balancer configuration -- \> Create - -## 9\\ Create **DNS Record** for Load Balancer frontend - -- Network Services \> Load Balancing, select your load balancer `YOUR_COMPANY-imgstudio-lb` -- Details \> Frontend \> IP-Port -- Note the **IP address** -- Network Services \> Cloud DNS, select your DNS Zone `imgstudio` -- Record Sets \> Add Standard -- Create **record set** - - DNS name: `imgstudio.YOUR_COMPANY_DOMAIN` - - **IPv4 Address**: your load balancer IP Address - - \> Create - -## 10\\ Enable **IAP** & grant **user access** - -- Security \> Identity-Aware Proxy -- Turn on **IAP** for your **backend service** `YOUR_COMPANY-imgstudio-back` -- Select the checkbox next to your service, then \> **Add** **Principal** -- Enter the address of the user (**or group**) you want to allow access to imgstudio -- Assign the role `IAP-secured Web App User`, \> Save - -## 11\\ **Firestore** Database creation - -- Firestore \> Create database - - Firestore **mode**: `Native mode`, \> Continue - - Database **ID**: `(default)` (**very important you keep it that way**) - - Location type: `Region` - - Region: your desired region (ex: `europe-west9` in Paris) - - Secure rules: `Production rules` -- Firestore \> Indexes \> **Composite indexes** \> Create Index - - **Collection ID**: `metadata` - - **Fields to index** - - Field path 1: `combinedFilters`, Index options 1: `Array contains` - - Field path 2: `timestamp`, Index options 2: `Descending` - - Field path 3: `__name__`, Index options 3: `Descending` - - Query **scope**: `Collection` - - \> Create - - **Wait for the index to be successfully created\!** -- Let’s **setup security rules on your database**, and only allow your Cloud Run service account to access it - - In a new tab, go to - - `https://console.firebase.google.com/project/PROJECT_ID/firestore/databases/-default-/rules` - - If necessary, follow the steps to **setup your Firebase project** - - Once in Firestore Database \> Rules, go to the **security rules editor** - - Write the following content, don’t forget to replace **`YOUR_COMPANY`** & **`PROJECT_ID`** in the Cloud Run service account - `rules_version = '2';` - `service cloud.firestore {` - `match /databases/{database}/documents {` - `match /{document=**} {` - `allow read, get, list, create, update: if get(/databases/$(database)/documents/request.auth.uid).data.serviceAccount == 'YOUR_COMPANY-imgstudio-sa@PROJECT_ID.iam.gserviceaccount.com';` - `allow delete: if false;` - `}` - `}` - `}` - - \> Publish - -. - -> ###### _This is not an officially supported Google product. This project is not eligible for the [Google Open Source Software Vulnerability Rewards Program](https://bughunters.google.com/open-source-security)._ +# ImgStudio +![](./assets/imgstudio.jpg) + +ImgStudio is a web tool to experiment with Google Cloud image generation models. + +## Infrastructure Setup Guide + +### Pre-requisites + +- Ensure required access to **Imagen models** + - **For Imagen Generation:** + - Models are now in public GA, **Imagen 3 Generate** (`imagen-3.0-generate-002`) and **Imagen 3 Generate Fast** (`imagen-3.0-fast-generate-001`) + - **For people generation** (adult and/ or children), you now need to fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSduBp9w84qgim6vLriQ9p7sdz62bMJaL-nNmIVoyiOwd84SMw/viewform) to get access. + - **For Imagen Editing & Customization** + - You can fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLScN9KOtbuwnEh6pV7xjxib5up5kG_uPqnBtJ8GcubZ6M3i5Cw/viewform) to get access to the Preview feature (name: `imagen-3.0-capability-001`) + - You will also need the **Vertex Image Segmentation model** when using editing, fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSdzIR1EeQGFcMsqd9nPip5e9ovDKSjfWRd58QVjo1zLpfdvEg/viewform?resourcekey=0-Pvqc66u-0Z1QmuzHq4wLKg&pli=1) to get access (name: `image-segmentation-001`) +- Install the [Google Cloud CLI](https://cloud.google.com/sdk/docs/install) +- Authenticate with your user account: `gcloud auth login` (then follow login steps in the browser). You must have the `Project Creator` role or the `Project Editor` role on an existing project. We recommend you set up a new project for this. +- Ensure that the following constraint is not set in the project: + - `constraints/iam.disableServiceAccountCreation` (Justification: service accounts are created by the solution) + +### 1: Run **boothstrap.sh** script +This script will confirm your target GCP project and configurations, enable necessary APIs, configure OAuth, create GCS Bucket (for Terraform state storage), artifact registry repository, and Cloud Build service account with the necessary permissions. + +**How to Run** +From the project's root folder, run the script `bootstrap.sh`: + +``` +./bootstraph.sh +``` + +Only proceed to the next step if the script completed successfully. + +### 2: Configure and run the build pipeline +The next step is to run a build pipeline (using Cloud Build) to deploy the app and infrastructure resources. + +#### High Level Build Steps + - Step-1: Build imgstudio docker image + - Step-2: Push docker image to artifact registry + - Step-3: Terraform init + - Step-4: Terraform deploy +#### Necessary Parameters + - The bootstrap script will do this for you, but you may want to double check the following parameters are correctly specified inside your `cloudbuild.yaml` file: + - _IAP_ALLOWED_MEMBERS: The list of IAM members allowed to access the app. Use prefix user:, group:, or domain: for IAM users, groups, or domains, respectively. Example: `'["group:my-user-group@example.com", "user:admin-joe@example.com"]'` + - _NEXT_PUBLIC_PRINCIPAL_TO_USER_FILTERS: Set this your company's domain using the following format: `',@example.com'` + - _DOMAIN_NAME: If you plan on setting up a custom DNS name, set this value. Otherwise, leave it empty. If you need it empty, your app will be accessible through `.nip.io`. The LB_IP_ADDRESS will be shown in the output of the build logs. + - See the `substitutions` part inside your `cloudbuild.yaml` file for details and additional variables you can optionally change. + +#### How to Run + +From the project's root folder, run : + +``` +gcloud builds submit . --config=cloudbuild.yaml +``` + +You can open the URL shown in the output to inspect the build logs. Look for the line `Logs are available at [ URL ]` + + +#### If you're not configuring a custom domain (If you're setting up a custom domain, move on the step #3) +Once the build has successfully completed, Run the following command to check the status of the SSL certificate provisioning: +``` +gcloud compute ssl-certificates describe iap-lb-cert +``` + +Example output: +``` +creationTimestamp: '2025-03-31T01:37:26.597-07:00' +id: '123456789' +kind: compute#sslCertificate +managed: + domainStatus: + 34.55.25.140.nip.io: PROVISIONING + domains: + - 34.55.25.140.nip.io + status: PROVISIONING +name: iap-lb-cert +selfLink: https://www.googleapis.com/compute/v1/projects//global/sslCertificates/iap-lb-cert +type: MANAGED +``` + +If it says `PROVISIONING`, wait a few minutes before running it again. Once provisioning is complete, you should see the status `ACTIVE`. It can take sometime for the SSL certificate to be provisioned. + +At this point, you can open a browser and navigate to the domain that ends in `nip.io` (in the above example, it's `34.55.25.140.nip.io`). + +**Note**: There is a known issue where you get an error message with error code 11 when accessing the app. To fix this, check the **Known Issues and Workarounds** section below. + +### 3: [Optional] Configure DNS record for custom domain +In this step, you will create an A record that points your chosen domain name to the load balancer's IP address. Terraform will print out "external LB's IP address" in the cloud build logs. + +You can also inspect the external IP address with the following command: +``` +gcloud compute forwarding-rules describe iap-lb --global --format='value(IPAddress)' +``` + +Copy the IP address, and navigate to your domain registrar to create an A record. You may need to request a network admin in your organization to do so. + +#### Check SSL certificate provisioning status +An SSL certificate is created by Terraform using Google Cloud's Certificate Manager. For the certificate to be successfully provisioned by Google's certificate authority (CA), a DNS record must point the domain to the load balancer's IP. There is a polling mechanism to check if this has been set up, but it can take a while for changes to be reflected. + +Once you've created the proper DNS record, run the following command to check the status: +``` +gcloud compute ssl-certificates describe iap-lb-cert +``` + +If it says `PROVISIONING`, wait a few minutes before running it again. Once provisioning is complete, you should see the status `ACTIVE`. If provisioning failed, try running the Cloud Build pipelien again. This could happen if too much time has passed since the SSL certificate resource was deployed and the DNS record was created. + +At this point, you can open a browser and navigate to the domain that ends in `nip.io` (in the above example, it's `34.55.25.140.nip.io`). + +**Note**: There is a known issue where you get an error message with error code 11 when accessing the app. To fix this, check the **Known Issues and Workarounds** section below. + +## Enjoy it ! +If everything run successfully you can now start using ImgStudio. We're working on a user guide for guidance and best practices. Stay tuned for an update. + + +## Known Issues and Workarounds +#### If you get the following error message while trying to access the app: There was a problem with your request. Please reference https://cloud.google.com/iap/docs/faq#error_codes. Error code 11 + +Do the following steps: +1. Navigate to the [IAP console](https://console.cloud.google.com/security/iap) +2. Disable IAP by selecting the app and clicking on the toggle: + +![](./assets/iap-toggle.jpg) + +3. Wait a few seconds for the operation to complete and then click on the toggle again to re-enable it. Select the checkbox `I have read the configuration requirements and configured my Backend Service according to documentation` and click on **Turn on IAP** +4. Wait a few seconds for the operation to complete and you should be able to access the app now. + +If those steps don't solve the issue, please wait a few minutes before trying to access the app again. + +## Acknowledgements +Terraform contributions: [@maoye-google](https://github.com/maoye-google) + +> ###### _This is not an officially supported Google product. This project is not eligible for the [Google Open Source Software Vulnerability Rewards Program](https://bughunters.google.com/open-source-security)._ \ No newline at end of file diff --git a/assets/iap-toggle.jpg b/assets/iap-toggle.jpg new file mode 100644 index 00000000..0d286e61 Binary files /dev/null and b/assets/iap-toggle.jpg differ diff --git a/assets/imgstudio.jpg b/assets/imgstudio.jpg new file mode 100644 index 00000000..624f1230 Binary files /dev/null and b/assets/imgstudio.jpg differ diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100755 index 00000000..4afb023d --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,428 @@ +#!/usr/bin/env bash +#=============================================================================== +# Script: bootstrap.sh +# Description: Sets up resources in Google Cloud for IMG Studio application +# Date: March 21, 2025 +# Usage: ./bootstrap.sh +#=============================================================================== + +# Exit on any command error +set -e + +#------------------------------------------------------------------------------- +# Color definitions +#------------------------------------------------------------------------------- +readonly GREEN='\033[0;32m' +readonly RED='\033[0;31m' +readonly YELLOW='\033[0;33m' +readonly BLUE='\033[0;34m' +readonly NC='\033[0m' # No Color + +#------------------------------------------------------------------------------- +# Global variables +#------------------------------------------------------------------------------- +DEBUG=false +SERVICES=( + "secretmanager.googleapis.com" + "cloudbuild.googleapis.com" + "iap.googleapis.com" + "artifactregistry.googleapis.com" + "aiplatform.googleapis.com" + "cloudresourcemanager.googleapis.com" + "firestore.googleapis.com" + "compute.googleapis.com" + "iam.googleapis.com" + "cloudidentity.googleapis.com" + "run.googleapis.com" + "serviceusage.googleapis.com" + "storage-api.googleapis.com" + "storage.googleapis.com" +) + +#------------------------------------------------------------------------------- +# Utility functions +#------------------------------------------------------------------------------- +log_info() { + echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] ${BLUE}INFO${NC}: $1" +} + +log_success() { + echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] ${GREEN}SUCCESS${NC}: $1" +} + +log_warning() { + echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] ${YELLOW}WARNING${NC}: $1" +} + +log_error() { + echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] ${RED}ERROR${NC}: $1" >&2 +} + +log_debug() { + if [[ "$DEBUG" == true ]]; then + echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] DEBUG: $1" + fi +} + +die() { + log_error "$1" + exit 1 +} + +# Function to handle cleanup on script exit +cleanup() { + log_debug "Performing cleanup..." + # Add any cleanup tasks here if needed +} + +# Set trap for cleanup on script exit +trap cleanup EXIT + +#------------------------------------------------------------------------------- +# Core functions +#------------------------------------------------------------------------------- + +# Function to check if required tools are installed +check_prerequisites() { + log_info "Checking prerequisites..." + + # Check if gcloud is installed + if ! command -v gcloud &> /dev/null; then + die "gcloud is not installed. Please install the Google Cloud SDK first." + fi + + # Check if user is authenticated with gcloud + if ! gcloud auth list --filter=status:ACTIVE --format="value(account)" &> /dev/null; then + die "Not authenticated with gcloud. Please run 'gcloud auth login' first." + fi + + log_success "All prerequisites are satisfied" +} + +# Function to get default project ID +get_default_project() { + gcloud config get project 2>/dev/null || echo "" +} + +# Function to add IAM policy binding with better error handling +add_iam_member() { + local member=$1 + local role=$2 + + log_debug "Adding IAM binding: $member -> $role" + + if ! gcloud projects add-iam-policy-binding "$PROJECT_ID" --member="$member" --role="$role" &> /dev/null; then + log_warning "Failed to add IAM binding for $member with role $role" + return 1 + fi + + return 0 +} + +# Function to enable required GCP services +enable_services() { + log_info "Enabling required APIs... (this will take a few minutes)" + + for service in "${SERVICES[@]}"; do + log_debug "Enabling $service" + if ! gcloud services enable "$service" &> /dev/null; then + log_warning "Failed to enable $service, continuing anyway" + fi + done + + log_success "Required APIs have been enabled" +} + +# Function to create Artifact Registry repository +create_artifact_registry() { + log_info "Creating Artifact Registry repository" + + if gcloud artifacts repositories describe docker-repo --location="$REGION" &> /dev/null; then + log_info "Artifact Registry repository already exists" + else + gcloud artifacts repositories create docker-repo \ + --repository-format=docker \ + --location="$REGION" \ + --description="Private docker images" \ + --project "$PROJECT_ID" + + log_success "Created Artifact Registry repository" + fi +} + +# Function to create Terraform state bucket +create_tf_state_bucket() { + local bucket_name="gs://$PROJECT_ID-tf-state" + + if gsutil ls "$bucket_name" &> /dev/null; then + log_info "Terraform bucket already exists" + else + log_info "Creating Terraform state bucket..." + gsutil mb "$bucket_name" + log_success "Created Terraform state bucket: $bucket_name" + fi +} + +# Function to set up build service account +setup_build_service_account() { + log_info "Setting up Cloud Build Service Account with required permissions" + + # Check if service account already exists + if gcloud iam service-accounts describe "build-sa@$PROJECT_ID.iam.gserviceaccount.com" &> /dev/null; then + log_info "Build service account already exists" + else + gcloud iam service-accounts create build-sa \ + --display-name="Cloud Build worker SA" \ + --description="Cloud Build worker SA" + fi + + local member="serviceAccount:build-sa@$PROJECT_ID.iam.gserviceaccount.com" + + local roles=( + "roles/datastore.user" + "roles/logging.logWriter" + "roles/secretmanager.secretAccessor" + "roles/secretmanager.viewer" + "roles/iam.securityAdmin" + "roles/storage.admin" + "roles/artifactregistry.writer" + "roles/aiplatform.user" + "roles/run.builder" + "roles/run.developer" + "roles/compute.admin" + "roles/datastore.owner" + "roles/iam.serviceAccountTokenCreator" + "roles/iam.serviceAccountUser" + "roles/iam.serviceAccountCreator" + "roles/iap.settingsAdmin" + "roles/firebase.developAdmin" + ) + + log_info "Granting IAM roles to build service account..." + + for role in "${roles[@]}"; do + log_debug "Adding role: $role" + add_iam_member "$member" "$role" + done + + log_success "Service account permissions configured" +} + +# Function to configure IAP OAuth +configure_iap_oauth() { + log_info "Configuring IAP OAuth..." + + # Create brand if not exists + local brand_exists + brand_exists=$(gcloud alpha iap oauth-brands list --format="value(name)" 2>/dev/null | grep -c "$PROJECT_NUMBER" || true) + + if [[ "$brand_exists" -eq 0 ]]; then + gcloud alpha iap oauth-brands create \ + --application_title="$APP_TITLE" \ + --support_email="$USER_EMAIL" + + # Give time for the brand to propagate + log_info "Waiting for OAuth brand to propagate..." + sleep 30 + else + log_info "OAuth brand already exists" + fi + + # Create OAuth client if not exists + local client_exists + client_exists=$(gcloud alpha iap oauth-clients list "projects/$PROJECT_NUMBER/brands/$PROJECT_NUMBER" --format="value(displayName)" 2>/dev/null | grep -c "$SERVICE_NAME" || true) + + if [[ "$client_exists" -eq 0 ]]; then + gcloud alpha iap oauth-clients create \ + "projects/$PROJECT_ID/brands/$PROJECT_NUMBER" \ + --display_name="$SERVICE_NAME" + + log_success "Created OAuth client" + else + log_info "OAuth client already exists" + fi + + # Store the client name, ID, and secret + export CLIENT_NAME=$(gcloud alpha iap oauth-clients list \ + "projects/$PROJECT_NUMBER/brands/$PROJECT_NUMBER" --format='value(name)' \ + --filter="displayName:$SERVICE_NAME") + + export CLIENT_ID=${CLIENT_NAME##*/} + + export CLIENT_SECRET=$(gcloud alpha iap oauth-clients describe "$CLIENT_NAME" --format='value(secret)') + + log_info "Creating IAP service account..." + # Create IAP Service Account + gcloud beta services identity create \ + --service=iap.googleapis.com \ + --project="$PROJECT_ID" + + # Store secret in Secret Manager + if gcloud secrets describe iap_client_secret &> /dev/null; then + log_info "IAP client secret already exists in Secret Manager" + else + log_info "Storing OAuth client secret in Secrets Manager..." + echo "$CLIENT_SECRET" | gcloud secrets create iap_client_secret \ + --data-file=- \ + --replication-policy=user-managed \ + --locations="$REGION" + + log_success "Stored client secret in Secret Manager" + fi + + # Store client ID in Secret Manager (not really a secret, but convenient to store it together with client secret) + if gcloud secrets describe iap_client_id &> /dev/null; then + log_info "IAP client ID already exists in Secret Manager" + else + log_info "Storing OAuth client secret in Secrets Manager..." + echo "$CLIENT_ID" | gcloud secrets create iap_client_id \ + --data-file=- \ + --replication-policy=user-managed \ + --locations="$REGION" + + log_success "Stored client ID in Secret Manager" + fi + + log_success "IAP OAuth configuration complete" +} + +# Function to update cloudbuild template +update_cloudbuild_template() { + log_info "Updating cloudbuild.yaml from template..." + + if [[ ! -f "cloudbuild.yaml.template" ]]; then + log_error "Template file cloudbuild.yaml.template not found" + return 1 + fi + + # Check if cloudbuild.yaml already exists + if [[ -f "cloudbuild.yaml" ]]; then + log_warning "A cloudbuild.yaml file already exists" + read -rp "Do you want to overwrite it? [y/N]: " OVERWRITE + if [[ ! "$OVERWRITE" =~ ^[yY]$ ]]; then + log_warning "Skipping cloudbuild.yaml update at user request. Please ensure the variable substitutions in cloudbuild.yaml are correct." + return 0 + fi + fi + + cp cloudbuild.yaml.template cloudbuild.yaml + + # Replace placeholders in the template + local domain_value="${DOMAIN_NAME:-}" + + # Extract company domain from IAP_USER_GROUP + local company_domain="" + if [[ "$IAP_USER_GROUP" =~ @([^@]+)$ ]]; then + company_domain="${BASH_REMATCH[1]}" + log_debug "Extracted company domain: $company_domain" + else + log_warning "Could not extract company domain from $IAP_USER_GROUP" + # Default to empty string if extraction fails + company_domain="" + fi + + log_debug "Replacing with '$domain_value'" + log_debug "Replacing with '$IAP_USER_GROUP'" + log_debug "Replacing with '$company_domain'" + + sed -i "s||${domain_value}|g" cloudbuild.yaml + sed -i "s||${IAP_USER_GROUP}|g" cloudbuild.yaml + sed -i "s||${company_domain}|g" cloudbuild.yaml + + log_success "Updated cloudbuild.yaml with your configuration" +} + +# Function to collect user inputs with validation +collect_inputs() { + # Service name + read -rp "Enter service name [imgstudio]: " SERVICE_NAME + export SERVICE_NAME=${SERVICE_NAME:-"imgstudio"} + + # Project ID + DEFAULT_PROJECT=$(get_default_project) + if [[ -n "$DEFAULT_PROJECT" ]]; then + read -rp "Enter project ID [$DEFAULT_PROJECT]: " PROJECT_ID + export PROJECT_ID=${PROJECT_ID:-"$DEFAULT_PROJECT"} + else + while [[ -z "$PROJECT_ID" ]]; do + read -rp "Enter project ID: " PROJECT_ID + if [[ -z "$PROJECT_ID" ]]; then + log_warning "Project ID cannot be empty" + fi + done + export PROJECT_ID="$PROJECT_ID" + fi + + # Region + read -rp "Enter region [us-central1]: " REGION + export REGION=${REGION:-"us-central1"} + + # App title + read -rp "Enter app title [IMG Studio]: " APP_TITLE + export APP_TITLE=${APP_TITLE:-"IMG Studio"} + + + # IAP user group (required) + while [[ -z "$IAP_USER_GROUP" ]]; do + read -rp "Enter Google Identity group for IAP access (e.g. imgstudio-users@example.com): " IAP_USER_GROUP + if [[ -z "$IAP_USER_GROUP" ]]; then + log_warning "IAP user group cannot be empty" + fi + done + export IAP_USER_GROUP="$IAP_USER_GROUP" + + # Domain name (optional) + read -rp "(OPTIONAL) Enter domain name [''] (e.g. imgstudio.example.com): " DOMAIN_NAME + export DOMAIN_NAME="$DOMAIN_NAME" + + export USER_EMAIL=$(gcloud config list account --format "value(core.account)") + + # Set project in gcloud config + log_debug "Setting gcloud project to $PROJECT_ID" + gcloud config set project "$PROJECT_ID" + + # Get project number + export PROJECT_NUMBER=$(gcloud projects list --filter="$PROJECT_ID" --format='value(PROJECT_NUMBER)') + + # Validate we got a project number + if [[ -z "$PROJECT_NUMBER" ]]; then + die "Failed to retrieve project number for project ID: $PROJECT_ID" + fi + + # Echo the configured values + echo -e "\nConfiguration:" + echo "USER_EMAIL: $USER_EMAIL (obtained from gcloud)" + echo "SERVICE_NAME: $SERVICE_NAME" + echo "PROJECT_ID: $PROJECT_ID" + echo "PROJECT_NUMBER: $PROJECT_NUMBER (obtained from gcloud)" + echo "REGION: $REGION" + echo "APP_TITLE: $APP_TITLE" + echo "DOMAIN_NAME: ${DOMAIN_NAME:-""}" + echo "IAP_USER_GROUP: $IAP_USER_GROUP" + + read -rp "Proceed? [Y/n]: " PROCEED + if [[ "$PROCEED" =~ ^[nN]$ ]]; then + log_info "Exiting at user request" + exit 0 + fi +} + +# Main function to orchestrate the script execution +main() { + log_info "Starting GCP IMG Studio setup" + + check_prerequisites + collect_inputs + enable_services + create_artifact_registry + create_tf_state_bucket + setup_build_service_account + configure_iap_oauth + update_cloudbuild_template + + log_success "Setup completed successfully" + +} + +# Execute main function +main "$@" \ No newline at end of file diff --git a/clean_up_after_update.sh b/clean_up_after_update.sh new file mode 100755 index 00000000..db80ff85 --- /dev/null +++ b/clean_up_after_update.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +rm -rf src/app.bkp +rm -rf src/public.bkp +rm -rf src/third_party.bkp +rm -rf img-studio diff --git a/cloudbuild.yaml b/cloudbuild.yaml deleted file mode 100644 index 8f7957a6..00000000 --- a/cloudbuild.yaml +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -steps: - - name: 'gcr.io/cloud-builders/docker' - args: - [ - 'build', - '-t', - 'gcr.io/$PROJECT_ID/img-studio:latest', - '--build-arg', - '_NEXT_PUBLIC_PROJECT_ID=$PROJECT_ID', - '--build-arg', - '_NEXT_PUBLIC_VERTEX_API_LOCATION=${_NEXT_PUBLIC_VERTEX_API_LOCATION}', - '--build-arg', - '_NEXT_PUBLIC_GCS_BUCKET_LOCATION=${_NEXT_PUBLIC_GCS_BUCKET_LOCATION}', - '--build-arg', - '_NEXT_PUBLIC_GEMINI_MODEL=${_NEXT_PUBLIC_GEMINI_MODEL}', - '--build-arg', - '_NEXT_PUBLIC_SEG_MODEL=${_NEXT_PUBLIC_SEG_MODEL}', - '--build-arg', - '_NEXT_PUBLIC_EDIT_ENABLED=${_NEXT_PUBLIC_EDIT_ENABLED}', - '--build-arg', - '_NEXT_PUBLIC_PRINCIPAL_TO_USER_FILTERS=${_NEXT_PUBLIC_PRINCIPAL_TO_USER_FILTERS}', - '--build-arg', - '_NEXT_PUBLIC_OUTPUT_BUCKET=${_NEXT_PUBLIC_OUTPUT_BUCKET}', - '--build-arg', - '_NEXT_PUBLIC_TEAM_BUCKET=${_NEXT_PUBLIC_TEAM_BUCKET}', - '--build-arg', - '_NEXT_PUBLIC_EXPORT_FIELDS_OPTIONS_URI=${_NEXT_PUBLIC_EXPORT_FIELDS_OPTIONS_URI}', - '.', - ] -images: - - 'gcr.io/$PROJECT_ID/img-studio:latest' - -options: - logging: CLOUD_LOGGING_ONLY diff --git a/cloudbuild.yaml.template b/cloudbuild.yaml.template new file mode 100644 index 00000000..e5152b59 --- /dev/null +++ b/cloudbuild.yaml.template @@ -0,0 +1,113 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +steps: +# Img Studio Container steps +- id: 'Img Studio - App Build' + name: 'gcr.io/cloud-builders/docker' + args: + - 'build' + - '-t' + - '${_APP_IMAGE_NAME}' + - '--build-arg' + - '_NEXT_PUBLIC_PROJECT_ID=$_NEXT_PUBLIC_PROJECT_ID' + - '--build-arg' + - '_NEXT_PUBLIC_VERTEX_API_LOCATION=$_NEXT_PUBLIC_VERTEX_API_LOCATION' + - '--build-arg' + - '_NEXT_PUBLIC_GCS_BUCKET_LOCATION=$_NEXT_PUBLIC_GCS_BUCKET_LOCATION' + - '--build-arg' + - '_NEXT_PUBLIC_GEMINI_MODEL=$_NEXT_PUBLIC_GEMINI_MODEL' + - '--build-arg' + - '_NEXT_PUBLIC_SEG_MODEL=$_NEXT_PUBLIC_SEG_MODEL' + - '--build-arg' + - '_NEXT_PUBLIC_EDIT_ENABLED=$_NEXT_PUBLIC_EDIT_ENABLED' + - '--build-arg' + - '_NEXT_PUBLIC_PRINCIPAL_TO_USER_FILTERS=$_NEXT_PUBLIC_PRINCIPAL_TO_USER_FILTERS' + - '--build-arg' + - '_NEXT_PUBLIC_OUTPUT_BUCKET=$_NEXT_PUBLIC_OUTPUT_BUCKET' + - '--build-arg' + - '_NEXT_PUBLIC_TEAM_BUCKET=$_NEXT_PUBLIC_TEAM_BUCKET' + - '--build-arg' + - '_NEXT_PUBLIC_EXPORT_FIELDS_OPTIONS_URI=$_NEXT_PUBLIC_EXPORT_FIELDS_OPTIONS_URI' + - '.' + dir: src/ +- id: 'Img Studio - Push Image' + name: 'gcr.io/cloud-builders/docker' + args: + - 'push' + - '${_APP_IMAGE_NAME}' + dir: src/ + +# Terraform init +- id: 'Terraform Inititalization' + name: 'hashicorp/terraform:1.3.6' + entrypoint: 'sh' + args: + - '-c' + - | + terraform init \ + -backend-config="bucket=$PROJECT_ID-tf-state" \ + -backend-config="prefix=img-studio" + dir: terraform + +# Terraform Apply +- id: 'Terraform Apply' + name: 'hashicorp/terraform:1.3.6' + args: + - apply + - -auto-approve + dir: terraform + +# General Setting +substitutions: + _APP_NAME: 'imgstudio' # Optional: change the app name (used in artifact registry) + _APP_REGION: 'us-central1' # Do not change unless you changed the Cloud region in the bootstrap script + _AR_REPO_NAME: 'docker-repo' # Do not change unless you changed the name of the artifact registry repository in the bootstrap script + _IAP_ALLOWED_MEMBERS: '["group:"]' # IAM members allowed to access the app. Use prefixes 'user:', 'group:', or 'domain:' for user, user groups, and Cloud Identity domains, respectively. + _NEXT_PUBLIC_PRINCIPAL_TO_USER_FILTERS: ',@' # String to filter out of user principal IDs + _DOMAIN_NAME: '' # Leave this empty if you do not want to configure a custom domain name at this time + + # The following values should not be changed unless needed + _APP_IMAGE_NAME: ${_APP_REGION}-docker.pkg.dev/${PROJECT_ID}/${_AR_REPO_NAME}/${_APP_NAME}:${BUILD_ID} + _NEXT_PUBLIC_PROJECT_ID: ${PROJECT_ID} # Do not change + _NEXT_PUBLIC_VERTEX_API_LOCATION: 'us-central1' # Be sure to check the availability before changing this + _NEXT_PUBLIC_GCS_BUCKET_LOCATION: ${_APP_REGION} # Do not change + _NEXT_PUBLIC_GEMINI_MODEL: 'gemini-2.0-flash-exp' # Default recommended text handling model. + _NEXT_PUBLIC_SEG_MODEL: 'image-segmentation-001' # Segmentation Model name. Not recommend to change + _NEXT_PUBLIC_EDIT_ENABLED: 'true' # Your allowlist status for this feature + _NEXT_PUBLIC_OUTPUT_BUCKET: '${PROJECT_ID}-imgstudio-output' # Do not change + _NEXT_PUBLIC_TEAM_BUCKET: '${PROJECT_ID}-imgstudio-library' # Do not change + _NEXT_PUBLIC_EXPORT_FIELDS_OPTIONS_URI: 'gs://${PROJECT_ID}-imgstudio-export-config/export-fields-options.json' # Do not change +options: + logging: CLOUD_LOGGING_ONLY + dynamic_substitutions: true + env: # everything need to be in lower case format + - TF_VAR_project_id=$PROJECT_ID + - TF_VAR_app_tag=$BUILD_ID + - TF_VAR_region=${_APP_REGION} + - TF_VAR_app_name=${_APP_NAME} + - TF_VAR_app_container_name=${_APP_IMAGE_NAME} + - TF_VAR_iap_allowed_members=${_IAP_ALLOWED_MEMBERS} + - TF_VAR_domain_name=${_DOMAIN_NAME} + +tags: + - img-studio + - terraform +images: + - ${_APP_IMAGE_NAME} +timeout: 3600s + +serviceAccount: 'projects/${PROJECT_ID}/serviceAccounts/build-sa@${PROJECT_ID}.iam.gserviceaccount.com' + + \ No newline at end of file diff --git a/cloudbuild_destroy.yaml b/cloudbuild_destroy.yaml new file mode 100644 index 00000000..09397cc3 --- /dev/null +++ b/cloudbuild_destroy.yaml @@ -0,0 +1,29 @@ +steps: +- id: 'tf init' + name: 'hashicorp/terraform:1.3.6' + entrypoint: 'sh' + args: + - '-c' + - | + terraform init \ + -backend-config="bucket=$PROJECT_ID-tf-state" \ + -backend-config="prefix=img-studio" + dir: terraform + +- id: 'tf destroy' + name: 'hashicorp/terraform:1.3.6' + args: + - destroy + - -auto-approve + dir: terraform + +options: + env: + - TF_VAR_project_id=$PROJECT_ID + +tags: + - terraform + - img-studio + - destroy + +timeout: 3600s diff --git a/config/export-fields-options.json b/config/export-fields-options.json new file mode 100644 index 00000000..5652174b --- /dev/null +++ b/config/export-fields-options.json @@ -0,0 +1,130 @@ +{ + "contextAuthorTeam": { + "label": "In which team are you?", + "name": "Associated team(s)", + "type": "multiple-select", + "isUpdatable": true, + "isMandatory": true, + "isExportVisible": true, + "isExploreVisible": true, + "options": [ + { + "value": "marketing", + "label": "Content marketing" + }, + { + "value": "communityManagement", + "label": "Community Management" + }, + { + "value": "hr", + "label": "Human Ressources" + }, + { + "value": "product", + "label": "Product development" + }, + { + "value": "sales", + "label": "Sales enablement" + } + ] + }, + "contextTargetPlatform": { + "label": "For which platform is it targetted?", + "name": "Targetted platform(s)", + "type": "multiple-select", + "isUpdatable": true, + "isMandatory": true, + "isExportVisible": true, + "isExploreVisible": true, + "options": [ + { + "value": "mailing", + "label": "Mailing compaign" + }, + { + "value": "website", + "label": "Public website" + }, + { + "value": "socialMedias", + "label": "Social medias" + }, + { + "value": "product", + "label": "Product development" + } + ] + }, + "contextAssociatedBrand": { + "label": "For which brand(s) did you create this image?", + "name": "Associated brand(s)", + "type": "multiple-select", + "isUpdatable": true, + "isMandatory": false, + "isExportVisible": true, + "isExploreVisible": true, + "options": [ + { + "value": "gemo", + "label": "G\u00e9mo" + }, + { + "value": "mellowYellow", + "label": "Mellow Yellow" + }, + { + "value": "bocage", + "label": "Bocage" + }, + { + "value": "maje", + "label": "Maje" + }, + { + "value": "bash", + "label": "Ba&sh" + }, + { + "value": "sessun", + "label": "Sess\u00f9n" + }, + { + "value": "americanVintage", + "label": "American Vintage" + }, + { + "value": "theKooples", + "label": "The Kooples" + } + ] + }, + "contextCollection": { + "label": "To which collection(s) is it associated?", + "name": "Associated collection(s)", + "type": "multiple-select", + "isUpdatable": true, + "isMandatory": false, + "isExportVisible": true, + "isExploreVisible": true, + "options": [ + { + "value": "spring", + "label": "Spring" + }, + { + "value": "summer", + "label": "Summer" + }, + { + "value": "fall", + "label": "Fall" + }, + { + "value": "winter", + "label": "Winter" + } + ] + } +} diff --git a/src/.prettierrc.json b/src/.prettierrc.json new file mode 100644 index 00000000..23b8710b --- /dev/null +++ b/src/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "printWidth": 120 +} diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 00000000..c8844324 --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,92 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Use the official Node.js image as the base +FROM node:20-alpine AS base + +# Set the working directory within the container +WORKDIR /app + +# Copy package.json and package-lock.json (if available) +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Builder stage +FROM base AS builder +WORKDIR /app + +# Copy the rest of the application code +COPY . . + +# Set environment variables from build arguments +ARG _NEXT_PUBLIC_PROJECT_ID +ARG _NEXT_PUBLIC_VERTEX_API_LOCATION +ARG _NEXT_PUBLIC_GCS_BUCKET_LOCATION +ARG _NEXT_PUBLIC_GEMINI_MODEL +ARG _NEXT_PUBLIC_SEG_MODEL +ARG _NEXT_PUBLIC_EDIT_ENABLED +ARG _NEXT_PUBLIC_VEO_ENABLED +ARG _NEXT_PUBLIC_VEO_ITV_ENABLED +ARG _NEXT_PUBLIC_VEO_ADVANCED_ENABLED +ARG _NEXT_PUBLIC_PRINCIPAL_TO_USER_FILTERS +ARG _NEXT_PUBLIC_OUTPUT_BUCKET +ARG _NEXT_PUBLIC_TEAM_BUCKET +ARG _NEXT_PUBLIC_EXPORT_FIELDS_OPTIONS_URI +ENV NEXT_PUBLIC_PROJECT_ID=$_NEXT_PUBLIC_PROJECT_ID \ + NEXT_PUBLIC_VERTEX_API_LOCATION=$_NEXT_PUBLIC_VERTEX_API_LOCATION \ + NEXT_PUBLIC_GCS_BUCKET_LOCATION=$_NEXT_PUBLIC_GCS_BUCKET_LOCATION \ + NEXT_PUBLIC_GEMINI_MODEL=$_NEXT_PUBLIC_GEMINI_MODEL \ + NEXT_PUBLIC_SEG_MODEL=$_NEXT_PUBLIC_SEG_MODEL \ + NEXT_PUBLIC_EDIT_ENABLED=$_NEXT_PUBLIC_EDIT_ENABLED \ + NEXT_PUBLIC_VEO_ENABLED=$_NEXT_PUBLIC_VEO_ENABLED \ + NEXT_PUBLIC_VEO_ITV_ENABLED=$_NEXT_PUBLIC_VEO_ITV_ENABLED \ + NEXT_PUBLIC_VEO_ADVANCED_ENABLED=$_NEXT_PUBLIC_VEO_ADVANCED_ENABLED \ + NEXT_PUBLIC_PRINCIPAL_TO_USER_FILTERS=$_NEXT_PUBLIC_PRINCIPAL_TO_USER_FILTERS \ + NEXT_PUBLIC_OUTPUT_BUCKET=$_NEXT_PUBLIC_OUTPUT_BUCKET \ + NEXT_PUBLIC_TEAM_BUCKET=$_NEXT_PUBLIC_TEAM_BUCKET \ + NEXT_PUBLIC_EXPORT_FIELDS_OPTIONS_URI=$_NEXT_PUBLIC_EXPORT_FIELDS_OPTIONS_URI + +# Build the Next.js application +RUN npm run build + +# Use a smaller Node.js image for production +FROM node:20-alpine AS runner + +# Set the working directory +WORKDIR /app + +# Install ffmpeg using Alpine's package manager +USER root +RUN apk update && apk add --no-cache ffmpeg + +# Copy the built application and required files from the builder stage +COPY --from=builder --chown=node:node /app/next.config.mjs ./ +COPY --from=builder --chown=node:node /app/public ./public +COPY --from=builder --chown=node:node /app/.next ./.next +COPY --from=builder --chown=node:node /app/node_modules ./node_modules +COPY --from=builder --chown=node:node /app/package.json ./package.json + +# Explicitly create the cache/images directory and ensure node user owns it if +RUN mkdir -p /app/.next/cache/images && \ + chown -R node:node /app/.next/cache + +USER node + +# Expose the port the app will run on +EXPOSE 3000 + +# Start the Next.js application in production mode +CMD ["npm", "start"] diff --git a/src/app/(studio)/edit/page.tsx b/src/app/(studio)/edit/page.tsx new file mode 100644 index 00000000..f79e3d4a --- /dev/null +++ b/src/app/(studio)/edit/page.tsx @@ -0,0 +1,93 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use client' + +import * as React from 'react' +import Grid from '@mui/material/Grid2' +import Box from '@mui/material/Box' +import { useCallback, useEffect, useState } from 'react' +import { ImageI } from '../../api/generate-image-utils' +import OutputImagesDisplay from '../../ui/transverse-components/ImagenOutputImagesDisplay' +import { useAppContext } from '../../context/app-context' +import { Typography } from '@mui/material' + +import theme from '../../theme' +import EditForm from '@/app/ui/edit-components/EditForm' +import { redirect } from 'next/navigation' +const { palette } = theme + +export default function Page() { + const [editedImagesInGCS, setEditedImagesInGCS] = useState([]) + const [isEditLoading, setIsEditLoading] = useState(false) + const [editErrorMsg, setEditErrorMsg] = useState('') + const { appContext, error } = useAppContext() + const [editedCount, setEditedCount] = useState(0) + + const handleImageGeneration = (newImages: ImageI[]) => { + setEditedImagesInGCS(newImages) + setIsEditLoading(false) + } + + const handleRequestSent = (valid: boolean, count: number) => { + editErrorMsg !== '' && valid && setEditErrorMsg('') + setIsEditLoading(valid) + setEditedCount(count) + } + const handleNewErrorMsg = useCallback((newErrorMsg: string) => { + setEditErrorMsg(newErrorMsg) + setIsEditLoading(false) + }, []) + + if (appContext?.isLoading === true) { + return ( + + + {error === null + ? 'Loading your profile content...' + : 'Error while loading your profile content! Retry or contact you IT admin.'} + + + ) + } else if (process.env.NEXT_PUBLIC_EDIT_ENABLED === 'false') { + redirect('/generate') + } else { + return ( + + + + + + + + + + + ) + } +} diff --git a/src/app/(studio)/generate/page.tsx b/src/app/(studio)/generate/page.tsx new file mode 100644 index 00000000..0e498e55 --- /dev/null +++ b/src/app/(studio)/generate/page.tsx @@ -0,0 +1,416 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use client' + +import * as React from 'react' +import Grid from '@mui/material/Grid2' +import Box from '@mui/material/Box' +import GenerateForm from '../../ui/generate-components/GenerateForm' +import { useEffect, useRef, useState } from 'react' +import { imageGenerationUtils, ImageI, ImageRandomPrompts } from '../../api/generate-image-utils' +import OutputImagesDisplay from '../../ui/transverse-components/ImagenOutputImagesDisplay' +import { appContextDataDefault, useAppContext } from '../../context/app-context' +import { Typography } from '@mui/material' + +import theme from '../../theme' +const { palette } = theme +import { + InterpolImageI, + OperationMetadataI, + VideoGenerationStatusResult, + videoGenerationUtils, + VideoI, + VideoRandomPrompts, +} from '@/app/api/generate-video-utils' +import { getVideoGenerationStatus } from '@/app/api/veo/action' +import { ChipGroup } from '@/app/ui/ux-components/InputChipGroup' +import OutputVideosDisplay from '@/app/ui/transverse-components/VeoOutputVideosDisplay' +import { downloadMediaFromGcs } from '@/app/api/cloud-storage/action' +import { getAspectRatio } from '@/app/ui/edit-components/EditImageDropzone' + +// Video Polling Constants +const INITIAL_POLLING_INTERVAL_MS = 6000 // Start polling after 6 seconds +const MAX_POLLING_INTERVAL_MS = 60000 // Max interval 60 seconds +const BACKOFF_FACTOR = 1.2 // Increase interval by 20% each time +const MAX_POLLING_ATTEMPTS = 30 // Max 30 attempts +const JITTER_FACTOR = 0.2 // Add up to 20% jitter + +export default function Page() { + const [generationMode, setGenerationMode] = useState('Generate an Image') + + const [isLoading, setIsLoading] = useState(false) + + const [generatedImages, setGeneratedImages] = useState([]) + const [generatedVideos, setGeneratedVideos] = useState([]) + const [generatedCount, setGeneratedCount] = useState(0) + + const [generationErrorMsg, setGenerationErrorMsg] = useState('') + const { appContext, error: appContextError, setAppContext } = useAppContext() + + // Handle 'Replay prompt' from Library + const [initialPrompt, setInitialPrompt] = useState(null) + useEffect(() => { + if (appContext && appContext.promptToGenerateImage) { + setGenerationMode('Generate an Image') + setInitialPrompt(appContext.promptToGenerateImage) + // Re-initialize parameter in context + setAppContext((prevContext) => { + if (prevContext) return { ...prevContext, promptToGenerateImage: '' } + else return { ...appContextDataDefault, promptToGenerateImage: '' } + }) + } + if (appContext && appContext.promptToGenerateVideo) { + setGenerationMode('Generate a Video') + setInitialPrompt(appContext.promptToGenerateVideo) + // Re-initialize parameter in context + setAppContext((prevContext) => { + if (prevContext) return { ...prevContext, promptToGenerateVideo: '' } + else return { ...appContextDataDefault, promptToGenerateVideo: '' } + }) + } + }, [appContext?.promptToGenerateImage, appContext?.promptToGenerateVideo]) + + // Handle Image to video from generated or edited image + const [initialITVimage, setInitialITVimage] = useState(null) + useEffect(() => { + const fetchAndSetImage = async () => { + if (appContext && appContext.imageToVideo) { + setGenerationMode('Generate a Video') + try { + const { data } = await downloadMediaFromGcs(appContext.imageToVideo) + const newImage = `data:image/png;base64,${data}` + + const img = new window.Image() + + img.onload = () => { + const width = img.width + const height = img.height + const ratio = getAspectRatio(img.width, img.height) + + const initialITVimage = { + format: 'png', + base64Image: newImage, + purpose: 'first', + ratio: ratio, + width: width, + height: height, + } + + data && setInitialITVimage(initialITVimage as InterpolImageI) + + // Re-initialize parameter in context + setAppContext((prevContext) => { + if (prevContext) return { ...prevContext, imageToVideo: '' } + else return { ...appContextDataDefault, imageToVideo: '' } + }) + } + + img.onerror = () => { + throw Error('Error loading image for dimension calculation.') + } + + img.src = newImage + } catch (error) { + console.error('Error fetching image:', error) + } + } + } + + fetchAndSetImage() + }, [appContext?.imageToVideo]) + + // Video Polling State + const [pollingOperationName, setPollingOperationName] = useState(null) + const [operationMetadata, setOperationMetadata] = useState(null) + const timeoutIdRef = useRef(null) + const pollingAttemptsRef = useRef(0) + const currentPollingIntervalRef = useRef(INITIAL_POLLING_INTERVAL_MS) + + // Handler for switching generation mode + const generationModeSwitch = ({ clickedValue }: { clickedValue: string }) => { + if (clickedValue !== generationMode && !isLoading) { + setGenerationMode(clickedValue) + setGenerationErrorMsg('') + setGeneratedImages([]) + setGeneratedVideos([]) + setInitialPrompt(null) + // Ensure polling state is reset if switching mode + if (timeoutIdRef.current) { + clearTimeout(timeoutIdRef.current) + timeoutIdRef.current = null + } + setPollingOperationName(null) + setOperationMetadata(null) + setIsLoading(false) + + if (clickedValue === 'Generate an Image') setInitialITVimage(null) + } + } + + // Handler called when GenerateForm starts ANY request + const handleRequestSent = (loading: boolean, count: number) => { + setIsLoading(loading) + setGenerationErrorMsg('') + setGeneratedCount(count) + setGeneratedImages([]) + setGeneratedVideos([]) + } + + // Handler called on ANY final error (initial or polling) or polling timeout + const handleNewErrorMsg = (newErrorMsg: string) => { + setIsLoading(false) + setGenerationErrorMsg(newErrorMsg) + + if (timeoutIdRef.current) { + clearTimeout(timeoutIdRef.current) + timeoutIdRef.current = null + } + setPollingOperationName(null) // Stop further polling by clearing operation name + setOperationMetadata(null) + } + + // Handler for successful IMAGE generation completion + const [isPromptReplayAvailable, setIsPromptReplayAvailable] = useState(true) + const handleImageGeneration = (newImages: ImageI[]) => { + setGeneratedImages(newImages) + setIsLoading(false) + setGeneratedVideos([]) + setGenerationErrorMsg('') + } + + // Handler for successful VIDEO generation completion (called by polling effect) + const handleVideoGenerationComplete = (newVideos: VideoI[]) => { + setGeneratedVideos(newVideos) + setGeneratedImages([]) + setGenerationErrorMsg('') + // isLoading is set to false within the polling effect's stopPolling call + } + + // Handler called by GenerateForm ONLY when video generation is initiated successfully + const handleVideoPollingStart = (operationName: string, metadata: OperationMetadataI) => { + // Clear any existing polling timeout if a new generation starts + if (timeoutIdRef.current) { + clearTimeout(timeoutIdRef.current) + timeoutIdRef.current = null + } + setPollingOperationName(operationName) + setOperationMetadata(metadata) + pollingAttemptsRef.current = 0 + currentPollingIntervalRef.current = INITIAL_POLLING_INTERVAL_MS + // setIsLoading(true) is handled by onRequestSent + } + + // Video generation polling useEffect + useEffect(() => { + // Stop polling and reset relevant states + const stopPolling = (isSuccess: boolean, finalLoadingState = false) => { + if (timeoutIdRef.current) { + clearTimeout(timeoutIdRef.current) + timeoutIdRef.current = null + } + + setIsLoading(finalLoadingState) + } + + // Function to perform one poll attempt + const poll = async () => { + if (!pollingOperationName || !operationMetadata) { + console.warn('Poll called without active operation details.') + stopPolling(false, false) + return + } + + // Timeout check + if (pollingAttemptsRef.current >= MAX_POLLING_ATTEMPTS) { + console.error(`Polling timeout for operation: ${pollingOperationName} after ${MAX_POLLING_ATTEMPTS} attempts.`) + handleNewErrorMsg( + `Video generation timed out after ${MAX_POLLING_ATTEMPTS} attempts. Please check operation status manually or try again.` + ) + + return + } + + pollingAttemptsRef.current++ + + try { + const statusResult: VideoGenerationStatusResult = await getVideoGenerationStatus( + pollingOperationName, + appContext, + operationMetadata.formData, + operationMetadata.prompt + ) + + // If pollingOperationName became null while waiting (e.g., user switched mode or cancelled) + if (!pollingOperationName) { + console.log('Polling stopped externally (operation name cleared) during async operation.') + stopPolling(false, false) + return + } + + if (statusResult.done) { + if (statusResult.error) { + handleNewErrorMsg(statusResult.error) + } else if (statusResult.videos && statusResult.videos.length > 0) { + handleVideoGenerationComplete(statusResult.videos) + stopPolling(true, false) + setPollingOperationName(null) + setOperationMetadata(null) + } else { + console.warn( + `Polling done, but no videos or error for ${pollingOperationName}. Videos array empty or undefined.` + ) + handleNewErrorMsg('Video generation finished, but no valid results were returned.') + } + } else { + // Not done, schedule next poll with exponential backoff + const jitter = currentPollingIntervalRef.current * JITTER_FACTOR * (Math.random() - 0.5) // Symmetrical jitter + const nextInterval = Math.round(currentPollingIntervalRef.current + jitter) + + timeoutIdRef.current = setTimeout(poll, nextInterval) + + // Increase interval for the subsequent attempt + currentPollingIntervalRef.current = Math.min( + currentPollingIntervalRef.current * BACKOFF_FACTOR, + MAX_POLLING_INTERVAL_MS + ) + } + } catch (error: any) { + console.error( + `Error during polling attempt ${pollingAttemptsRef.current} for ${pollingOperationName}:`, + error.response?.data || error.message || error + ) + if (!pollingOperationName) { + // Check if polling was stopped externally + console.log('Polling stopped externally (operation name cleared) during async error handling.') + stopPolling(false, false) + return + } + // Use error.message if available, otherwise a generic fallback + const errorMessage = error.message || 'An unexpected error occurred while checking the video status.' + handleNewErrorMsg(errorMessage) + } + } + + // Start polling only when an operation name is set and no timeout is currently active + if (pollingOperationName && !timeoutIdRef.current) { + // Resetting attempts and interval is now done in handleVideoPollingStart + // pollingAttemptsRef.current = 0; + // currentPollingIntervalRef.current = INITIAL_POLLING_INTERVAL_MS; + + // Initial poll, subsequent polls are scheduled by poll() itself via setTimeout + timeoutIdRef.current = setTimeout(poll, currentPollingIntervalRef.current) + } + + // Cleanup: Clear timeout if component unmounts or pollingOperationName becomes null + return () => { + if (timeoutIdRef.current) { + clearTimeout(timeoutIdRef.current) + console.log( + `Cleaned up active polling timeout on effect cleanup/re-run for ${ + pollingOperationName || 'previous operation' + }` + ) + timeoutIdRef.current = null + } + } + }, [pollingOperationName, operationMetadata, appContext]) + + if (appContext?.isLoading === true) { + return ( + + + {appContextError === null + ? 'Loading your profile content...' + : 'Error while loading your profile content! Retry or contact you IT admin.'} + + + ) + } + + return ( + + + + + + {generationMode === 'Generate an Image' && ( + + )} + + {process.env.NEXT_PUBLIC_VEO_ENABLED === 'true' && generationMode === 'Generate a Video' && ( + + )} + + + {generationMode === 'Generate an Image' ? ( + + ) : ( + + )} + + + + ) +} diff --git a/src/app/(studio)/layout.tsx b/src/app/(studio)/layout.tsx new file mode 100644 index 00000000..43b15f2f --- /dev/null +++ b/src/app/(studio)/layout.tsx @@ -0,0 +1,27 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use client' + +import SideNav from '../ui/transverse-components/SideNavigation' +import Box from '@mui/material/Box' + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + ) +} diff --git a/src/app/(studio)/library/page.tsx b/src/app/(studio)/library/page.tsx new file mode 100644 index 00000000..fd48fe30 --- /dev/null +++ b/src/app/(studio)/library/page.tsx @@ -0,0 +1,365 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use client' + +import * as React from 'react' +import Box from '@mui/material/Box' +import { useCallback, useEffect, useState } from 'react' + +import { Button, Collapse, IconButton, Stack, Typography } from '@mui/material' + +import theme from '../../theme' +import { getSignedURL } from '@/app/api/cloud-storage/action' +import { ExportAlerts } from '@/app/ui/transverse-components/ExportAlerts' +import { fetchDocumentsInBatches, firestoreDeleteBatch } from '@/app/api/firestore/action' +import { MediaMetadataI, MediaMetadataWithSignedUrl } from '@/app/api/export-utils' +import LibraryMediasDisplay from '../../ui/library-components/LibraryMediasDisplay' +import LibraryFiltering from '../../ui/library-components/LibraryFiltering' +import { CustomizedSendButton } from '@/app/ui/ux-components/Button-SX' +import { Autorenew, Close, Delete, TouchApp, WatchLater } from '@mui/icons-material' +const { palette } = theme + +const iconSx = { + fontSize: '1.4rem', + color: palette.secondary.main, + position: 'center', + '&:hover': { + color: palette.primary.main, + fontSize: '1.5rem', + }, +} + +export default function Page() { + const [errorMsg, setErrorMsg] = useState('') + const [isMediasLoading, setIsMediasLoading] = useState(false) + const [fetchedMediasByPage, setFetchedMediasByPage] = useState([]) + const [lastVisibleDocument, setLastVisibleDocument] = useState(null) + const [isMorePageToLoad, setIsMorePageToLoad] = useState(false) + const [filters, setFilters] = useState(null) + const [openFilters, setOpenFilters] = useState(false) + + // State for deletion flow + const [deletionStatus, setDelStatus] = useState<'init' | 'selecting' | 'deleting'>('init') + const [deletionSuccess, setDeletionSuccess] = useState(false) + const [selectedIdsForDeletion, setSelectedIdsForDeletion] = useState([]) + + const fetchDataAndSignedUrls = useCallback( + async (currentFiltersArg: any, explicitFetchCursor: any | null, isReplacingExistingData: boolean) => { + setIsMediasLoading(true) + if (isReplacingExistingData) { + setIsMorePageToLoad(false) + setFetchedMediasByPage([]) + setLastVisibleDocument(null) + } + + const selectedFilters = Object.entries(currentFiltersArg ?? {}) + .filter(([, value]) => (Array.isArray(value) ? value.length > 0 : value !== undefined && value !== '')) + .reduce((acc, [key, value]) => { + acc[key] = value + return acc + }, {} as any) + + try { + let res + if (Object.values(selectedFilters).length === 0) res = await fetchDocumentsInBatches(explicitFetchCursor) + else res = await fetchDocumentsInBatches(explicitFetchCursor, selectedFilters) + + if (res.error) throw Error(res.error.replaceAll('Error: ', '')) + + const documents = res.thisBatchDocuments || [] + + // This is a check to workaround the fact the document structure changed in Firestore when making content more generic: + // from image only to medias (images + videos) + if (isReplacingExistingData) { + const hasOldFormat = documents.some((doc: any) => Object.prototype.hasOwnProperty.call(doc, 'imageID')) // 'imageID' is a old property name + + if (hasOldFormat) { + setErrorMsg( + "Attention: To ensure compatibility with the new Veo features in ImgStudio, the Firestore metadata database must be updated. Please have your system administrator execute the instructions provided in library-update-script.md, located in the app's code repository." + ) + setIsMediasLoading(false) + setFetchedMediasByPage([]) + setLastVisibleDocument(null) + setIsMorePageToLoad(false) + return + } + } + + // Case were no media were fetched + if (isReplacingExistingData && documents.length === 0) { + setErrorMsg('Sorry, your search returned no results') + setFetchedMediasByPage([]) + setIsMorePageToLoad(false) + setLastVisibleDocument(null) + setIsMediasLoading(false) + return + } + + setErrorMsg('') + + const documentsWithSignedUrlsPromises = documents.map( + async (doc: { gcsURI: string; videoThumbnailGcsUri?: string }) => { + if (!doc.gcsURI) return { ...doc, signedUrl: '' } as MediaMetadataWithSignedUrl + try { + const signedUrlResult = await getSignedURL(doc.gcsURI) + + if (signedUrlResult.error) throw Error(String(signedUrlResult.error).replaceAll('Error: ', '')) + const finalSignedUrl = typeof signedUrlResult === 'string' ? signedUrlResult : signedUrlResult.url + + let finalThumbnailSignedUrl = null + if (doc.videoThumbnailGcsUri) { + const thumbnailResult = await getSignedURL(doc.videoThumbnailGcsUri) + if (thumbnailResult.error) throw Error(String(thumbnailResult.error).replaceAll('Error: ', '')) + finalThumbnailSignedUrl = typeof thumbnailResult === 'string' ? thumbnailResult : thumbnailResult.url + } + return { + ...doc, + signedUrl: finalSignedUrl, + videoThumbnailSignedUrl: finalThumbnailSignedUrl, + } as MediaMetadataWithSignedUrl + } catch (error) { + console.error('Error fetching signed URL for a document:', doc.gcsURI, error) + return { ...doc, signedUrl: '' } as MediaMetadataWithSignedUrl + } + } + ) + const documentsWithSignedUrls = (await Promise.all(documentsWithSignedUrlsPromises)).filter( + (doc) => !doc.gcsURIError + ) + + setLastVisibleDocument(res.lastVisibleDocument) + setIsMorePageToLoad(res.isMorePageToLoad || false) + + setFetchedMediasByPage((prevPages) => { + if (isReplacingExistingData) return [documentsWithSignedUrls] + else return prevPages.concat([documentsWithSignedUrls]) + }) + } catch (error: any) { + console.error(error) + setErrorMsg(`An error occurred while fetching medias. Please try again.`) + + if (isReplacingExistingData) { + setFetchedMediasByPage([]) + setLastVisibleDocument(null) + } + } finally { + setIsMediasLoading(false) + } + }, + [] + ) + + const triggerFetch = useCallback((newFilters: any) => { + setErrorMsg('') + setOpenFilters(false) + setFilters(newFilters) + }, []) + + useEffect(() => { + if (lastVisibleDocument === null && filters !== null) fetchDataAndSignedUrls(filters, null, true) + }, [filters, lastVisibleDocument, fetchDataAndSignedUrls]) + + useEffect(() => { + setIsMediasLoading(true) + setFetchedMediasByPage([]) + setLastVisibleDocument(null) + fetchDataAndSignedUrls({}, null, true) + }, [fetchDataAndSignedUrls]) + + const handleLoadMore = useCallback(async () => { + if (lastVisibleDocument && isMorePageToLoad) { + await fetchDataAndSignedUrls(filters ?? {}, lastVisibleDocument, false) + } + }, [lastVisibleDocument, isMorePageToLoad, filters, fetchDataAndSignedUrls]) + + // Deletion handlers + const handleDeletion = useCallback(async () => { + if (deletionStatus === 'init') { + setDelStatus('selecting') + setSelectedIdsForDeletion([]) + setDeletionSuccess(false) + setErrorMsg('') + return + } else if (deletionStatus === 'selecting') { + if (selectedIdsForDeletion.length === 0) { + setDelStatus('init') + return + } + setDelStatus('deleting') + setErrorMsg('') + setDeletionSuccess(false) + try { + const allFetchedMedias: MediaMetadataI[] = fetchedMediasByPage.flat() + const result = await firestoreDeleteBatch(selectedIdsForDeletion, allFetchedMedias) + + if (result === true) { + setFetchedMediasByPage([]) + setLastVisibleDocument(null) + setIsMorePageToLoad(false) + + await fetchDataAndSignedUrls({}, null, true) + + setDeletionSuccess(true) + setSelectedIdsForDeletion([]) + setDelStatus('init') + } else if (typeof result === 'object' && 'error' in result) throw new Error(result.error) + else throw new Error('Deletion completed with an unknown status.') // Unexpected result + } catch (error: any) { + console.error('Deletion failed:', error) + setErrorMsg('An error occurred during deletion. Please try again.') + setDeletionSuccess(false) + setDelStatus('init') + } + } + }, [deletionStatus, selectedIdsForDeletion, fetchedMediasByPage, filters, fetchDataAndSignedUrls]) + + const handleMediaDeletionSelect = useCallback( + (docId: string) => { + if (deletionStatus !== 'selecting') return + + setSelectedIdsForDeletion((prevSelectedIds) => + prevSelectedIds.includes(docId) ? prevSelectedIds.filter((id) => id !== docId) : [...prevSelectedIds, docId] + ) + }, + [deletionStatus] + ) + const [displayedAlertProps, setDisplayedAlertProps] = useState<{ + message: string + style: 'success' | 'error' + } | null>(null) + + useEffect(() => { + if (deletionSuccess) setDisplayedAlertProps({ message: 'Media(s) deleted with success!', style: 'success' }) + else if (errorMsg !== '') setDisplayedAlertProps({ message: errorMsg, style: 'error' }) + }, [deletionSuccess, errorMsg]) + const alertOnClose = useCallback(() => { + if (displayedAlertProps?.style === 'success') setDeletionSuccess(false) + else if (displayedAlertProps?.style === 'error') setErrorMsg('') + }, [displayedAlertProps, setDeletionSuccess, setErrorMsg]) + + let delButtonLabel = 'Batch Delete' + if (deletionStatus === 'selecting') { + if (selectedIdsForDeletion.length > 0) + delButtonLabel = `Delete ${selectedIdsForDeletion.length} media${selectedIdsForDeletion.length > 1 ? 's' : ''}` + else delButtonLabel = 'Select media(s)' + } else if (deletionStatus === 'deleting') delButtonLabel = 'Deleting...' + + return ( + + + + {'Library/'} + + + {'Shared content'} + + + { + setDisplayedAlertProps(null) + }} + > + {displayedAlertProps && ( + + )} + + + + triggerFetch(filters)} + openFilters={openFilters} + setOpenFilters={setOpenFilters} + /> + + {deletionStatus === 'selecting' && ( + 0 + ? () => setSelectedIdsForDeletion([]) // Handler when items are selected + : () => setDelStatus('init') // Handler when no items are selected + } + aria-label="Reset delete selection" + disableRipple + sx={{ + px: 0.5, + }} + > + {selectedIdsForDeletion.length > 0 ? : } + + )} + + + + + + + + ) +} diff --git a/src/app/api/cloud-storage/action.tsx b/src/app/api/cloud-storage/action.tsx new file mode 100644 index 00000000..7243d526 --- /dev/null +++ b/src/app/api/cloud-storage/action.tsx @@ -0,0 +1,305 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use server' + +const ffmpegInstaller = require('@ffmpeg-installer/ffmpeg') +const ffprobeInstaller = require('@ffprobe-installer/ffprobe') +import ffmpeg from 'fluent-ffmpeg' +import fs from 'fs/promises' +import path from 'path' +import os from 'os' +ffmpeg.setFfmpegPath(ffmpegInstaller.path) +ffmpeg.setFfprobePath(ffprobeInstaller.path) + +const { Storage } = require('@google-cloud/storage') + +interface optionsI { + version: 'v2' | 'v4' + action: 'read' | 'write' | 'delete' | 'resumable' + expires: number +} +const projectId = process.env.NEXT_PUBLIC_PROJECT_ID + +export async function decomposeUri(uri: string) { + const sourceUriParts = uri.replace('gs://', '').split('/') + const sourceBucketName = sourceUriParts[0] + const sourceObjectName = sourceUriParts.slice(1).join('/') + + return { + bucketName: sourceBucketName, + fileName: sourceObjectName, + } +} + +export async function getSignedURL(gcsURI: string) { + const { bucketName, fileName } = await decomposeUri(gcsURI) + + const storage = new Storage({ projectId }) + + const options: optionsI = { + version: 'v4', + action: 'read', + expires: Date.now() + 60 * 60 * 1000, + } + + try { + const [url] = await storage.bucket(bucketName).file(fileName).getSignedUrl(options) + return url + } catch (error) { + console.error(error) + return { + error: 'Error while getting secured access to content.', + } + } +} + +export async function copyImageToTeamBucket(sourceGcsUri: string, id: string) { + const storage = new Storage({ projectId }) + + try { + if (!sourceGcsUri || !sourceGcsUri.startsWith('gs://')) { + console.error('Invalid source GCS URI provided:', sourceGcsUri) + return { + error: 'Invalid source GCS URI format. It must start with gs://', + } + } + if (!id) { + console.error('Invalid id provided:', id) + return { + error: 'Invalid id. It cannot be empty.', + } + } + + const { bucketName, fileName } = await decomposeUri(sourceGcsUri) + + const destinationBucketName = process.env.NEXT_PUBLIC_TEAM_BUCKET + + if (!bucketName || !fileName || !destinationBucketName) throw new Error('Invalid source or destination URI.') + + const sourceObject = storage.bucket(bucketName).file(fileName) + const destinationBucket = storage.bucket(destinationBucketName) + const destinationFile = destinationBucket.file(id) + + // Check if file already exists in destination bucket, if not copy it + const [exists] = await destinationFile.exists() + if (!exists) await sourceObject.copy(destinationFile) + + return `gs://${destinationBucketName}/${id}` + } catch (error) { + console.error(error) + return { + error: 'Error while moving media to team Library', + } + } +} + +export async function downloadMediaFromGcs(gcsUri: string): Promise<{ data?: string; error?: string }> { + const storage = new Storage() + + if (!gcsUri || !gcsUri.startsWith('gs://')) { + console.error('Invalid GCS URI provided:', gcsUri) + return { + error: 'Invalid GCS URI format. It must start with gs://', + } + } + + try { + const { bucketName, fileName } = await decomposeUri(gcsUri) + + if (!bucketName || !fileName) { + console.error('Could not determine bucket name or file name from URI:', gcsUri) + return { + error: 'Invalid GCS URI, could not extract bucket or file name.', + } + } + + const [fileBuffer] = await storage.bucket(bucketName).file(fileName).download() + const base64Data = fileBuffer.toString('base64') + + return { + data: base64Data, + } + } catch (error: any) { + console.error('Error during GCS file download:', error) + + const errorMessage = error.message || 'Error while downloading the media' + return { + error: errorMessage, + } + } +} + +export async function downloadTempVideo(gcsUri: string): Promise { + const storage = new Storage() + + const { bucketName, fileName } = await decomposeUri(gcsUri) + + const tempFileName = `video_${Date.now()}_${path.basename(fileName)}` + const tempFilePath = path.join(os.tmpdir(), tempFileName) + + await storage.bucket(bucketName).file(fileName).download({ + destination: tempFilePath, + }) + + return tempFilePath +} + +export async function fetchJsonFromStorage(gcsUri: string) { + const storage = new Storage({ projectId }) + + try { + const { bucketName, fileName } = await decomposeUri(gcsUri) + + const bucket = storage.bucket(bucketName) + const file = bucket.file(fileName) + + const [contents] = await file.download() + + const jsonData = JSON.parse(contents.toString()) + return jsonData + } catch (error) { + console.error('Error fetching JSON from storage:', error) + if (error instanceof SyntaxError) { + console.error('JSON parsing error. Downloaded content might not be valid JSON.') + } + throw error + } +} + +export async function uploadBase64Image( + base64Image: string, + bucketName: string, + objectName: string, + contentType: string = 'image/png' +): Promise<{ success?: boolean; message?: string; error?: string; fileUrl?: string }> { + const storage = new Storage({ projectId }) + + if (!base64Image) return { error: 'Invalid base64 data.' } + + const imageBuffer = Buffer.from(base64Image, 'base64') + const options = { + destination: objectName, + metadata: { + contentType: contentType, + }, + } + + try { + await storage.bucket(bucketName).file(objectName).save(imageBuffer, options) + + const fileUrl = `gs://${bucketName}/${objectName}` + + return { + success: true, + message: `File uploaded to: ${fileUrl}`, + fileUrl: fileUrl, + } + } catch (error) { + console.error('Error uploading file:', error) + return { + error: 'Error uploading file to Google Cloud Storage.', + } + } +} + +export async function getVideoThumbnailBase64( + videoSourceGcsUri: string, + ratio: string +): Promise<{ thumbnailBase64Data?: string; mimeType?: string; error?: string }> { + const outputMimeType = 'image/png' + const tempThumbnailFileName = `thumbnail_${Date.now()}.png` + const tempThumbnailPath = path.join(os.tmpdir(), tempThumbnailFileName) + + let localVideoPath: string | null = null + + try { + // 1. Ensure video is locally accessible + localVideoPath = await downloadTempVideo(videoSourceGcsUri) + + if (!localVideoPath) throw Error('Failed to download video') + + // 2. Use FFmpeg to extract the thumbnail + await new Promise((resolve, reject) => { + let command = ffmpeg(localVideoPath!).seekInput('00:00:01').frames(1) // Extract a single frame + + const size = ratio === '16:9' ? '320x180' : '180x320' + command = command.size(size) + command = command.outputFormat('image2') + + command + .output(tempThumbnailPath) + .on('end', () => { + resolve() + }) + .on('error', (err: { message: any }) => { + console.error('FFmpeg Error:', err.message) + reject(new Error(`FFmpeg failed to extract thumbnail: ${err.message}`)) + }) + .run() + }) + + // 3. Read the generated thumbnail file into a buffer + const thumbnailBuffer = await fs.readFile(tempThumbnailPath) + + // 4. Convert buffer to base64 string + const thumbnailBase64Data = thumbnailBuffer.toString('base64') + + return { + thumbnailBase64Data, + mimeType: outputMimeType, + } + } catch (error: any) { + console.error('Error in getVideoThumbnailBase64:', error) + return { error: error.message || 'An unexpected error occurred while generating thumbnail.' } + } finally { + // 5. Cleanup temporary files + if (localVideoPath) + await fs + .unlink(localVideoPath) + .catch((err: any) => console.error(`Failed to delete temp video file: ${localVideoPath}`, err)) + + // Attempt to delete the temp thumbnail even if an error occurred earlier + await fs.unlink(tempThumbnailPath).catch((err: { code: string }) => { + if (err.code !== 'ENOENT') console.error(`Failed to delete temp thumbnail file: ${tempThumbnailPath}`, err) + }) + } +} + +export async function deleteMedia(gcsURI: string): Promise { + const storage = new Storage({ projectId }) + + if (!gcsURI || !gcsURI.startsWith('gs://')) return { error: 'Invalid GCS URI. It must start with "gs://".' } + + const { bucketName, fileName: objectName } = await decomposeUri(gcsURI) + + if (!bucketName || !objectName) return { error: 'Invalid GCS URI' } + + try { + await storage.bucket(bucketName).file(objectName).delete() + + return true + } catch (error: any) { + console.error(`Error deleting file ${gcsURI} from GCS:`, error) + + if (error.code === 404) + return { + error: `File ${gcsURI} not found in Google Cloud Storage.`, + } + + return { + error: `An error occurred while deleting file ${gcsURI} from Google Cloud Storage.`, + } + } +} diff --git a/src/app/api/edit-utils.tsx b/src/app/api/edit-utils.tsx new file mode 100644 index 00000000..95b0d37a --- /dev/null +++ b/src/app/api/edit-utils.tsx @@ -0,0 +1,345 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export interface EditImageFieldStyleI { + type: string + label: string + description?: string + default?: number | string + min?: number + max?: number + step?: number + isDataResetable: boolean + options?: + | { + value: string + label: string + indication?: string + description?: string + mandatoryPrompt?: boolean + mandatoryMask?: boolean + maskType?: string[] + }[] + | string[] +} + +export interface EditImageFormFieldsI { + modelVersion: EditImageFieldStyleI + inputImage: EditImageFieldStyleI + inputMask: EditImageFieldStyleI + prompt: EditImageFieldStyleI + sampleCount: EditImageFieldStyleI + negativePrompt: EditImageFieldStyleI + editMode: EditImageFieldStyleI + maskDilation: EditImageFieldStyleI + baseSteps: EditImageFieldStyleI + outputOptions: EditImageFieldStyleI + personGeneration: EditImageFieldStyleI +} + +export const EditImageFormFields = { + modelVersion: { + type: 'select', + label: 'Model version', + default: 'imagen-3.0-capability-001', + options: [ + { + value: 'imagen-3.0-capability-001', + label: 'Imagen 3', + indication: '', + }, + ], + isDataResetable: false, + }, + inputImage: { + type: 'base64 encoded string', + label: 'Input image', + isDataResetable: true, + }, + inputMask: { + type: 'base64 encoded string', + label: 'Input mask', + isDataResetable: true, + }, + prompt: { + type: 'textInput', + label: 'Prompt', + isDataResetable: true, + }, + sampleCount: { + label: 'Quantity of outputs', + type: 'chip-group', + default: '4', + options: ['1', '2', '3', '4'], + isDataResetable: false, + }, + negativePrompt: { + label: 'Negative prompt', + type: 'textInput', + isDataResetable: true, + }, + editMode: { + type: 'in-place-menu', + label: 'What do you want to do with your image?', + default: 'EDIT_MODE_INPAINT_INSERTION', + options: [ + { + value: 'EDIT_MODE_INPAINT_INSERTION', + label: 'Insert', + description: 'Add a new object', + icon: 'add_photo_alternate', + mandatoryPrompt: true, + promptIndication: 'Prompt - Describe what you want to insert to selected zone', + mandatoryMask: true, + maskButtonLabel: 'Select zone', + maskButtonIcon: 'ads_click', + maskDialogTitle: 'Select a zone where to insert', + maskDialogIndication: 'Only pixels within the zone can and will be edited', + maskType: ['manual', 'background', 'foreground', 'semantic', 'interactive', 'prompt'], + enabled: true, + defaultMaskDilation: 0.01, + defaultBaseSteps: 35, + }, + { + value: 'EDIT_MODE_INPAINT_REMOVAL', + label: 'Remove', + description: 'Erase selected object(s)', + icon: 'cancel', + mandatoryPrompt: false, + mandatoryMask: true, + maskButtonLabel: 'Select object(s)', + maskButtonIcon: 'category', + maskDialogTitle: 'Select object(s) to be removed', + maskDialogIndication: 'Only selected pixels within can and will be edited', + maskType: ['manual', 'background', 'foreground', 'semantic', 'interactive', 'prompt'], + enabled: true, + defaultMaskDilation: 0.01, + defaultBaseSteps: 12, + }, + { + value: 'EDIT_MODE_OUTPAINT', + label: 'Outpaint', + description: 'Extend the image', + icon: 'aspect_ratio', + mandatoryPrompt: false, + promptIndication: '(Optional) Prompt - If you want, be specific on what to put in extended space', + mandatoryMask: true, + maskButtonLabel: 'New ratio', + maskButtonIcon: 'crop', + maskDialogTitle: 'Select your new image format', + maskDialogIndication: 'Only pixels in outpaint zone will be edited', + maskType: ['outpaint'], // Vertical/ Horizontal zones generated from new ratio and image position within it + enabled: true, + defaultMaskDilation: 0.03, + defaultBaseSteps: 35, + }, + { + value: 'EDIT_MODE_BGSWAP', + label: 'Product showcase', + description: 'Swap image background', + icon: 'store', + mandatoryPrompt: true, + promptIndication: 'Prompt - Describe in what situation you want to put the product', + mandatoryMask: false, + enabled: true, + defaultMaskDilation: 0.0, + defaultBaseSteps: 75, + }, + { + value: 'EDIT_MODE_DEFAULT', + label: 'Transform', + description: "Change what's happening", + icon: 'model_training', + mandatoryPrompt: true, + promptIndication: 'Prompt - Describe what you want to see change in the image', + mandatoryMask: false, + enabled: false, + defaultMaskDilation: 0.01, + defaultBaseSteps: 35, + }, + ], + isDataResetable: false, + }, + maskDilation: { + type: 'float', + label: 'Mask dilation', + description: 'Determines the dilation percentage of the mask provided', + default: 0.01, + min: 0.0, + max: 0.3, + step: 0.01, + isDataResetable: true, + }, + baseSteps: { + type: 'integer', + label: 'Base steps', + description: 'Controls how many steps should be used to generate output', + default: 35, + min: 1, + max: 100, + step: 1, + isDataResetable: true, + }, + outputOptions: { + label: 'Ouput format', + type: 'select', + default: 'image/png', + options: [ + { + value: 'image/png', + label: 'PNG', + }, + { + value: 'image/jpeg', + label: 'JPEG', + }, + ], + isDataResetable: false, + }, + personGeneration: { + label: 'People generation', + type: 'select', + default: 'allow_adult', + options: [ + { + value: 'dont_allow', + label: 'No people', + }, + { + value: 'allow_adult', + label: 'Adults only', + }, + { + value: 'allow_all', + label: 'Adults & Children', + }, + ], + isDataResetable: false, + }, +} + +export const maskTypes = [ + { + value: 'manual', + label: 'Manual selection', + description: 'One or more zone(s) you manually brush over', + readOnlyCanvas: false, + requires: 'manualSelection', + }, + { + value: 'background', + label: 'Background', + description: 'Everything except the primary object, person, or subject', + readOnlyCanvas: true, + }, + { + value: 'foreground', + label: 'Foreground', + description: 'Primary object, person, or subject only', + readOnlyCanvas: true, + }, + /*{ //TODO to be added back when feature fixed + value: 'semantic', + label: 'Semantic', + description: 'One or more element(s) by their semantic class(es)', + readOnlyCanvas: true, + requires: 'semanticDropdown', + },*/ + { + value: 'interactive', + label: 'Interactive', + description: 'A zone targetted by circling or brushing over it', + readOnlyCanvas: false, + requires: 'manualSelection', + }, + { + value: 'prompt', + label: 'Descriptive', + description: 'A zone targetted through a written description of it', + readOnlyCanvas: true, + requires: 'promptInput', + }, + { + value: 'outpaint', + label: 'Configure outpaint zone', + description: 'A new image format to be edited in', + readOnlyCanvas: true, + requires: 'ratioSelection', + }, +] + +export const semanticClasses = [ + { class_id: 43, value: 'Floor' }, + { class_id: 94, value: 'Gravel' }, + { class_id: 95, value: 'Platform' }, + { class_id: 96, value: 'Playingfield' }, + { class_id: 186, value: 'River Lake' }, + { class_id: 98, value: 'Road' }, + { class_id: 101, value: 'Runway' }, + { class_id: 187, value: 'Sea' }, + { class_id: 100, value: 'Sidewalk Pavement' }, + { class_id: 142, value: 'Sky' }, + { class_id: 99, value: 'Snow' }, + { class_id: 189, value: 'Swimming Pool' }, + { class_id: 102, value: 'Terrain' }, + { class_id: 191, value: 'Wall' }, + { class_id: 188, value: 'Water' }, + { class_id: 190, value: 'Waterfall' }, +] + +// Interface of Edit form fields +export interface EditImageFormI { + modelVersion: string + inputImage: string + ratio: string + width: number + height: number + inputMask: string + prompt: string + sampleCount: string + negativePrompt: string + editMode: string + maskMode?: string + maskDilation: string + baseSteps: string + outputOptions: string + personGeneration: string +} + +// Sort out Edit fields depending on purpose +export interface EditSettingsFieldsI { + sampleCount: EditImageFieldStyleI + maskDilation: EditImageFieldStyleI + baseSteps: EditImageFieldStyleI + outputOptions: EditImageFieldStyleI + personGeneration: EditImageFieldStyleI + negativePrompt: EditImageFieldStyleI +} +export const editSettingsFields: EditSettingsFieldsI = { + sampleCount: EditImageFormFields.sampleCount, + maskDilation: EditImageFormFields.maskDilation, + baseSteps: EditImageFormFields.baseSteps, + outputOptions: EditImageFormFields.outputOptions, + personGeneration: EditImageFormFields.personGeneration, + negativePrompt: EditImageFormFields.negativePrompt, +} + +// Set default values for Edit Form +const editFieldList: [keyof EditImageFormFieldsI] = Object.keys(EditImageFormFields) as [keyof EditImageFormFieldsI] +export var formDataEditDefaults: any +editFieldList.forEach((field) => { + const fieldParams: EditImageFieldStyleI = EditImageFormFields[field] + const defaultValue = 'default' in fieldParams ? fieldParams.default : '' + formDataEditDefaults = { ...formDataEditDefaults, [field]: defaultValue } +}) diff --git a/src/app/api/export-utils.tsx b/src/app/api/export-utils.tsx new file mode 100644 index 00000000..4438668a --- /dev/null +++ b/src/app/api/export-utils.tsx @@ -0,0 +1,192 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { ImageI } from './generate-image-utils' +import { VideoI } from './generate-video-utils' + +export interface ExportMediaFieldI { + label: string + name?: string + type: string + prop?: string + isUpdatable: boolean + isMandatory?: boolean + isExportVisible: boolean + isExploreVisible: boolean + options?: { + value: string + label: string + }[] +} + +export interface ExportMediaFormFieldsI { + id: ExportMediaFieldI + gcsURI: ExportMediaFieldI + creationDate: ExportMediaFieldI + leveragedModel: ExportMediaFieldI + author: ExportMediaFieldI + prompt: ExportMediaFieldI + format: ExportMediaFieldI + videoDuration: ExportMediaFieldI + videoThumbnailGcsUri: ExportMediaFieldI + aspectRatio: ExportMediaFieldI + upscaleFactor: ExportMediaFieldI + width: ExportMediaFieldI + height: ExportMediaFieldI + [key: string]: ExportMediaFieldI +} + +export const exportStandardFields: ExportMediaFormFieldsI = { + id: { + label: 'Media ID', + type: 'text-info', + prop: 'key', + isUpdatable: false, + isExportVisible: false, + isExploreVisible: false, + }, + gcsURI: { + label: 'Media GCS URI', + type: 'text-info', + prop: 'gcsUri', + isUpdatable: false, + isExportVisible: false, + isExploreVisible: false, + }, + creationDate: { + label: 'Generation date', + type: 'text-info', + prop: 'date', + isUpdatable: false, + isExportVisible: false, + isExploreVisible: true, + }, + leveragedModel: { + label: 'Leveraged model', + type: 'text-info', + prop: 'modelVersion', + isUpdatable: false, + isExportVisible: false, + isExploreVisible: true, + }, + mediaCreationMode: { + label: 'Creation mode', + type: 'text-info', + prop: 'mode', + isUpdatable: false, + isExportVisible: false, + isExploreVisible: true, + }, + author: { + label: 'Author', + type: 'text-info', + prop: 'author', + isUpdatable: false, + isExportVisible: false, + isExploreVisible: true, + }, + prompt: { + label: 'Prompt', + type: 'text-info', + prop: 'prompt', + isUpdatable: false, + isExportVisible: true, + isExploreVisible: true, + }, + format: { + label: 'Format', + type: 'text-info', + prop: 'format', + isUpdatable: false, + isExportVisible: true, + isExploreVisible: true, + }, + videoDuration: { + label: 'Duration (sec)', + type: 'text-info', + prop: 'duration', + isUpdatable: false, + isExportVisible: true, + isExploreVisible: true, + }, + videoThumbnailGcsUri: { + label: 'Video Thumbnail GCS URI', + type: 'text-info', + prop: 'videoThumbnailGcsUri', + isUpdatable: false, + isExportVisible: false, + isExploreVisible: false, + }, + aspectRatio: { + label: 'Ratio', + type: 'text-info', + prop: 'ratio', + isUpdatable: false, + isExportVisible: true, + isExploreVisible: true, + }, + upscaleFactor: { + label: 'Scale factor', + type: 'radio-button', + prop: 'upscaleFactor', + isUpdatable: false, + isExportVisible: true, + isExploreVisible: true, + }, + width: { + label: 'Width (px)', + type: 'text-info', + prop: 'width', + isUpdatable: false, + isExportVisible: true, + isExploreVisible: true, + }, + height: { + label: 'Height (px)', + type: 'text-info', + prop: 'height', + isUpdatable: false, + isExportVisible: true, + isExploreVisible: true, + }, +} + +export interface ExportMediaFormI { + mediaToExport: ImageI | VideoI + upscaleFactor: string + [key: string]: any +} + +export interface FilterMediaFormI { + [key: string]: any +} + +export interface MediaMetadataI { + id: string + gcsURI: string + creationDate: any + leveragedModel: string + author: string + prompt: string + format: string + videoDuration?: number + videoThumbnailGcsUri?: string + aspectRatio: string + upscaleFactor?: string + width: number + height: number + [key: string]: any +} + +export type MediaMetadataWithSignedUrl = MediaMetadataI & { signedUrl: string; videoThumbnailSignedUrl?: string } diff --git a/src/app/api/firestore/action.ts b/src/app/api/firestore/action.ts new file mode 100644 index 00000000..d4f77a0b --- /dev/null +++ b/src/app/api/firestore/action.ts @@ -0,0 +1,204 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use server' + +import { Timestamp } from '@google-cloud/firestore' +import { ExportMediaFormI, MediaMetadataI, ExportMediaFormFieldsI } from '../export-utils' +import { deleteMedia } from '../cloud-storage/action' + +const { Firestore, FieldValue } = require('@google-cloud/firestore') +const firestore = new Firestore() +firestore.settings({ ignoreUndefinedProperties: true }) + +export async function addNewFirestoreEntry( + entryID: string, + data: ExportMediaFormI, + ExportImageFormFields: ExportMediaFormFieldsI +) { + const document = firestore.collection('metadata').doc(entryID) + + let cleanData: MediaMetadataI = {} as MediaMetadataI + data = { ...data.mediaToExport, ...data } + let combinedFilters: string[] = [] + + if (ExportImageFormFields) { + Object.entries(ExportImageFormFields).forEach(([name, field]) => { + const sourceProp = field.prop || name + const valueFromData = data[sourceProp as keyof ExportMediaFormI] + let transformedValue = valueFromData + + if (Array.isArray(valueFromData) && valueFromData.every((item) => typeof item === 'string')) { + transformedValue = valueFromData.length > 0 ? Object.fromEntries(valueFromData.map((str) => [str, true])) : null + valueFromData.forEach((item) => combinedFilters.push(`${name}_${item}`)) + } + + cleanData[name as keyof MediaMetadataI] = transformedValue ?? null + }) + } + + const dataToSet = { + ...cleanData, + timestamp: FieldValue.serverTimestamp(), + combinedFilters: combinedFilters, + } + + try { + const res = await document.set(dataToSet, { ignoreUndefinedProperties: true }) + return res._writeTime._seconds + } catch (error) { + console.error(error) + return { + error: 'Error while setting new metadata entry to database.', + } + } +} + +export async function fetchDocumentsInBatches(lastVisibleDocument?: any, filters?: any) { + const batchSize = 24 + + const collection = firestore.collection('metadata') + let thisBatchDocuments: MediaMetadataI[] = [] + + let query = collection + + if (filters) { + const filterEntries = Object.entries(filters).filter(([, values]) => Array.isArray(values) && values.length > 0) + let combinedFilterEntries: string[] = [] + for (const [filterKey, filterValues] of filterEntries) { + ;(filterValues as string[]).forEach((filterValue) => { + combinedFilterEntries.push(filterKey + '_' + filterValue) + }) + } + query = query.where('combinedFilters', 'array-contains-any', combinedFilterEntries) + } + + query = query.orderBy('timestamp', 'desc').limit(batchSize) + + try { + if (lastVisibleDocument) { + query = query.startAfter( + new Timestamp( + Math.floor(lastVisibleDocument.timestamp / 1000), + (lastVisibleDocument.timestamp % 1000) * 1000000 + ) + ) + } + + const snapshot = await query.get() + + // No more documents + if (snapshot.empty) { + return { thisBatchDocuments: null, lastVisibleDocument: null, isMorePageToLoad: false } + } + + thisBatchDocuments = snapshot.docs.map((doc: { data: () => any }) => { + const data = doc.data() + delete data.timestamp + delete data.combinedFilters + return data as MediaMetadataI + }) + + const newLastVisibleDocument = { + id: snapshot.docs[snapshot.docs.length - 1].id, + timestamp: + snapshot.docs[snapshot.docs.length - 1].data().timestamp._seconds * 1000 + + snapshot.docs[snapshot.docs.length - 1].data().timestamp._nanoseconds / 1000000, + } + + // Check if there's a next page + let nextPageQuery = collection + if (filters) { + const filterEntries = Object.entries(filters).filter(([, values]) => Array.isArray(values) && values.length > 0) + let combinedFilterEntries: string[] = [] + for (const [filterKey, filterValues] of filterEntries) { + ;(filterValues as string[]).forEach((filterValue) => { + combinedFilterEntries.push(filterKey + '_' + filterValue) + }) + } + nextPageQuery = nextPageQuery.where('combinedFilters', 'array-contains-any', combinedFilterEntries) + } + + nextPageQuery = nextPageQuery + .orderBy('timestamp', 'desc') + .limit(1) + .startAfter( + new Timestamp( + Math.floor(newLastVisibleDocument.timestamp / 1000), + (newLastVisibleDocument.timestamp % 1000) * 1000000 + ) + ) + const nextPageSnapshot = await nextPageQuery.get() + const isMorePageToLoad = !nextPageSnapshot.empty + + return { + thisBatchDocuments: thisBatchDocuments, + lastVisibleDocument: newLastVisibleDocument, + isMorePageToLoad: isMorePageToLoad, + } + } catch (error) { + console.error(error) + return { + error: 'Error while fetching metadata', + } + } +} + +export async function firestoreDeleteBatch( + idsToDelete: string[], + currentMedias: MediaMetadataI[] +): Promise { + // Ensure firestore is initialized and collection name is correct + const collection = firestore.collection('metadata') + const batch = firestore.batch() + + const gcsDeletionPromises: Promise[] = [] + + if (!idsToDelete || idsToDelete.length === 0) { + console.log('No IDs provided for deletion. Exiting.') + return true + } + + for (const id of idsToDelete) { + const mediaItem = currentMedias.find((media) => media.id === id) + + if (mediaItem && mediaItem.gcsURI) + gcsDeletionPromises.push( + deleteMedia(mediaItem.gcsURI) + .then(() => { + console.log(`Successfully deleted GCS file: ${mediaItem.gcsURI} for document ID: ${id}`) + }) + .catch((error: any) => { + console.error(`Failed to delete GCS file ${mediaItem.gcsURI} for document ID: ${id}. Error:`, error) + }) + ) + + // Add the Firestore document deletion to the batch + const docRef = collection.doc(id) + batch.delete(docRef) + } + + // Attempt to delete all GCS files concurrently and wait for all attempts to settle. + if (gcsDeletionPromises.length > 0) await Promise.all(gcsDeletionPromises) + + // Commit the batch of Firestore deletions + try { + await batch.commit() + return true + } catch (error) { + console.error('Firestore batch commit failed:', error) + + return { error: `Firestore batch deletion failed. ` } + } +} diff --git a/src/app/api/gemini/action.tsx b/src/app/api/gemini/action.tsx new file mode 100644 index 00000000..a5bdf875 --- /dev/null +++ b/src/app/api/gemini/action.tsx @@ -0,0 +1,440 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use server' + +const { VertexAI } = require('@google-cloud/vertexai') + +const location = process.env.NEXT_PUBLIC_VERTEX_API_LOCATION +const geminiModel = process.env.NEXT_PUBLIC_GEMINI_MODEL +const projectId = process.env.NEXT_PUBLIC_PROJECT_ID +const vertexAI = new VertexAI({ project: projectId, location: location }) + +// Truncate logs to be readable +export async function truncateLog(obj: any, maxLength = 300) { + const truncatedObj = JSON.parse(JSON.stringify(obj)) + + for (const key in truncatedObj) { + if (typeof truncatedObj[key] === 'string' && truncatedObj[key].length > maxLength) { + truncatedObj[key] = truncatedObj[key].slice(0, maxLength) + '...' + } else if (typeof truncatedObj[key] === 'object') { + truncatedObj[key] = truncateLog(truncatedObj[key], maxLength) + } + } + + return truncatedObj +} + +export async function cleanResult(inputString: string) { + return inputString.toString().replaceAll('\n', '').replaceAll(/\//g, '').replaceAll('*', '') +} + +function getFormatFromBase64(base64String: string) { + if (!base64String.startsWith('data:image/')) return 'image/png' + return base64String.split(';')[0].split(':')[1] +} + +export async function rewriteWithGemini( + userPrompt: string, + generationType: string, + isPersonRefProvided?: boolean, + isAnimalRefProvided?: boolean, + isObjectRefProvided?: boolean, + isStyleRefProvided?: boolean +) { + const generativeModel = vertexAI.getGenerativeModel({ + model: geminiModel, + }) + + // REWRITE IMAGE PROMPT + let rewriteImagePrompt = `You are an AI Image Prompt Synthesizer and Enhancer. + Your mission is to take a user's core prompt and intelligently weave it together with information about any pre-defined visual references (for a specific person, animal, object, or overall style that the user has already visually defined elsewhere). + Your goal is to produce a single, cohesive, detailed, and creative prompt suitable for a text-to-image model like Imagen. + Prioritize enhancing the creativity and overall quality of the potential AI-generated image by adding context, actions, interactions, and richer details where appropriate, while strictly respecting the visual integrity of any provided references. + Follow Google's prompt engineering best practices for clarity and specificity. + + **Core Directives:**` + + // 1 - Handling Subject References + if (isPersonRefProvided) + rewriteImagePrompt += `* **Referenced Person:** + * **DO NOT** re-describe the physical appearance, features, or clothing of the referenced person; these are fixed by their reference. + * **DO** focus on the user's prompt to detail: + * **Actions & Pose:** What is this specific person *doing*? Describe their activity, pose, gestures, and expression in a way that adds context or creativity. If the user only describes location, creatively infer a suitable action or pose for that context. + * **Location & Interaction:** Precisely where is this person within the scene? How are they interacting with the environment or other elements/subjects based on the user prompt or a creative addition?` + + if (isAnimalRefProvided) + rewriteImagePrompt += `* **Referenced Animal:** + * **DO NOT** re-describe the species, breed, colors, or physical features of the referenced animal; these are fixed. + * **DO** focus on the user's prompt to detail: + * **Actions & Behavior:** What is this specific animal *doing*? Describe its activity, pose, or behavior, adding creative context if the user only implies it. + * **Location & Interaction:** Precisely where is this animal within the scene? How is it interacting with its environment or other elements?` + + if (isObjectRefProvided) + rewriteImagePrompt += `* **Referenced Object:** + * **DO NOT** re-describe the intrinsic visual properties (e.g., its fundamental design, color, material as per its reference) of the referenced object. + * **DO** focus on the user's prompt to detail: + * **Placement & Context:** Where is this specific object located within the scene? How is it positioned, oriented, or arranged? What is its immediate context or purpose in that location? + * **Interaction & Scale:** How does it interact with other scene elements or subjects? What is its scale relative to them, or how does its placement contribute to the scene's narrative or composition?` + + // 2 - Handling aspects NOT covered by references + rewriteImagePrompt += `* **Subject Details:** + * **New Persons:** If the userPrompt introduces a person and no specific person reference is active for them, describe this person with vivid detail: apparent age, gender, ethnicity, hair style/color, facial features, body type, specific clothing (style, color, material that fits the scene), accessories, pose, actions, and expression. Be creative in adding plausible details if the user is vague. + * **New Animals:** If the userPrompt introduces an animal and no specific animal reference is active for them, describe it with vivid detail: species, breed, colors, patterns, size, body shape, fur/feathers/scales, pose, actions, and key features. Add creative and fitting details. + * **Other Objects/Elements:** For any other significant objects, items, or elements mentioned in the userPrompt that are not tied to a specific reference, describe them with relevant visual details (type, color, material, texture, state, specific characteristics) that enhance the scene's richness and creativity.` + + rewriteImagePrompt += `* **Scene Composition & Framing:** + * Detail the overall arrangement of all subjects and elements. + * Specify a creative and fitting camera angle, shot type (e.g., "Dynamic low-angle shot capturing the referenced person's determined expression as they [action]," "Intimate close-up of the referenced animal's paws interacting with [object]," "Establishing wide shot showing the referenced object as a small but significant part of a vast, atmospheric landscape"). + * Describe perspective, framing, and use of compositional principles (e.g., rule of thirds, leading lines, depth of field) to create a visually compelling image. + * Ensure the placement and spatial relationships between referenced subjects and other elements are clear and contribute to the overall narrative or visual impact.` + + rewriteImagePrompt += `* **Environment & Atmosphere:** + * Provide rich, imaginative details about the setting. Don't just state location; describe it. + * **Location:** Specifics like "a forgotten, moss-covered library hidden deep in a mystical forest," "a vibrant, bustling alien marketplace on a terraformed moon, under twin suns," "the meticulously organized, minimalist control room of a sleek starship." + * **Time of Day & Weather:** Be evocative (e.g., "the ethereal glow of a pre-dawn aurora borealis," "a torrential downpour illuminated by flickering neon signs," "the hazy, golden light of a perpetual sunset on a fantasy world"). + * **Background & Foreground:** Include significant elements that build the world and atmosphere, ensuring they complement the main subjects and any referenced style.` + + // 3 - Handling stylistic elements + if (isStyleRefProvided) + rewriteImagePrompt += `* **Overall Style:** + * The artistic style, primary lighting characteristics (e.g., type of light sources, overall light quality like 'dramatic' or 'soft'), color palette, and mood/atmosphere are ALL PRE-DEFINED by an external style reference. + * **CRITICAL: Your task is to describe the Subject, Composition, and Setting in a STYLE-NEUTRAL manner.** + * **DO NOT:** + * Use any words that imply a specific artistic style (e.g., "photo of", "painting of", "realistic", "impressionistic", "romantic", "dramatic cityscape", "ethereal glow"). + * Describe specific light qualities or colors that imply an artistic choice (e.g., "sunlight illuminates the scene", "soft golden light," "shimmering light," "cinematic lighting"). Instead, if lighting is part of the setting (e.g., user prompt implies "daytime"), state it factually and neutrally (e.g., "daylight conditions," "ambient light from streetlamps"). + * Describe colors in a way that suggests an artistic palette (e.g., "warm tones," "cool hues"). Describe object colors factually (e.g., "a red car," "green leaves"). + * Imply or state a mood or atmosphere (e.g., "romantic," "peaceful," "cyberpunk vibe" - the vibe comes from the reference, not your words here). + * **DO (for Setting and Composition):** + * Describe the *factual content* of the setting (e.g., "a rooftop," "a city street," "Eiffel Tower in the distance," "a nearby balcony"). + * Describe the *physical arrangement* and composition of elements. + * Describe *natural/environmental* light sources if they are part of the basic scene description (e.g., "sun in the sky," "visible streetlamps," "moonlight") but NOT their artistic qualities or color. + * Your output should be a purely factual, descriptive prompt of the scene's contents and layout, ready to be rendered *by* the external style reference. Think of it as providing the "blueprint" of the scene, and the style reference is the "renderer."` + else + rewriteImagePrompt += `* **Artistic Style (be creative and specific):** + * Propose a distinct and fitting artistic style (e.g., "Hyperrealistic fantasy illustration, incredible detail, style of Brom," "Luminous impressionistic landscape,Alla Prima, style of Erin Hanson," "Gritty neo-noir comic book art, heavy shadows, style of Frank Miller," "Whimsical children's book illustration, soft watercolors, charming characters"). + * **Lighting (be evocative):** + * Describe the lighting with artistic intent (e.g., "Dramatic chiaroscuro with a single, focused beam of light illuminating the subject's face," "Ethereal, volumetric rays of godlight breaking through ancient forest canopy," "Dynamic, colorful reflections from multiple neon sources on wet pavement"). + * **Color Palette (be descriptive and harmonious):** + * Suggest a specific and evocative color palette (e.g., "A rich analogous palette of deep crimsons, burnt oranges, and ochre yellows, creating a fiery ambiance," "A cool, desaturated palette of slate blues, misty greys, and stark whites, with a single, startling accent of blood red"). + * **Mood/Atmosphere (be specific):** + * Define the precise mood or atmosphere (e.g., "A sense of profound ancient mystery and quiet solitude," "An atmosphere of high-octane, futuristic urgency," "A feeling of tender, nostalgic warmth and comfort").` + + // 4 - Handling final touch ups & formatting + rewriteImagePrompt += `**Your Task:** + Based on the user's base prompt below, and strictly following all applicable instructions above regarding provided references (or their absence), synthesize and enhance it into a single, highly descriptive, creative, and coherent image generation prompt. Fill in unspecified details (like actions if only location is given) to elevate the creative potential. + User's base prompt: "${userPrompt}" + + **Output Requirements:** + * The final output MUST be ONLY the enhanced prompt itself. + * It should be a single, well-structured sentence or a concise short paragraph. + * Do NOT include any introductory phrases, explanations, headings, field labels (like "Subject:", "Style:"), or your own reasoning. + * Aim for the enhanced prompt to be under 75 tokens if possible, but prioritize richness, creativity, and adherence to all reference-handling rules over strict token count if necessary to achieve a high-quality result.` + + // REWRITE VIDEO PROMPT + const rewriteVideoPrompt = `You are an AI video prompt enhancer. + Your task is to take a user-provided prompt designed for a text-to-video model (like Veo) and enhance it according to Google's recommended prompt engineering guidelines for video generation. + These guidelines emphasize clarity, specificity, detail, action, and camera work to achieve high-quality, dynamic, and predictable video results. + + Focus on improving the prompt in the following areas: + * **Subject:** Be specific about the main subject(s). Include age, appearance, clothing, species, breed, color, size, and any distinctive features. (e.g., "a joyful woman in her early 30s with curly brown hair, wearing a yellow raincoat") + * **Scene:** Describe the environment/location clearly. Include place, time of day, weather, and key background details. (e.g., "on a bustling Parisian street corner during a light spring shower") + * **Action:** Detail what the subject is actively doing during the shot. Be specific about the movement or activity. (e.g., "laughing as she opens a bright red umbrella") + * **Camera Motion:** Specify how the camera moves or its perspective. Use terms like tracking shot, panning, tilting, dolly zoom, handheld, static shot, drone view, aerial shot, POV shot, orbit shot, etc. (e.g., "Slow-motion orbiting shot") + * **Style:** Define the overall aesthetic and visual treatment. Mention cinematic, animation style (e.g., 3D cartoon, anime), film look (e.g., vintage film, film noir), documentary, etc. (e.g., "cinematic, high definition") + * **Composition:** Describe the framing and angle of the shot. Use terms like wide shot, medium shot, close-up, extreme close-up, low angle, high angle, eye-level, over-the-shoulder, etc. (e.g., "medium shot framing her from the waist up") + * **Ambiance:** Convey the mood using descriptions of lighting and color. Mention time of day lighting (e.g., golden hour, night scene, overcast daylight), color temperature (e.g., warm tones, cool blue tones), and overall mood (e.g., dramatic, cheerful, mysterious). (e.g., "overcast daylight with bright, cheerful colors contrasted against grey skies") + + **Input:** The user-provided Veo prompt: "${userPrompt}" + **Output:** The enhanced prompt, ready for a text-to-video model. The output should be a single, well-structured sentence or a short paragraph. Do not include any introductory or explanatory text. The enhanced prompt should be concise yet detailed, prioritizing the most visually important elements and describing the key action and camera work. Aim for a prompt that is under 75 tokens if possible. + + **Example:** + **Input:** "A dog running in a park." + **Output:** "Cinematic tracking shot following a Golden Retriever puppy running excitedly through a green park field, chasing a red ball, bright sunny afternoon light, wide shot capturing the dog's movement and the expanse of the park, joyful and energetic mood."` + + try { + const resp = await generativeModel.generateContent( + generationType === 'Image' ? rewriteImagePrompt : rewriteVideoPrompt + ) + const contentResponse = await resp.response + + if ('error' in contentResponse) throw Error(await cleanResult(contentResponse.error)) + + if (contentResponse.instances !== undefined && 'error' in contentResponse.instances[0].prompt) + throw Error(await cleanResult(contentResponse.instances[0].prompt)) + + const newPrompt = await cleanResult(contentResponse.candidates[0].content.parts[0].text) + + return newPrompt + } catch (error) { + console.error(error) + return { + error: 'Error while rewriting prompt with Gemini.', + } + } +} + +export async function getDescriptionFromGemini(base64Image: string, type: string) { + const generativeModel = vertexAI.getGenerativeModel({ + model: geminiModel, + }) + + let descriptionPrompt = '' + if (type === 'Person') + descriptionPrompt = + "State the primary subject in this image. Only use terms that describe a person's age and gender (e.g., boy, girl, man, woman). " + + 'Do not state what the person is doing, or other object present in the image. ' + if (type === 'Animal') descriptionPrompt = 'State the primary animal in this image. Only use its race. ' + if (type === 'Product') + descriptionPrompt = + 'State the primary product in this image using the most common and simple term (e.g., chair, table, phone). ' + + 'If you recognize the brand or the model, use them. ' + if (type === 'Style') + descriptionPrompt = + "Describe the overall style of this image, not what is happening in it. Use terms like 'minimalist', 'vintage', 'surreal', 'abstract', 'modern', etc. " + if (type === 'Default') + descriptionPrompt = + "State the primary subject in this image using the most common and simple term. Don't state what it is doing or where it is. " + + descriptionPrompt = + descriptionPrompt + + 'Use a subject format of 40 characters or less, with no period at the end. ' + + "If you can't generate the output, for instance because the image content is not matching the type, just send back 'Error'" + + const imagePart = { + inline_data: { + data: base64Image.startsWith('data:') ? base64Image.split(',')[1] : base64Image, + mimeType: getFormatFromBase64(base64Image), + }, + } + const textPart = { + text: descriptionPrompt, + } + + const reqData = { + contents: [{ role: 'user', parts: [imagePart, textPart] }], + } + + try { + const resp = await generativeModel.generateContent(reqData) + const contentResponse = await resp.response + + if ('error' in contentResponse) throw Error(await cleanResult(contentResponse.error)) + + if (contentResponse.instances !== undefined && 'error' in contentResponse.instances[0].prompt) + throw Error(await cleanResult(contentResponse.instances[0].prompt)) + + const newDescription = await cleanResult(contentResponse.candidates[0].content.parts[0].text) + + if (newDescription.includes('Error')) return '(provided type is not matching image)' + else return newDescription + } catch (error) { + console.error(JSON.stringify(truncateLog(error), undefined, 4)) + return { + error: 'Error while getting description from Gemini.', + } + } +} + +export async function getFullReferenceDescription(base64Image: string, type: string) { + const generativeModel = vertexAI.getGenerativeModel({ + model: geminiModel, + }) + + let specificPromptInstructions = '' + let activeCommonDetailedInstructions = '' + + // This is the set of common instructions for most types (Person, Animal, Product, Default) + const generalCommonDetailedInstructions = + " Your primary goal is to generate an exceptionally detailed, meticulous, and comprehensive description of the primary subject's visual attributes. " + + '**The entire description should be concise, ideally around 100-120 words, and must not exceed 130 words.** ' + // New length constraint + 'While achieving this, strictly adhere to the following rules: ' + + "1. Begin the description directly with the subject's characteristics, without any introductory phrases like 'This image shows...' or 'The subject is...'. " + + '2. The description must focus exclusively on the visual attributes of the primary subject itself. ' + + "3. Do NOT describe the subject's actions, what the subject is doing, its location, the surrounding environment, or the background. Confine the description strictly to the physical appearance of the subject. " + + '4. Ensure the description paints a clear and vivid visual picture as if under close inspection, focusing on objective visual facts. ' + + "If you cannot satisfy the primary goal (an exceptionally detailed, subject-focused visual description) while strictly adhering to all the numbered rules, or if the image content does not clearly match the requested type, is ambiguous, or if a meaningful description of a singular primary subject cannot be generated, then respond with the single word 'error'." // Changed 'Error' to 'error' + + // These are the tailored common instructions specifically for the 'Style' type + const styleCommonDetailedInstructions = + " Your primary goal is to generate an exceptionally detailed, meticulous, and comprehensive analysis of the image's overall artistic and visual style. " + + '**The entire description should be concise, ideally around 100-120 words, and must not exceed 130 words.** ' + // New length constraint + 'While achieving this, strictly adhere to the following rules: ' + + "1. Begin the description directly with the style's characteristics, without any introductory phrases like 'This image shows...'. " + + "2. Focus on how visual elements collectively create the style. When discussing composition, color, lighting, and texture as they contribute to the style, you may refer to how these apply to the general forms, shapes, and atmosphere of the depicted scene. However, do NOT provide an inventory of discrete objects as if describing a scene's content, nor describe any narrative actions or specific, identifiable real-world locations. The emphasis is on the *how* of the style, not the *what* of the scene's literal content. " + + '3. Ensure the description paints a clear and vivid visual picture of the style itself, focusing on objective visual analysis of its components. ' + + "If a meaningful and detailed analysis of the image's style cannot be generated according to these exacting rules, or if the image is too ambiguous, respond with the single word 'error'." // Changed 'Error' to 'error' + + if (type === 'Person') { + activeCommonDetailedInstructions = generalCommonDetailedInstructions + specificPromptInstructions = + 'Provide an exceptionally detailed and meticulous description of the primary person in this image, focusing strictly on their physical appearance and attire. Break down their appearance into specific regions and features, describing each with precision. ' + + 'Detail their apparent age range and gender. For their hair, describe its color nuances, style from roots to ends, length, texture (e.g., fine, coarse, wavy, straight, coily), and any specific characteristics like parting, layers, or highlights. ' + + 'For their face, provide granular details about eye color, iris patterns if visible, eye shape, eyelashes, eyebrows (shape, thickness, color), nose (shape of bridge, nostrils, tip), mouth and lip characteristics (shape, fullness, color, texture), chin, jawline, and skin (tone, texture, any visible pores or fine lines if clear). Describe any static facial expression (e.g., a gentle smile, a neutral look) by detailing the muscle positioning. ' + + 'Describe their build or physique (e.g., slender, muscular, average) if discernible. Enumerate and describe any unique identifying features like glasses (detailing frame style, material, color, lens appearance), tattoos (location, colors, subject matter if clear), scars, or birthmarks with precision. ' + + 'For their attire, describe each visible garment (e.g., shirt, pants, dress, jacket) in exhaustive detail: its type, specific color(s) and shades, fabric type (e.g., cotton, silk, denim, knit) and weave if apparent, pattern (name it if possible, e.g., plaid, floral, pinstripe, and describe its scale and colors), fit (e.g., loose, fitted, oversized), and all specific features like collar type, neckline, sleeve style and length, cuffs, buttons (type, material, number), zippers (type, puller details), seams, hems, and any embellishments or logos. ' + + 'Also, describe any visible accessories like jewelry (earrings, necklaces, rings – specifying type, material, gemstones, clasp, and intricate design details), hats (style, material, brim, crown), belts (buckle, material, width), or bags with similar exhaustive detail. ' + } else if (type === 'Animal') { + activeCommonDetailedInstructions = generalCommonDetailedInstructions + specificPromptInstructions = + 'Provide an exceptionally detailed and meticulous description of the primary animal in this image, focusing strictly on its physical characteristics. Break down its appearance into specific features and describe each with precision. ' + + 'Detail its species and breed (if identifiable). For its coat or covering, describe the primary and secondary color(s) and shades, intricate patterns (e.g., spots, stripes, patches – noting their shape, size, color, and exact distribution on the body), and texture (e.g., smooth, shaggy, sleek, dense, sparse, glossy, matte) of its fur, feathers, scales, or skin. ' + + 'Describe its approximate size, overall build (e.g., slender, robust, delicate, muscular), and specific body shape and proportions. ' + + "Enumerate and describe any distinctive physical features with specificity: the shape and size of its head, ear shape and position (e.g., pricked, floppy, tufted), eye color and pupil shape, muzzle or beak (length, width, shape, color, nostril details), presence and nature of teeth or fangs if visible, tongue if visible, horns or antlers (size, shape, texture, color, number of points if applicable), neck (length, thickness), legs (number, length, thickness, joint appearance), paws or hooves or claws (shape, color, number of digits, claw details), tail (length, shape, covering, how it's held if static and characteristic), and any unique markings or physical traits not covered by general patterning. " + + 'If discernible, mention its apparent age (e.g., juvenile, adult, very old based on physical indicators). ' + } else if (type === 'Product') { + activeCommonDetailedInstructions = generalCommonDetailedInstructions + specificPromptInstructions = + 'Provide an exceptionally detailed and meticulous description of the primary product in this image, focusing strictly on its physical attributes. Break down the product into its constituent parts, components, and surfaces, describing each with precision, as if conducting a thorough visual inspection for a catalog or engineering specification. ' + + 'Detail its exact type (e.g., specific type of chair, smartphone model, winter jacket). Identify brand and model if any markings or distinct design cues are visible. ' + + 'For its materials, specify all visible types (e.g., polished chrome, brushed aluminum, matte plastic, specific wood like oak or walnut, type of fabric like corduroy or canvas, glass, ceramic) and describe their textures (e.g., smooth, grained, ribbed, dimpled, woven) and finishes (e.g., glossy, matte, satin, metallic). ' + + 'Describe all colors and shades present, and any patterns or graphical elements. Detail its overall shape and geometry, approximate dimensions or proportions if inferable. ' + + 'Describe each specific design element meticulously: for a jacket, this would include the collar type (e.g., stand-up, notch lapel), fastening mechanisms (e.g., specific type of zipper, buttons - their material, shape, and how they attach, snaps, Velcro), pocket design (e.g., welt, patch, zippered - their placement, size, flap details), cuff and hem finishing, stitching type and visibility, lining if visible, and any logos or tags. For a phone, describe screen borders, button placement and shape, port types and locations, camera lens arrangement, and casing details. For furniture, describe legs, supports, surfaces, joinery if visible, and hardware. ' + + 'Note any visible aspects of its construction, assembly, seams, or edges. The goal is a comprehensive inventory of all its visual characteristics. ' + } else if (type === 'Style') { + activeCommonDetailedInstructions = styleCommonDetailedInstructions + specificPromptInstructions = + 'Analyze and describe the overall artistic and visual style of this image with meticulous and analytical detail. ' + + 'Elaborate on stylistic elements such as: the dominant aesthetic (e.g., minimalist, vintage, surreal, abstract, modern, photorealistic, painterly, graphic novel art, cyberpunk, solarpunk), elaborating on how specific visual choices achieve this effect; ' + + 'the color palette – its range (e.g., monochromatic, analogous, complementary), specific hues, saturation, value, temperature, and how colors interact or are used to create harmony or contrast, noting dominant and accent colors; ' + + 'lighting techniques – the quality (hard, soft), direction, intensity, color of light, and its precise impact on mood, form, texture, and creation of highlights and shadows (e.g., volumetric lighting, neon glow, diffuse, chiaroscuro); ' + + "compositional choices – adherence to or deviation from principles like the rule of thirds, leading lines, symmetry/asymmetry, balance, framing, viewpoint (e.g., low-angle, high-angle, eye-level), perspective (e.g., linear, atmospheric), and depth of field, and their effect on the viewer's focus and interpretation of the style; " + + 'prevalent textures (e.g., weathered stone, metallic sheen, organic overgrowth, digital noise) and patterns, noting their characteristics, repetition, and contribution to the style; ' + + 'and the overall mood or atmosphere the style distinctively creates (e.g., dystopian, ethereal, gritty, vibrant, mysterious, tranquil). Analyze how these visual and artistic elements interrelate to define the overall style comprehensively. ' + } else { + activeCommonDetailedInstructions = generalCommonDetailedInstructions + specificPromptInstructions = + 'Identify the single most prominent primary subject in this image. If a singular primary subject is clearly identifiable, ' + + 'provide an exceptionally detailed and meticulous description of its visual characteristics. This includes its specific category (e.g., a particular species of flower, a type of antique clock, a specific pastry, an abstract sculptural form). ' + + 'Then, provide a granular breakdown of its physical appearance: all visible colors and their shades, precise shapes and geometric forms, an estimation of its real-world size if inferable, all discernible textures (e.g., smooth, rough, porous, reflective, matte), types of materials it appears to be made of, and a detailed account of any specific parts, components, segments, layers, or markings. Describe each aspect with precision. ' + + "If the image does not contain a singular, clearly identifiable primary subject that can be described in such exhaustive detail according to these rules (e.g., it is primarily a complex landscape or cityscape without a single dominant subject easily isolated from its context, or a very abstract pattern where 'subject' is ill-defined for this purpose), " + + "or if describing it adequately requires detailing background, location, or actions, then respond with the single word 'Error'. " + } + + const fullPrompt = specificPromptInstructions + activeCommonDetailedInstructions + + const imagePart = { + inline_data: { + data: base64Image.startsWith('data:') ? base64Image.split(',')[1] : base64Image, + mimeType: getFormatFromBase64(base64Image), + }, + } + const textPart = { + text: fullPrompt, + } + + const reqData = { + contents: [{ role: 'user', parts: [imagePart, textPart] }], + } + + try { + const resp = await generativeModel.generateContent(reqData) + + if (!resp.response) { + console.error('No response object found from generateContent call.') + return 'error' + } + + const contentResponse = await resp.response + + // Assuming cleanResult and truncateLog are defined elsewhere + if ('error' in contentResponse) throw Error(await cleanResult(contentResponse.error)) + + if (contentResponse.instances !== undefined && 'error' in contentResponse.instances[0].prompt) { + throw Error(await cleanResult(contentResponse.instances[0].prompt)) + } + + const newDescription = await cleanResult(contentResponse.candidates[0].content.parts[0].text) + + if (newDescription.includes('Error')) { + // Checks if Gemini explicitly returned "Error" as instructed + return '(provided type is not matching image or description could not be generated)' + } else { + return newDescription + } + } catch (error) { + console.error(JSON.stringify(truncateLog(error), undefined, 4)) + return { + error: 'Error while getting description from Gemini.', + } + } +} + +export async function getPromptFromImageFromGemini(base64Image: string, target: 'Image' | 'Video') { + const generativeModel = vertexAI.getGenerativeModel({ + model: geminiModel, + }) + + const imagenPrompt = `Generate a highly detailed text prompt, suitable for a text-to-image model such as Imagen 3, to recreate the uploaded image with maximum accuracy. The prompt should describe these aspects of the image: + 1. **Subject:** Main objects/figures, appearance, features, species (if applicable), clothing, pose, actions. Be extremely specific (e.g., "a fluffy ginger cat with emerald green eyes sitting on a windowsill" instead of "a cat"). + 2. **Composition:** Arrangement of subjects (centered, off-center, foreground, background), perspective/camera angle (close-up, wide shot, bird's-eye view). + 3. **Setting:** Environment, location, time of day, weather. Be specific (e.g., "a dimly lit, ornate library with towering bookshelves" instead of "a library"). + 4. **Style:** Artistic style (photorealistic, oil painting, watercolor, cartoon, pixel art, abstract). Mention specific artists if relevant. + 5. **Lighting:** Lighting conditions (bright sunlight, soft indoor lighting, dramatic shadows, backlighting), direction and intensity of light. + 6. **Color Palette:** Dominant colors, overall color scheme (vibrant, muted, monochromatic, warm, cool). + 7. **Texture:** Textures of objects and surfaces (smooth, rough, furry, metallic, glossy). + 8. **Mood/Atmosphere:** Overall feeling or emotion (serene, joyful, mysterious, ominous). + + **Output Format:** I want the prompt to be ONLY a single paragraph of text, directly usable by the text-to-image model. **Do not add any conversational filler, preambles, or extra sentences like "Text-to-Image Prompt:". Do not format the output as a list or use any special characters like <0xC2><0xA0>.** + **Example Output (Correct Format): "A photorealistic image of a Ragdoll or Birman cat with light cream and beige long fur, sitting upright on a kitchen counter or appliance with its paws tucked beneath it. The cat has bright blue eyes, a small pink nose, and pointed, tufted ears. Its tail is long and fluffy, draping down behind it. The background is slightly blurred and features a dark horizontal band suggesting an appliance, and a glass partition with black metal frames. The lighting is soft and diffused, illuminating the cat evenly. The dominant colors are light cream, beige, white, blue, and black. The overall style is realistic photography with a focus on detail and natural lighting. The image conveys a sense of calmness and gentle curiosity." + **Important:** The prompt must be highly descriptive, prioritizing the most visually important elements for accurate recreation. The prompt can be up to 75 tokens.` + + const veoPrompt = `Generate a highly detailed text prompt, suitable for a text-to-video model such as **Veo**, to create a short video clip inspired by the uploaded image, focusing on dynamic action and visual storytelling. The prompt should describe these aspects for the video: + 1. **Subject & Action:** Main objects/figures, their appearance, features, species (if applicable), clothing. Crucially, describe their **movements, actions, interactions, and any changes in expression or pose over the duration of the clip**. Be extremely specific (e.g., "a fluffy ginger cat with emerald green eyes slowly blinking, then stretching its front paws forward on a windowsill" instead of "a cat"). + 2. **Scene Composition & Camera Work:** Initial arrangement of subjects (centered, off-center, foreground, background). Specify the **camera angle and shot type (e.g., close-up, wide shot, POV) and describe any camera movement** (e.g., slow pan right, zoom in, tracking shot following the subject, static shot). + 3. **Setting & Environmental Dynamics:** Environment, location, time of day, weather, including any **dynamic environmental elements** (e.g., "leaves blowing in the wind in a sun-dappled forest at golden hour," "rain streaking down a window in a cozy, dimly lit room at night"). + 4. **Visual Style:** Artistic style of the video (e.g., photorealistic, cinematic, anime, watercolor animation, gritty found footage, pixel art). Mention specific directors or cinematic styles if relevant. + 5. **Lighting & Atmosphere:** Lighting conditions (bright sunlight, moody twilight, soft indoor lighting, dramatic shadows), its direction, intensity, and **how it might change or interact with the scene's motion**. This contributes to the overall mood (serene, joyful, mysterious, ominous). + 6. **Color Palette:** Dominant colors and overall color scheme of the video (vibrant, desaturated, monochromatic, warm, cool tones), and if they shift. + 7. **Key Textures:** Prominent textures of subjects and the environment relevant to the video's look and feel (smooth, rough, furry, metallic, wet, windswept). + 8. **Video Clip Focus & Pacing:** Briefly suggest the overall pacing (e.g., slow and graceful, fast-paced action, serene and calm) and what key moment, action, or transformation the short video clip should focus on. + + **Output Format:** I want the prompt to be ONLY a single paragraph of text, directly usable by the text-to-video model **Veo**. **Do not add any conversational filler, preambles, or extra sentences like "Text-to-Video Prompt:". Do not format the output as a list or use any special characters like .** + **Example Output (Correct Format): "A cinematic, photorealistic short video clip of a Ragdoll cat with light cream and beige long fur, initially curled up sleeping on a sunlit wooden floor. The cat slowly awakens, stretches its paws out, and yawns widely, its bright blue eyes blinking open. The camera is at a low angle, close-up on the cat, with a very gentle zoom-out as it stretches. Dust motes drift in the warm sunlight streaming from a nearby window, creating a soft, hazy atmosphere. The background shows a slightly out-of-focus cozy living room. The dominant colors are warm wood tones, light cream, and soft blues. The clip should convey a serene and peaceful morning moment."** + **Important:** The prompt must be highly descriptive, prioritizing key visual elements and **essential motion cues** for generating an engaging video clip. The prompt can be up to 75 tokens.` + + const imagePart = { + inline_data: { + data: base64Image.startsWith('data:') ? base64Image.split(',')[1] : base64Image, + mimeType: getFormatFromBase64(base64Image), + }, + } + const textPart = { + text: target === 'Image' ? imagenPrompt : veoPrompt, + } + + const reqData = { + contents: [{ role: 'user', parts: [imagePart, textPart] }], + } + + try { + const resp = await generativeModel.generateContent(reqData) + const contentResponse = await resp.response + + if ('error' in contentResponse) throw Error(await cleanResult(contentResponse.error)) + + if (contentResponse.instances !== undefined && 'error' in contentResponse.instances[0].prompt) + throw Error(await cleanResult(contentResponse.instances[0].prompt)) + + const newDescription = contentResponse.candidates[0].content.parts[0].text.replace(/ +/g, ' ').trimEnd() + + if (newDescription.includes('Error')) return '(provided type is not matching image)' + else return newDescription + } catch (error) { + console.error(JSON.stringify(truncateLog(error), undefined, 4)) + return { + error: 'Error while getting prompt from Image with Gemini.', + } + } +} diff --git a/src/app/api/generate-image-utils.tsx b/src/app/api/generate-image-utils.tsx new file mode 100644 index 00000000..6dd03747 --- /dev/null +++ b/src/app/api/generate-image-utils.tsx @@ -0,0 +1,564 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export interface GenerateFieldI1 { + label?: string + type?: string + default?: string + options?: + | string[] + | { + value: string + label: string + indication?: string + type?: string + }[] + isDataResetable: boolean + isFullPromptAdditionalField: boolean +} +export interface GenerateFieldStyleI { + type: string + default: string + defaultSub: string + options: { + value: string + label: string + subID: string + }[] + isDataResetable: boolean + isFullPromptAdditionalField: boolean +} + +export interface GenerateFieldSecondartStyleI { + type: string + options: { + label: string + subID: string + type: string + options: string[] + default: string + }[] + isDataResetable: boolean + isFullPromptAdditionalField: boolean +} + +export interface GenerateImageFormFieldsI { + prompt: GenerateFieldI1 + modelVersion: GenerateFieldI1 + sampleCount: GenerateFieldI1 + negativePrompt: GenerateFieldI1 + aspectRatio: GenerateFieldI1 + personGeneration: GenerateFieldI1 + outputOptions: GenerateFieldI1 + style: GenerateFieldStyleI + secondary_style: GenerateFieldSecondartStyleI + light: GenerateFieldI1 + light_coming_from: GenerateFieldI1 + shot_from: GenerateFieldI1 + perspective: GenerateFieldI1 + image_colors: GenerateFieldI1 + use_case: GenerateFieldI1 +} + +export const GenerateImageFormFields = { + prompt: { + type: 'textInput', + isDataResetable: true, + isFullPromptAdditionalField: false, + }, + modelVersion: { + type: 'select', + default: 'imagen-4.0-generate-preview-05-20', + options: [ + { + value: 'imagen-4.0-generate-preview-05-20', + label: 'Imagen 4', + indication: 'High performance latest model version', + }, + { + value: 'imagen-3.0-generate-002', + label: 'Imagen 3', + indication: 'Standard performance model version', + }, + { + value: 'imagen-3.0-fast-generate-001', + label: 'Imagen 3 - Fast', + indication: 'Low latency model version', + }, + ], + isDataResetable: false, + isFullPromptAdditionalField: false, + }, + sampleCount: { + label: 'Quantity of outputs', + type: 'chip-group', + default: '4', + options: ['1', '2', '3', '4'], + isDataResetable: false, + isFullPromptAdditionalField: false, + }, + negativePrompt: { + type: 'textInput', + isDataResetable: true, + isFullPromptAdditionalField: false, + }, + aspectRatio: { + label: 'Aspect ratio', + type: 'chip-group', + default: '1:1', + options: ['1:1', '9:16', '16:9', '3:4', '4:3'], + isDataResetable: false, + isFullPromptAdditionalField: false, + }, + personGeneration: { + label: 'People generation', + type: 'select', + default: 'allow_adult', + options: [ + { + value: 'dont_allow', + label: 'No people', + }, + { + value: 'allow_adult', + label: 'Adults only', + }, + { + value: 'allow_all', + label: 'Adults & Children', + }, + ], + isDataResetable: false, + isFullPromptAdditionalField: false, + }, + outputOptions: { + label: 'Ouput format', + type: 'select', + default: 'image/png', + options: [ + { + value: 'image/png', + label: 'PNG', + }, + { + value: 'image/jpeg', + label: 'JPEG', + }, + ], + isDataResetable: false, + isFullPromptAdditionalField: false, + }, + style: { + type: 'select', + default: 'photo', + defaultSub: 'photographySub', + options: [ + { + value: 'photo', + label: 'Photography', + subID: 'photographySub', + }, + { + value: 'drawing', + label: 'Drawing', + subID: 'drawingSub', + }, + { + value: 'painting', + label: 'Painting', + subID: 'paintingSub', + }, + { + value: 'computer digital creation', + label: 'Digital art', + subID: 'digitalSub', + }, + ], + isDataResetable: false, + isFullPromptAdditionalField: false, + }, + secondary_style: { + type: 'controled-chip-group', + options: [ + { + label: 'Photography style', + subID: 'photographySub', + type: 'select', + options: [ + 'Landscape', + 'Studio', + 'Portrait', + 'Candid', + 'Street', + 'Architectural', + 'Wildlife', + 'Photojournalism', + 'Fashion', + 'Food', + 'Travel', + 'Fine Art', + 'Polaroid', + 'Astronomy', + ], + default: '', + }, + { + label: 'Drawing style', + subID: 'drawingSub', + type: 'select', + options: [ + 'Technical pencil', + 'Color pencil', + 'Cartoon', + 'Graphic Novel', + 'Charcoal', + 'Pastel', + 'Ink', + 'Sketch', + 'Doodle', + ], + default: '', + }, + { + label: 'Painting style', + subID: 'paintingSub', + type: 'select', + options: [ + 'Gouache', + 'Oil', + 'Watercolor', + 'Pastel', + 'Street Art', + 'Impressionism', + 'Expressionism', + 'Surrealism', + 'Abstract', + 'Minimalism', + ], + default: '', + }, + { + label: 'Digital creation style', + subID: 'digitalSub', + type: 'select', + options: [ + 'Typography', + 'Digital illustration', + 'Pop Art', + 'Cyberpunk poster', + 'Pixel Art', + 'Vector Art', + '3D Rendering', + 'Video game', + 'Animation', + 'Visual Effects', + 'Motion Graphics', + ], + default: '', + }, + ], + isDataResetable: true, + isFullPromptAdditionalField: false, + }, + light: { + label: 'Lightning', + type: 'chip-group', + options: ['Natural', 'Bright Sun', 'Golden Hour', 'Night time', 'Dramatic', 'Warm', 'Cold'], + isDataResetable: true, + isFullPromptAdditionalField: true, + }, + light_coming_from: { + label: 'Light origin', + type: 'chip-group', + options: ['Front', 'Back', 'Above', 'Below', 'Side'], + isDataResetable: true, + isFullPromptAdditionalField: true, + }, + shot_from: { + label: 'View angle', + type: 'chip-group', + options: ['Front', 'Back', 'Above', 'Below', 'Side'], + isDataResetable: true, + isFullPromptAdditionalField: true, + }, + perspective: { + label: 'Perspective', + type: 'chip-group', + options: ['Macro', 'Close-up', 'Standard', 'Wide angle', 'Extra wide', 'Aerial'], + isDataResetable: true, + isFullPromptAdditionalField: true, + }, + image_colors: { + label: 'Colors', + type: 'chip-group', + options: ['Colorful', 'Light', 'Dark', 'Black & White', 'Vintage', 'Cinematic grain'], + isDataResetable: true, + isFullPromptAdditionalField: true, + }, + use_case: { + label: 'Specific use case', + type: 'chip-group', + options: [ + 'Food, insects, plants (still life)', + 'Sports, wildlife (motion)', + 'Astronomical, landscape (wide-angle)', + ], + isDataResetable: true, + isFullPromptAdditionalField: false, + }, + referenceObjects: { + type: 'array', + isDataResetable: true, + }, +} + +// Reference utils for Few Shots Customization in Image Generation + +export const referenceTypeField = { + label: 'Reference type', + options: ['Person', 'Animal', 'Product', 'Style', 'Default'], +} +export const referenceTypeMatching = { + Person: { + referenceType: 'REFERENCE_TYPE_SUBJECT', + subjectType: 'SUBJECT_TYPE_PERSON', + }, + Animal: { + referenceType: 'REFERENCE_TYPE_SUBJECT', + subjectType: 'SUBJECT_TYPE_ANIMAL', + }, + Product: { + referenceType: 'REFERENCE_TYPE_SUBJECT', + subjectType: 'SUBJECT_TYPE_PRODUCT', + }, + Style: { + referenceType: 'REFERENCE_TYPE_STYLE', + subjectType: '', + }, + Default: { + referenceType: 'REFERENCE_TYPE_SUBJECT', + subjectType: 'SUBJECT_TYPE_DEFAULT', + }, +} + +export interface ReferenceObjectI { + referenceType: string + base64Image: string + description: string + ratio: string + width: number + height: number + refId: number + objectKey: string + isAdditionalImage: boolean +} + +export const ReferenceObjectDefaults = { + referenceType: '', + base64Image: '', + description: '', + ratio: '', + width: 0, + height: 0, + isAdditionalImage: false, + refId: 0, + objectKey: '', +} + +export const ReferenceObjectInit: ReferenceObjectI[] = [ + { ...ReferenceObjectDefaults, objectKey: Math.random().toString(36).substring(2, 15), refId: 1 }, +] + +export const maxReferences = 4 + +// Interface of Generate form fields +export interface GenerateImageFormI { + prompt: string + modelVersion: string + sampleCount: string + negativePrompt: string + aspectRatio: string + personGeneration: string + outputOptions: string + style: string + secondary_style: string + light: string + light_coming_from: string + shot_from: string + perspective: string + image_colors: string + use_case: string + referenceObjects: ReferenceObjectI[] +} + +// Set default values for Generate Form +const generateFieldList: [keyof GenerateImageFormFieldsI] = Object.keys(GenerateImageFormFields) as [ + keyof GenerateImageFormFieldsI +] +var formDataDefaults: any +generateFieldList.forEach((field) => { + const fieldParams: GenerateFieldI1 | GenerateFieldStyleI | GenerateFieldSecondartStyleI = + GenerateImageFormFields[field] + const defaultValue = 'default' in fieldParams ? fieldParams.default : '' + formDataDefaults = { ...formDataDefaults, [field]: defaultValue } +}) +formDataDefaults.referenceObjects = ReferenceObjectInit + +export interface chipGroupFieldsI { + label: string + subID?: string + default?: string | number + options: string[] +} + +export interface selectFieldsI { + label?: string + default: string + options: { + value: string + label: string + indication?: string + }[] +} + +export interface generalSettingsI { + aspectRatio: chipGroupFieldsI + durationSeconds?: chipGroupFieldsI + sampleCount: chipGroupFieldsI +} +export interface advancedSettingsI { + personGeneration: selectFieldsI + outputOptions?: selectFieldsI +} + +interface CompositionFieldsI { + light: GenerateFieldI1 + perspective: GenerateFieldI1 + image_colors: GenerateFieldI1 + use_case: GenerateFieldI1 + light_coming_from: GenerateFieldI1 + shot_from: GenerateFieldI1 +} + +export interface ImageGenerationFieldsI { + model: GenerateFieldI1 + settings: generalSettingsI + advancedSettings: advancedSettingsI + styleOptions: GenerateFieldStyleI + subStyleOptions: GenerateFieldSecondartStyleI + compositionOptions: CompositionFieldsI + resetableFields: (keyof GenerateImageFormFieldsI)[] + fullPromptFields: (keyof GenerateImageFormFieldsI)[] + defaultValues: any +} + +// Sort out Generate fields depending on purpose +export const imageGenerationUtils: ImageGenerationFieldsI = { + model: GenerateImageFormFields.modelVersion, + settings: { + aspectRatio: GenerateImageFormFields.aspectRatio, + sampleCount: GenerateImageFormFields.sampleCount, + }, + advancedSettings: { + personGeneration: GenerateImageFormFields.personGeneration, + outputOptions: GenerateImageFormFields.outputOptions, + }, + styleOptions: GenerateImageFormFields.style, + subStyleOptions: GenerateImageFormFields.secondary_style, + compositionOptions: { + light: GenerateImageFormFields.light, + perspective: GenerateImageFormFields.perspective, + image_colors: GenerateImageFormFields.image_colors, + use_case: GenerateImageFormFields.use_case, + light_coming_from: GenerateImageFormFields.light_coming_from, + shot_from: GenerateImageFormFields.shot_from, + }, + resetableFields: generateFieldList.filter((field) => GenerateImageFormFields[field].isDataResetable == true), + fullPromptFields: generateFieldList.filter( + (field) => GenerateImageFormFields[field].isFullPromptAdditionalField == true + ), + defaultValues: formDataDefaults, +} + +// Interface of result sent back by Imagen within GCS or as base64 +export interface ImagenModelResultI { + gcsUri?: string + bytesBase64Encoded?: string + mimeType: string + prompt?: string +} + +// Interface of Image object created after image generation +export interface ImageI { + src: string + gcsUri: string + ratio: string + width: number + height: number + altText: string + key: string + format: string + prompt: string + date: string + author: string + modelVersion: string + mode: string +} + +// List of Imagen available ratio and their corresponding generation dimentions +export const RatioToPixel = [ + { ratio: '1:1', width: 1024, height: 1024 }, + { ratio: '9:16', width: 768, height: 1408 }, + { ratio: '16:9', width: 1408, height: 768 }, + { ratio: '3:4', width: 896, height: 1280 }, + { ratio: '4:3', width: 1280, height: 896 }, +] + +// Random prompt list the user can use if they lack prompt ideas +export const ImageRandomPrompts = [ + 'A woman hiking, close of her boots reflected in a puddle, large mountains in the background, in the style of an advertisement, dramatic angles', + 'Three women stand together laughing, with one woman slightly out of focus in the foreground. The sun is setting behind the women, creating a lens flare and a warm glow that highlights their hair and creates a bokeh effect in the background. The photography style is candid and captures a genuine moment of connection and happiness between friends. The warm light of golden hour lends a nostalgic and intimate feel to the image', + 'A weathered, wooden mech robot covered in flowering vines stands peacefully in a field of tall wildflowers, with a small bluebird resting on its outstretched hand. Digital cartoon, with warm colors and soft lines. A large cliff with a waterfall looms behind', + 'A real life dragon resting peacefully in a zoo, curled up next to its pet sheep. Cinematic movie still, high quality DSLR photo', + 'A large, colorful bouquet of flowers in an old blue glass vase on the table. In front is one beautiful peony flower surrounded by various other blossoms like roses, lilies, daisies, orchids, fruits, berries, green leaves. The background is dark gray. Oil painting in the style of the Dutch Golden Age', + 'Claymation scene. A medium wide shot of an elderly woman. She is wearing flowing clothing. She is standing in a lush garden watering the plants with an orange watering can', + "A view of a person's hand as they hold a little clay figurine of a bird in their hand and sculpt it with a modeling tool in their other hand. You can see the sculptor's scarf. Their hands are covered in clay dust. a macro DSLR image highlighting the texture and craftsmanship", + "White fluffy bear toy is sleeping in a children's room, on the floor of a baby bedroom with toy boxes and toys around, in the style of photorealistic 3D rendering", + 'A professional studio photo of french fries for a high end restaurant, in the style of a food magazine', + "A single comic book panel of an old dog and an adult man on a grassy hill, staring at the sunset. A speech bubble points from the man's mouth and says: 'The sun will rise again'. Muted, late 1990s coloring style", + "A photograph of a stately library entrance with the words 'Central Library' carved into the stone", + 'A close up of a warm and fuzzy colorful Peruvian poncho laying on a top of a chair in a bright day', + "Close up of a musician's fingers playing the piano, black and white film, vintage", + 'Close up shot, In a dimly lit jazz club, a soulful saxophone player, their face contorted in concentration, pours their heart out through their music. A small group of people listen intently, feeling every emotion', + 'Aerial shot of a river flowing up a mystical valley', + 'A sketch of a modern apartment building (subject) surrounded by skyscrapers', + 'Close up photo of a woman in her 20s, street photography, canon, movie still, muted orange warm tones', + 'A photo of a modern building with water in the background', + 'A photo of a chocolate bar on a kitchen counter', + 'An charcoal drawing of an angular sporty electric sedan with skyscrapers in the background', + 'A photo of a forest canopy with blue skies from below', + 'A studio photo of a modern arm chair, dramatic lighting', + 'Soft focus photograph of a bridge in an urban city at night', + 'Photo of a city with skyscrapers from the inside of a car with motion blur', + 'Photo of a leaf, macro lens', + 'A wind farm in the style of a renaissance painting', + 'A man wearing all white clothing sitting on the beach, close up, golden hour lighting', + 'A digital render of a massive skyscraper, modern, grand, epic with a beautiful sunset in the background', + '4K video game concept art, urban jungle, cyberpunk city, detailed rendering', + 'A woman, 35mm portrait, blue and grey duotones', + 'A plate of pasta, 100mm Macro lens', + 'A winning touchdown, fast shutter speed, movement tracking', + 'A deer running in the forest, fast shutter speed, movement tracking', + 'A photo of the moon, astro photography, wide angle 10mm', +] diff --git a/src/app/api/generate-video-utils.tsx b/src/app/api/generate-video-utils.tsx new file mode 100644 index 00000000..29aacfc5 --- /dev/null +++ b/src/app/api/generate-video-utils.tsx @@ -0,0 +1,546 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + advancedSettingsI, + generalSettingsI, + GenerateFieldI1, + GenerateFieldSecondartStyleI, + GenerateFieldStyleI, + selectFieldsI, +} from './generate-image-utils' + +export interface GenerateVideoFormFieldsI { + prompt: GenerateFieldI1 + modelVersion: GenerateFieldI1 + sampleCount: GenerateFieldI1 + negativePrompt: GenerateFieldI1 + aspectRatio: GenerateFieldI1 + durationSeconds: GenerateFieldI1 + personGeneration: GenerateFieldI1 + style: GenerateFieldStyleI + secondary_style: GenerateFieldSecondartStyleI + motion: GenerateFieldI1 + effects: GenerateFieldI1 + framing: GenerateFieldI1 + angle: GenerateFieldI1 + ambiance: GenerateFieldI1 + interpolImageFirst: GenerateFieldI1 + interpolImageLast: GenerateFieldI1 + cameraPreset: GenerateFieldI1 +} + +export const GenerateVideoFormFields = { + prompt: { + type: 'textInput', + isDataResetable: true, + isFullPromptAdditionalField: false, + }, + modelVersion: { + type: 'select', + default: 'veo-3.0-generate-preview', + options: [ + { + value: 'veo-3.0-generate-preview', + label: 'Veo 3', + }, + { + value: 'veo-2.0-generate-001', + label: 'Veo 2', + }, + ], + isDataResetable: false, + isFullPromptAdditionalField: false, + }, + isVideoWithAudio: { + type: 'toggleSwitch', + default: false, + isDataResetable: false, + isFullPromptAdditionalField: false, + }, + sampleCount: { + label: 'Quantity of outputs', + type: 'chip-group', + default: '1', + options: ['1', '2', '3', '4'], + isDataResetable: false, + isFullPromptAdditionalField: false, + }, + negativePrompt: { + type: 'textInput', + isDataResetable: true, + isFullPromptAdditionalField: false, + }, + aspectRatio: { + label: 'Aspect ratio', + type: 'chip-group', + default: '16:9', + options: ['16:9', '9:16'], + isDataResetable: false, + isFullPromptAdditionalField: false, + }, + durationSeconds: { + label: 'Video duration (seconds)', + type: 'chip-group', + default: '8', + options: ['5', '6', '7', '8'], + isDataResetable: true, + isFullPromptAdditionalField: false, + }, + personGeneration: { + label: 'People generation', + type: 'select', + default: 'allow_adult', + options: [ + { + value: 'allow_adult', + label: 'Adults only', + }, + { + value: 'dont_allow', + label: 'No people', + }, + ], + isDataResetable: false, + isFullPromptAdditionalField: false, + }, + style: { + type: 'select', + default: 'cinematic', + defaultSub: 'cinematicSub', + options: [ + { + value: 'cinematic', + label: 'Cinematic', + subID: 'cinematicSub', + }, + { + value: 'animation', + label: 'Animation', + subID: 'animationSub', + }, + ], + isDataResetable: false, + isFullPromptAdditionalField: false, + }, + secondary_style: { + type: 'controled-chip-group', + options: [ + { + label: 'Cinematic style', + subID: 'cinematicSub', + type: 'select', + options: [ + 'Film', + 'Black & White', + 'Horror', + 'Fantasy', + 'Western', + 'Silent film', + 'Vintage', + 'Documentary', + 'Action sequence', + 'Footage', + 'Drone footage', + ], + default: '', + }, + { + label: 'Animation style', + subID: 'animationSub', + type: 'select', + options: [ + '3D animation', + '3D cartoon', + 'Japan anime', + 'Classic cartoon', + 'Comic book', + 'Stop-motion', + 'Claymation', + 'Pixel art', + 'Vector art', + 'Motion graphics', + 'Whiteboard', + 'Cutout', + ], + default: '', + }, + ], + isDataResetable: true, + isFullPromptAdditionalField: false, + }, + motion: { + label: 'Camera motion', + type: 'chip-group', + options: ['Aerial', 'Tracking', 'POV', 'Orbit', 'Zoom in', 'Zoom out', 'Static', 'Panning', 'Tilting', 'Handheld'], + isDataResetable: true, + isFullPromptAdditionalField: true, + }, + framing: { + label: 'Framing', + type: 'chip-group', + options: ['Extreme wide', 'Wide', 'Medium', 'Close-up', 'Extreme close-Up', 'Over-the-shoulder'], + isDataResetable: true, + isFullPromptAdditionalField: true, + }, + angle: { + label: 'Angle', + type: 'chip-group', + options: ['High', 'Low', 'Eye-level', "Bird's eye"], + isDataResetable: true, + isFullPromptAdditionalField: true, + }, + ambiance: { + label: 'Ambiance', + type: 'chip-group', + options: ['Bright daylight', 'Golden hour', 'Night scene', 'Moody', 'Monochrome', 'Neon', 'Silhouette', 'Dramatic'], + isDataResetable: true, + isFullPromptAdditionalField: true, + }, + effects: { + label: 'Special effects', + type: 'chip-group', + options: [ + 'Film grain', + 'Slow motion', + 'Hyperlapse', + 'Split screen', + 'Glitch', + 'Analog noise', + 'Projection', + 'Visual collage', + 'Motion blur', + ], + isDataResetable: true, + isFullPromptAdditionalField: true, + }, + interpolImageFirst: { + type: 'image', + isDataResetable: true, + isFullPromptAdditionalField: false, + }, + interpolImageLast: { + type: 'image', + isDataResetable: true, + isFullPromptAdditionalField: false, + }, + cameraPreset: { + label: 'Camera preset', + type: 'chip-group', + default: '', + options: [ + 'Fixed', + 'Pan left', + 'Pan right', + 'Push in', + 'Pull out', + 'Pedestal down', + 'Pedestal up', + 'Truck left', + 'Truck right', + 'Tilt down', + 'Tilt up', + ], + isDataResetable: true, + isFullPromptAdditionalField: false, + }, +} + +// Camera preset options +export const cameraPresetsOptions = [ + { + value: 'FIXED', + label: 'Fixed', + }, + { + value: 'PAN_LEFT', + label: 'Pan left', + }, + { + value: 'PAN_RIGHT', + label: 'Pan right', + }, + { + value: 'PULL_OUT', + label: 'Pull out', + }, + { + value: 'PUSH_IN', + label: 'Push in', + }, + { + value: 'PEDESTAL_DOWN', + label: 'Pedestal down', + }, + { + value: 'PEDESTAL_UP', + label: 'Pedestal up', + }, + { + value: 'TRUCK_LEFT', + label: 'Truck left', + }, + { + value: 'TRUCK_RIGHT', + label: 'Truck right', + }, + { + value: 'TILT_DOWN', + label: 'Tilt down', + }, + { + value: 'TILT_UP', + label: 'Tilt up', + }, +] + +// Interface of Image use for interpolation during video generation +export const InterpolImageDefaults = { + format: 'image/png', + base64Image: '', + purpose: '', + ratio: '', + width: 0, + height: 0, +} + +export interface InterpolImageI { + format: string + base64Image: string + purpose: 'first' | 'last' + ratio: string + width: number + height: number +} + +// Set default values for Generate Form +const generateFieldList: [keyof GenerateVideoFormFieldsI] = Object.keys(GenerateVideoFormFields) as [ + keyof GenerateVideoFormFieldsI +] +var formDataDefaults: any +generateFieldList.forEach((field) => { + const fieldParams: GenerateFieldI1 | GenerateFieldStyleI | GenerateFieldSecondartStyleI = + GenerateVideoFormFields[field] + const defaultValue = 'default' in fieldParams ? fieldParams.default : '' + formDataDefaults = { ...formDataDefaults, [field]: defaultValue } +}) +formDataDefaults.interpolImageFirst = { ...InterpolImageDefaults, purpose: 'first' } +formDataDefaults.interpolImageLast = { ...InterpolImageDefaults, purpose: 'last' } + +interface CompositionFieldsI { + motion: GenerateFieldI1 + framing: GenerateFieldI1 + ambiance: GenerateFieldI1 + effects: GenerateFieldI1 + angle: GenerateFieldI1 +} +export interface VideoGenerationFieldsI { + model: GenerateFieldI1 + settings: generalSettingsI + advancedSettings: advancedSettingsI + styleOptions: GenerateFieldStyleI + subStyleOptions: GenerateFieldSecondartStyleI + compositionOptions: CompositionFieldsI + cameraPreset: GenerateFieldI1 + resetableFields: (keyof GenerateVideoFormFieldsI)[] + fullPromptFields: (keyof GenerateVideoFormFieldsI)[] + defaultValues: any +} + +// Sort out Generate fields depending on purpose +export const videoGenerationUtils: VideoGenerationFieldsI = { + model: GenerateVideoFormFields.modelVersion, + settings: { + aspectRatio: GenerateVideoFormFields.aspectRatio, + durationSeconds: GenerateVideoFormFields.durationSeconds, + sampleCount: GenerateVideoFormFields.sampleCount, + }, + advancedSettings: { + personGeneration: GenerateVideoFormFields.personGeneration, + }, + styleOptions: GenerateVideoFormFields.style, + subStyleOptions: GenerateVideoFormFields.secondary_style, + compositionOptions: { + ambiance: GenerateVideoFormFields.ambiance, + effects: GenerateVideoFormFields.effects, + framing: GenerateVideoFormFields.framing, + motion: GenerateVideoFormFields.motion, + angle: GenerateVideoFormFields.angle, + }, + cameraPreset: GenerateVideoFormFields.cameraPreset, + resetableFields: generateFieldList.filter((field) => GenerateVideoFormFields[field].isDataResetable == true), + fullPromptFields: generateFieldList.filter( + (field) => GenerateVideoFormFields[field].isFullPromptAdditionalField == true + ), + defaultValues: formDataDefaults, +} + +//TODO temp - remove when Veo 3 is fully released +export const tempVeo3specificSettings = { + sampleCount: { + label: 'Quantity of outputs', + type: 'chip-group', + default: '1', + options: ['1', '2'], + isDataResetable: false, + isFullPromptAdditionalField: false, + }, + aspectRatio: { + label: 'Aspect ratio', + type: 'chip-group', + default: '16:9', + options: ['16:9'], + isDataResetable: false, + isFullPromptAdditionalField: false, + }, + durationSeconds: { + label: 'Video duration (seconds)', + type: 'chip-group', + default: '8', + options: ['8'], + isDataResetable: true, + isFullPromptAdditionalField: false, + }, +} + +// Interface of Generate form fields +export interface GenerateVideoFormI { + prompt: string + modelVersion: string + isVideoWithAudio: boolean + sampleCount: string + negativePrompt: string + aspectRatio: string + durationSeconds: string + personGeneration: string + style: string + secondary_style: string + motion: string + effects: string + composition: string + angle: string + ambiance: string + interpolImageFirst: InterpolImageI + interpolImageLast: InterpolImageI + cameraPreset: string +} + +// Interface of Video object created after image generation +export interface VideoI { + src: string + gcsUri: string + ratio: string + duration: number + thumbnailGcsUri: string + width: number + height: number + altText: string + key: string + format: string + prompt: string + date: string + author: string + modelVersion: string + mode: string +} + +// Interface for the successful initiation response +export interface GenerateVideoInitiationResult { + operationName: string + prompt: string +} + +// Interface for error responses +export interface ErrorResult { + error: string +} + +// Interface definitions needed for polling +export interface VideoSample { + video: { uri: string; encoding: string } +} +export interface PollingSuccessResponse { + '@type': string + generatedSamples: VideoSample[] +} +export interface PollingResponse { + name: string + done: boolean + error?: { code: number; message: string; details: any[] } + response?: { + raiMediaFilteredReasons: boolean + '@type': string + videos?: VideoSample[] + } +} + +export interface VideoGenerationStatusResult { + done: boolean + name?: string + videos?: VideoI[] + error?: string +} + +// Interface of result sent back by Veo within GCS +export interface VeoModelResultI { + gcsUri: string + mimeType: string +} + +// Interface defining the input structure for clarity and type safety +export interface BuildVideoListParams { + videosInGCS: VeoModelResultI[] + aspectRatio: string + duration: number + width: number + height: number + usedPrompt: string + userID: string + modelVersion: string + mode: string +} + +// Interface defining the potential output objects from the map before filtering +export type ProcessedVideoResult = VideoI | { warning: string } | { error: string } + +// Metadata needed for polling result processing +export interface OperationMetadataI { + formData: GenerateVideoFormI + prompt: string +} + +// List of Veo available ratio and their corresponding generation dimentions +export const VideoRatioToPixel = [ + { ratio: '9:16', width: 720, height: 1280 }, + { ratio: '16:9', width: 1280, height: 720 }, +] + +// Random prompt list the user can use if they lack prompt ideas +export const VideoRandomPrompts = [ + 'A close-up cinematic shot follows a desperate man in a weathered green trench coat as he dials a rotary phone mounted on a gritty brick wall, bathed in the eerie glow of a green neon sign. The camera dollies in, revealing the tension in his jaw and the desperation etched on his face as he struggles to make the call. The shallow depth of field focuses on his furrowed brow and the black rotary phone, blurring the background into a sea of neon colors and indistinct shadows, creating a sense of urgency and isolation.', + 'Tracking drone view of a man driving a red convertible car in Palm Springs, 1970s, warm sunlight, long shadows', + 'A POV shot from a vintage car driving in the rain, Canada at night, cinematic', + 'Over the shoulder of a young woman in a car, 1970s, film grain, horror film, cinematic', + 'Film noir style, man and woman walk on the street, mystery, cinematic, black and white', + 'A cute creatures with snow leopard-like fur is walking in winter forest, 3D cartoon style render', + 'An architectural rendering of a white concrete apartment building with flowing organic shapes, seamlessly blending with lush greenery and futuristic elements.', + 'Extreme close-up of a an eye with city reflected in it', + 'A wide shot of surfer walking on a beach with a surfboard, beautiful sunset, cinematic', + 'A close-up of a girl holding adorable golden retriever puppy in the park, sunlight', + 'Cinematic close-up shot of a sad woman riding a bus in the rain, cool blue tones, sad mood', + 'A double exposure of silhouetted profile of a woman walking and lake, walking in a forest', + 'Close-up shot of a model with blue light with geometric shapes projected on her face', + 'Silhouette of a man walking in collage of cityscapes', + 'Glitch camera effect, close up of woman’s face speaking, neon colors', +] diff --git a/src/app/api/google-auth/route.ts b/src/app/api/google-auth/route.ts new file mode 100644 index 00000000..398ae13f --- /dev/null +++ b/src/app/api/google-auth/route.ts @@ -0,0 +1,34 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NextRequest, NextResponse } from 'next/server' + +export async function GET(req: NextRequest) { + let response = {} + + try { + if (req.headers.get('X-Goog-Authenticated-User-Email')) { + response = { + targetPrincipal: req.headers.get('X-Goog-Authenticated-User-Email'), + } + } else { + throw Error('ID header not found') + } + } catch (error) { + console.error(error) + response = { error: 'Authentication error', status: 500 } + } + + return NextResponse.json(response) +} diff --git a/src/app/api/imagen/action.tsx b/src/app/api/imagen/action.tsx new file mode 100644 index 00000000..a3c11ecd --- /dev/null +++ b/src/app/api/imagen/action.tsx @@ -0,0 +1,801 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use server' + +import { + GenerateImageFormI, + ImagenModelResultI, + ImageI, + RatioToPixel, + referenceTypeMatching, + ReferenceObjectI, + imageGenerationUtils, +} from '../generate-image-utils' +import { decomposeUri, downloadMediaFromGcs, getSignedURL, uploadBase64Image } from '../cloud-storage/action' +import { getFullReferenceDescription, rewriteWithGemini } from '../gemini/action' +import { appContextDataI } from '../../context/app-context' +import { EditImageFormI } from '../edit-utils' +const { GoogleAuth } = require('google-auth-library') + +function cleanResult(inputString: string) { + return inputString.toString().replaceAll('\n', '').replaceAll(/\//g, '').replaceAll('*', '') +} + +function generateUniqueFolderId() { + let number = Math.floor(Math.random() * 9) + 1 + for (let i = 0; i < 12; i++) number = number * 10 + Math.floor(Math.random() * 10) + return number +} + +export async function normalizeSentence(sentence: string) { + // Split the sentence into individual words + const words = sentence.toLowerCase().split(' ') + + // Capitalize the first letter of each sentence + let normalizedSentence = '' + let newSentence = true + for (let i = 0; i < words.length; i++) { + let word = words[i] + if (newSentence) { + word = word.charAt(0).toUpperCase() + word.slice(1) + newSentence = false + } + if (word.endsWith('.') || word.endsWith('!') || word.endsWith('?')) { + newSentence = true + } + normalizedSentence += word + ' ' + } + + // Replace multiple spaces with single spaces + normalizedSentence = normalizedSentence.replace(/ +/g, ' ') + + // Remove any trailing punctuation and spaces + normalizedSentence = normalizedSentence.trim() + + // Remove double commas + normalizedSentence = normalizedSentence.replace(/, ,/g, ',') + + return normalizedSentence +} + +async function generatePrompt(formData: any, isGeminiRewrite: boolean, references?: ReferenceObjectI[]) { + let fullPrompt = formData['prompt'] + + // Add the photo/ art/ digital style to the prompt + fullPrompt = `A ${formData['secondary_style']} ${formData['style']} of ` + fullPrompt + + // Add additional parameters to the prompt + let parameters = '' + imageGenerationUtils.fullPromptFields.forEach((additionalField) => { + if (formData[additionalField] !== '') + parameters += ` ${formData[additionalField]} ${additionalField.replaceAll('_', ' ')}, ` + }) + if (parameters !== '') fullPrompt = `${fullPrompt}, ${parameters}` + + // Add quality modifiers to the prompt for Image Generation + let quality_modifiers = '' + if (formData['style'] === 'photo') { + quality_modifiers = quality_modifiers + ', 4K' + } else quality_modifiers = quality_modifiers + ', by a professional, detailed' + + if (formData['use_case'] === 'Food, insects, plants (still life)') + quality_modifiers = quality_modifiers + ', High detail, precise focusing, controlled lighting' + + if (formData['use_case'] === 'Sports, wildlife (motion)') + quality_modifiers = quality_modifiers + ', Fast shutter speed, movement tracking' + + if (formData['use_case'] === 'Astronomical, landscape (wide-angle)') + quality_modifiers = quality_modifiers + ', Long exposure times, sharp focus, long exposure, smooth water or clouds' + + fullPrompt = fullPrompt + quality_modifiers + + // Old, now directly handled my model + // Rewrite the content of the prompt + /*if (isGeminiRewrite) { + try { + const isStyleRefProvided = references && references.some((ref) => ref.referenceType === 'Style') + const isPersonRefProvided = references && references.some((ref) => ref.referenceType === 'Person') + const isAnimalRefProvided = references && references.some((ref) => ref.referenceType === 'Animal') + const isObjectRefProvided = references && references.some((ref) => ref.referenceType === 'Product') + + const geminiReturnedPrompt = await rewriteWithGemini( + fullPrompt, + 'Image', + isPersonRefProvided, + isAnimalRefProvided, + isObjectRefProvided, + isStyleRefProvided + ) + + if (typeof geminiReturnedPrompt === 'object' && 'error' in geminiReturnedPrompt) { + const errorMsg = cleanResult(JSON.stringify(geminiReturnedPrompt['error']).replaceAll('Error: ', '')) + throw Error(errorMsg) + } else fullPrompt = geminiReturnedPrompt as string + } catch (error) { + console.error(error) + return { error: 'Error while rewriting prompt with Gemini .' } + } + }*/ + + // Add references to the prompt + if (references !== undefined && references.length > 0) { + let reference = 'Generate an image ' + let subjects: string[] = [] + let subjectsID: number[] = [] + let styles: string[] = [] + let stylesID: number[] = [] + + for (const [index, reference] of references.entries()) { + const params = referenceTypeMatching[reference.referenceType as keyof typeof referenceTypeMatching] + + if (params.referenceType === 'REFERENCE_TYPE_SUBJECT') + if (!subjectsID.includes(reference.refId)) { + subjects.push(`a ${reference.description.toLowerCase()} [${reference.refId}]`) + subjectsID.push(reference.refId) + } + + if (params.referenceType === 'REFERENCE_TYPE_STYLE') + if (!stylesID.includes(reference.refId)) { + styles.push(`in a ${reference.description.toLowerCase()} style [${reference.refId}]`) + stylesID.push(reference.refId) + } + } + + if (subjects.length > 0) reference = reference + 'about ' + subjects.join(', ') + if (styles.length > 0) reference = reference.trim() + ', ' + styles.join(', ') + reference = reference + ' to match the description: ' + + fullPrompt = reference + fullPrompt + } + + fullPrompt = normalizeSentence(fullPrompt) + + return fullPrompt +} + +export async function buildImageListFromURI({ + imagesInGCS, + aspectRatio, + width, + height, + usedPrompt, + userID, + modelVersion, + mode, +}: { + imagesInGCS: ImagenModelResultI[] + aspectRatio: string + width: number + height: number + usedPrompt: string + userID: string + modelVersion: string + mode: string +}) { + const promises = imagesInGCS.map(async (image) => { + if ('raiFilteredReason' in image) { + return { + warning: `${image['raiFilteredReason']}`, + } + } else { + const { fileName } = await decomposeUri(image.gcsUri ?? '') + + const format = image.mimeType.replace('image/', '').toUpperCase() + + const ID = fileName + .replaceAll('/', '') + .replace(userID, '') + .replace('generated-images', '') + .replace('edited-images', '') + .replace('sample_', '') + .replace(`.${format.toLowerCase()}`, '') + + const today = new Date() + const formattedDate = today.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }) + + // Get signed URL from Cloud Storage API + try { + const signedURL: string | { error: string } = await getSignedURL(image.gcsUri ?? '') + + if (typeof signedURL === 'object' && 'error' in signedURL) { + throw Error(cleanResult(signedURL['error'])) + } else { + return { + src: signedURL, + gcsUri: image.gcsUri, + format: format, + prompt: image.prompt && image.prompt != '' ? image.prompt : usedPrompt, + altText: `Generated image ${fileName}`, + key: ID, + width: width, + height: height, + ratio: aspectRatio, + date: formattedDate, + author: userID, + modelVersion: modelVersion, + mode: mode, + } + } + } catch (error) { + console.error(error) + return { + error: 'Error while getting secured access to content.', + } + } + } + }) + + const generatedImagesToDisplay = (await Promise.all(promises)).filter( + (image) => image !== null + ) as unknown as ImageI[] + + return generatedImagesToDisplay +} + +export async function buildImageListFromBase64({ + imagesBase64, + targetGcsURI, + aspectRatio, + width, + height, + usedPrompt, + userID, + modelVersion, + mode, +}: { + imagesBase64: ImagenModelResultI[] + targetGcsURI: string + aspectRatio: string + width: number + height: number + usedPrompt: string + userID: string + modelVersion: string + mode: string +}) { + const bucketName = targetGcsURI.replace('gs://', '').split('/')[0] + let uniqueFolderId = generateUniqueFolderId() + const folderName = targetGcsURI.split(bucketName + '/')[1] + '/' + uniqueFolderId + + const promises = imagesBase64.map(async (image) => { + if ('raiFilteredReason' in image) { + return { + warning: `${image['raiFilteredReason']}`, + } + } else { + const format = image.mimeType.replace('image/', '').toUpperCase() + + const index = imagesBase64.findIndex((obj) => obj.bytesBase64Encoded === image.bytesBase64Encoded) + const fileName = 'sample_' + index.toString() + + const fullOjectName = folderName + '/' + fileName + '.' + format.toLocaleLowerCase() + + const ID = fullOjectName + .replaceAll('/', '') + .replace(userID, '') + .replace('generated-images', '') + .replace('edited-images', '') + .replace('sample_', '') + .replace(`.${format.toLowerCase()}`, '') + + const today = new Date() + const formattedDate = today.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }) + + // Store base64 image in GCS, and get signed URL associated + try { + let imageGcsUri = '' + await uploadBase64Image(image.bytesBase64Encoded ?? '', bucketName, fullOjectName).then((result) => { + if (!result.success) throw Error(cleanResult(result.error ?? 'Could not upload image to GCS')) + imageGcsUri = result.fileUrl ?? '' + }) + + const signedURL: string | { error: string } = await getSignedURL(imageGcsUri) + + if (typeof signedURL === 'object' && 'error' in signedURL) { + throw Error(cleanResult(signedURL['error'])) + } else { + return { + src: signedURL, + gcsUri: imageGcsUri, + format: format, + prompt: image.prompt && image.prompt != '' ? image.prompt : usedPrompt, + altText: `Generated image ${fileName}`, + key: ID, + width: width, + height: height, + ratio: aspectRatio, + date: formattedDate, + author: userID, + modelVersion: modelVersion, + mode: mode, + } + } + } catch (error) { + console.error(error) + return { + error: 'Error while getting secured access to content.', + } + } + } + }) + + const generatedImagesToDisplay = (await Promise.all(promises)).filter( + (image) => image !== null + ) as unknown as ImageI[] + + return generatedImagesToDisplay +} + +export async function generateImage( + formData: GenerateImageFormI, + areAllRefValid: boolean, + isGeminiRewrite: boolean, + appContext: appContextDataI | null +) { + // 1 - Atempting to authent to Google Cloud & fetch project informations + let client + try { + const auth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform', + }) + client = await auth.getClient() + } catch (error) { + console.error(error) + return { + error: 'Unable to authenticate your account to access images', + } + } + + let references = formData['referenceObjects'] + + if (!areAllRefValid) references = [] + const modelVersion = formData['modelVersion'] + const location = + modelVersion === 'imagen-4.0-generate-preview-05-20' ? 'us-central1' : process.env.NEXT_PUBLIC_VERTEX_API_LOCATION //TODO temp - update when not in Preview anymore + const projectId = process.env.NEXT_PUBLIC_PROJECT_ID + const imagenAPIurl = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${modelVersion}:predict` + + // 2 - Building the prompt and rewrite it if needed with Gemini + let fullPrompt + try { + fullPrompt = await generatePrompt(formData, isGeminiRewrite, references) + + if (typeof fullPrompt === 'object' && 'error' in fullPrompt) { + throw Error(fullPrompt.error) + } + } catch (error) { + console.error(error) + return { + error: 'An error occurred while generating the prompt.', + } + } + + if (appContext === undefined) throw Error('No provided app context') + + // 3 - Building Imagen request body + let generationGcsURI = '' + if ( + appContext === undefined || + appContext === null || + appContext.gcsURI === undefined || + appContext.userID === undefined + ) + throw Error('No provided app context') + else { + generationGcsURI = `${appContext.gcsURI}/${appContext.userID}/generated-images` + } + let reqData: any = { + instances: [ + { + prompt: fullPrompt as string, + }, + ], + parameters: { + sampleCount: parseInt(formData['sampleCount']), + negativePrompt: formData['negativePrompt'], + aspectRatio: formData['aspectRatio'], + outputOptions: { + mimeType: formData['outputOptions'], + }, + includeRaiReason: true, + personGeneration: formData['personGeneration'], + storageUri: generationGcsURI, + enhancePrompt: isGeminiRewrite, + }, + } + + // Adding references if necessary + if (areAllRefValid) { + reqData.parameters.editMode = 'EDIT_MODE_DEFAULT' + + reqData.instances[0].referenceImages = [] + let fullRefDescriptionDone: number[] = [] + for (const [index, reference] of references.entries()) { + const params = referenceTypeMatching[reference.referenceType as keyof typeof referenceTypeMatching] + + let newReference: any = { + referenceType: params.referenceType, + referenceId: reference.refId, + referenceImage: { + bytesBase64Encoded: reference.base64Image.startsWith('data:') + ? reference.base64Image.split(',')[1] + : reference.base64Image, + }, + } + + if (params.referenceType === 'REFERENCE_TYPE_SUBJECT') + newReference = { + ...newReference, + subjectImageConfig: { + subjectDescription: reference.description, + subjectType: params.subjectType, + }, + } + + if (params.referenceType === 'REFERENCE_TYPE_STYLE') + newReference = { + ...newReference, + styleImageConfig: { + styleDescription: reference.description, + }, + } + + // Adding new reference to the API request data + reqData.instances[0].referenceImages[index] = newReference + + // Fetching for each reference a full description to add to the prompt for more performant results + if (!fullRefDescriptionDone.includes(reference.refId)) { + fullRefDescriptionDone.push(reference.refId) + const fullAIrefDescription = await getFullReferenceDescription(reference.base64Image, reference.referenceType) + reqData.instances[0].prompt = reqData.instances[0].prompt + `\n\n[${reference.refId}] ` + fullAIrefDescription + } + } + } + const opts = { + url: imagenAPIurl, + method: 'POST', + data: reqData, + } + + // 4 - Generating images + try { + const res = await client.request(opts) + + if (res.data.predictions === undefined) throw Error('There were an issue, no images were generated') + + // NO images at all were generated out of all samples + if ('raiFilteredReason' in res.data.predictions[0]) + throw Error(cleanResult(res.data.predictions[0].raiFilteredReason)) + + const usedRatio = RatioToPixel.find((item) => item.ratio === opts.data.parameters.aspectRatio) + + const resultImages: ImagenModelResultI[] = res.data.predictions + + const isResultBase64Images: boolean = resultImages.every((image) => image.hasOwnProperty('bytesBase64Encoded')) + + let enhancedImageList + if (isResultBase64Images) + enhancedImageList = await buildImageListFromBase64({ + imagesBase64: resultImages, + targetGcsURI: generationGcsURI, + aspectRatio: opts.data.parameters.aspectRatio, + width: usedRatio?.width ?? 0, + height: usedRatio?.height ?? 0, + usedPrompt: opts.data.instances[0].prompt, + userID: appContext?.userID ? appContext?.userID : '', + modelVersion: modelVersion, + mode: 'Generated', + }) + else + enhancedImageList = await buildImageListFromURI({ + imagesInGCS: resultImages, + aspectRatio: opts.data.parameters.aspectRatio, + width: usedRatio?.width ?? 0, + height: usedRatio?.height ?? 0, + usedPrompt: opts.data.instances[0].prompt, + userID: appContext?.userID ? appContext?.userID : '', + modelVersion: modelVersion, + mode: 'Generated', + }) + + return enhancedImageList + } catch (error) { + const errorString = error instanceof Error ? error.toString() : String(error) + console.error(errorString) + + if ( + errorString.includes('safety settings for peopleface generation') || + errorString.includes("All images were filtered out because they violated Vertex AI's usage guidelines") || + errorString.includes('Person Generation') + ) + return { + error: errorString.replace(/^Error: /i, ''), + } + + const myError = error as Error & { errors: any[] } + let myErrorMsg = '' + if (myError.errors && myError.errors[0] && myError.errors[0].message) + myErrorMsg = myError.errors[0].message.replace('Image generation failed with the following error: ', '') + + return { + error: myErrorMsg || 'An unexpected error occurred.', + } + } +} + +export async function editImage(formData: EditImageFormI, appContext: appContextDataI | null) { + // 1 - Atempting to authent to Google Cloud & fetch project informations + let client + try { + const auth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform', + }) + client = await auth.getClient() + } catch (error) { + console.error(error) + return { + error: 'Unable to authenticate your account to access images', + } + } + + const location = process.env.NEXT_PUBLIC_VERTEX_API_LOCATION + const projectId = process.env.NEXT_PUBLIC_PROJECT_ID + const modelVersion = formData['modelVersion'] + const imagenAPIurl = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${modelVersion}:predict` + + if (appContext === undefined) throw Error('No provided app context') + + // 2 - Building Imagen request body + let editGcsURI = '' + if ( + appContext === undefined || + appContext === null || + appContext.gcsURI === undefined || + appContext.userID === undefined + ) + throw Error('No provided app context') + else { + editGcsURI = `${appContext.gcsURI}/${appContext.userID}/edited-images` + } + + const refInputImage = formData['inputImage'].startsWith('data:') + ? formData['inputImage'].split(',')[1] + : formData['inputImage'] + const refInputMask = formData['inputMask'].startsWith('data:') + ? formData['inputMask'].split(',')[1] + : formData['inputMask'] + + const editMode = formData['editMode'] + + const reqData = { + instances: [ + { + prompt: formData.prompt as string, + referenceImages: [ + { + referenceType: 'REFERENCE_TYPE_RAW', + referenceId: 1, + referenceImage: { + bytesBase64Encoded: refInputImage, + }, + }, + { + referenceType: 'REFERENCE_TYPE_MASK', + referenceId: 2, + referenceImage: { + bytesBase64Encoded: refInputMask, + }, + maskImageConfig: { + maskMode: 'MASK_MODE_USER_PROVIDED', + dilation: parseFloat(formData['maskDilation']), + }, + }, + ], + }, + ], + parameters: { + negativePrompt: formData['negativePrompt'], + promptLanguage: 'en', + seed: 1, + editConfig: { + baseSteps: parseInt(formData['baseSteps']), + }, + editMode: editMode, + sampleCount: parseInt(formData['sampleCount']), + outputOptions: { + mimeType: formData['outputOptions'], + }, + includeRaiReason: true, + personGeneration: formData['personGeneration'], + storageUri: editGcsURI, + }, + } + + if (editMode === 'EDIT_MODE_BGSWAP') { + const referenceImage = reqData.instances[0].referenceImages[1] as any + + delete referenceImage.referenceImage + referenceImage.maskImageConfig.maskMode = 'MASK_MODE_BACKGROUND' + delete referenceImage.maskImageConfig.dilation + } + + const opts = { + url: imagenAPIurl, + method: 'POST', + data: reqData, + } + + // 3 - Editing image + let res + try { + res = await client.request(opts) + + if (res.data.predictions === undefined) { + throw Error('There were an issue, no images were generated') + } + // NO images at all were generated out of all samples + if ('raiFilteredReason' in res.data.predictions[0]) { + throw Error(cleanResult(res.data.predictions[0].raiFilteredReason)) + } + } catch (error) { + console.error(error) + + const errorString = error instanceof Error ? error.toString() : '' + if ( + errorString.includes('safety settings for peopleface generation') || + errorString.includes("All images were filtered out because they violated Vertex AI's usage guidelines") + ) { + return { + error: errorString.replace('Error: ', ''), + } + } + + const myError = error as Error & { errors: any[] } + const myErrorMsg = myError.errors[0].message + + return { + error: myErrorMsg, + } + } + + // 4 - Creating output image list + try { + const resultImages: ImagenModelResultI[] = res.data.predictions + + const isResultBase64Images: boolean = resultImages.every((image) => image.hasOwnProperty('bytesBase64Encoded')) + + let enhancedImageList + if (isResultBase64Images) + enhancedImageList = await buildImageListFromBase64({ + imagesBase64: resultImages, + targetGcsURI: editGcsURI, + aspectRatio: formData['ratio'], + width: formData['width'], + height: formData['height'], + usedPrompt: opts.data.instances[0].prompt, + userID: appContext?.userID ? appContext?.userID : '', + modelVersion: modelVersion, + mode: 'Generated', + }) + else + enhancedImageList = await buildImageListFromURI({ + imagesInGCS: resultImages, + aspectRatio: formData['ratio'], + width: formData['width'], + height: formData['height'], + usedPrompt: opts.data.instances[0].prompt, + userID: appContext?.userID ? appContext?.userID : '', + modelVersion: modelVersion, + mode: 'Edited', + }) + + return enhancedImageList + } catch (error) { + console.error(error) + return { + error: 'Issue while editing image.', + } + } +} + +export async function upscaleImage(sourceUri: string, upscaleFactor: string, appContext: appContextDataI | null) { + // 1 - Atempting to authent to Google Cloud & fetch project informations + let client + try { + const auth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform', + }) + client = await auth.getClient() + } catch (error) { + console.error(error) + return { + error: 'Unable to authenticate your account to access images', + } + } + const location = process.env.NEXT_PUBLIC_VERTEX_API_LOCATION + const projectId = process.env.NEXT_PUBLIC_PROJECT_ID + const imagenAPIurl = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/imagegeneration@002:predict` + + // 2 Downloading source image + let res + try { + res = await downloadMediaFromGcs(sourceUri) + + if (typeof res === 'object' && res['error']) { + throw Error(res['error'].replaceAll('Error: ', '')) + } + } catch (error: any) { + throw Error(error) + } + const { data } = res + + // 3 - Building Imagen request body + let targetGCSuri = '' + if ( + appContext === undefined || + appContext === null || + appContext.gcsURI === undefined || + appContext.userID === undefined + ) + throw Error('No provided app context') + else { + targetGCSuri = `${appContext.gcsURI}/${appContext.userID}/upscaled-images` + } + const reqData = { + instances: [ + { + prompt: '', + image: { + bytesBase64Encoded: data, + }, + }, + ], + parameters: { + sampleCount: 1, + mode: 'upscale', + upscaleConfig: { + upscaleFactor: upscaleFactor, + }, + storageUri: targetGCSuri, + }, + } + const opts = { + url: imagenAPIurl, + method: 'POST', + data: reqData, + } + + // 4 - Upscaling images + try { + const timeout = 60000 // ms, 20s + + const res = await Promise.race([ + client.request(opts), + new Promise((_, reject) => setTimeout(() => reject(new Error('Upscaling timed out')), timeout)), + ]) + if (res.data.predictions === undefined) { + throw Error('There were an issue, images could not be upscaled') + } + + const newGcsUri: string = res.data.predictions[0].gcsUri + + return newGcsUri + } catch (error) { + console.error(error) + return { + error: 'Error while upscaling images.', + } + } +} diff --git a/src/app/api/veo/action.tsx b/src/app/api/veo/action.tsx new file mode 100644 index 00000000..5088688b --- /dev/null +++ b/src/app/api/veo/action.tsx @@ -0,0 +1,550 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use server' + +import { decomposeUri, getSignedURL, uploadBase64Image } from '../cloud-storage/action' +import { rewriteWithGemini } from '../gemini/action' +import { appContextDataI } from '../../context/app-context' +import { + GenerateVideoFormI, + videoGenerationUtils, + VideoI, + GenerateVideoFormFields, + ErrorResult, + GenerateVideoInitiationResult, + PollingResponse, + VideoGenerationStatusResult, + VideoRatioToPixel, + BuildVideoListParams, + ProcessedVideoResult, + cameraPresetsOptions, +} from '../generate-video-utils' +import { normalizeSentence } from '../imagen/action' +const { GoogleAuth } = require('google-auth-library') + +function cleanResult(inputString: string) { + return inputString.toString().replaceAll('\n', '').replaceAll(/\//g, '').replaceAll('*', '') +} + +const customRateLimitMessage = 'Oops, too many incoming access right now, please try again later!' + +function isResourceExhaustedError(source: any) { + if (!source) return false + + let message = '' + let code = null + + if (typeof source === 'string') { + message = source.toLowerCase() + if (message.includes('code: 8') || message.includes('code === 8')) code = 8 + } else if (typeof source === 'object' && source !== null) { + message = String(source.message || '').toLowerCase() + code = source.code + } else return false + + if ( + (code === 8 && message.includes('resource exhausted')) || + message.includes("{ code: 8, message: 'resource exhausted.' }") || + (message.includes('resource exhausted') && + (code === 8 || message.includes('code: 8') || message.includes('code === 8'))) + ) + return true + + return false +} + +async function generatePrompt(formData: any, isGeminiRewrite: boolean) { + let fullPrompt = formData['prompt'] + + // Rewrite the content of the prompt + if (isGeminiRewrite) { + try { + const geminiReturnedPrompt = await rewriteWithGemini(fullPrompt, 'Video') + + if (typeof geminiReturnedPrompt === 'object' && 'error' in geminiReturnedPrompt) { + const errorMsg = cleanResult(JSON.stringify(geminiReturnedPrompt['error']).replaceAll('Error: ', '')) + throw Error(errorMsg) + } else fullPrompt = geminiReturnedPrompt as string + } catch (error) { + console.error(error) + return { error: 'Error while rewriting prompt with Gemini .' } + } + } + + // Add the photo/ art/ digital style to the prompt + fullPrompt = `A ${formData['secondary_style']} ${formData['style']} of ` + fullPrompt + + // Add additional parameters to the prompt + let parameters = '' + videoGenerationUtils.fullPromptFields.forEach((additionalField) => { + if (formData[additionalField] !== '') + parameters += ` ${formData[additionalField]} ${additionalField.replaceAll('_', ' ')}, ` + }) + if (parameters !== '') fullPrompt = `${fullPrompt}, ${parameters}` + + fullPrompt = normalizeSentence(fullPrompt) + + return fullPrompt +} + +// Returns only successfully processed VideoI objects +export async function buildVideoListFromURI({ + videosInGCS, + aspectRatio, + duration, + width, + height, + usedPrompt, + userID, + modelVersion, + mode, +}: BuildVideoListParams): Promise { + const promises = videosInGCS.map(async (videoResult): Promise => { + // 1. Check for RAI filtering + const raiReason = (videoResult as any).raiFilteredReason + if (raiReason) { + console.warn(`Video filtered due to RAI: ${raiReason}. GCS URI: ${videoResult.gcsUri || 'N/A'}`) + return { warning: `Video filtered due to RAI: ${raiReason}` } + } + + // 2. Ensure GCS URI exists - essential for processing + if (!videoResult.gcsUri) { + console.warn('Skipping video result due to missing gcsUri.') + return null + } + + try { + // 3. Decompose URI to get filename (assuming utility handles potential errors) + const { fileName } = await decomposeUri(videoResult.gcsUri) + + // 4. Determine video format from MIME type + const mimeType = videoResult.mimeType || 'video/mp4' + const format = mimeType.replace('video/', '').toUpperCase() + + // 5. Generate a unique-ish key/ID (adjust path segments if necessary) + const ID = fileName + .replaceAll('/', '') + .replace(userID, '') + .replace('generated-videos', '') + .replace('sample_', '') + .replace(`.${format.toLowerCase()}`, '') + + // 6. Format the date + const today = new Date() + const formattedDate = today.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }) + + // 7. Get Signed URL for the video file using the GCS URI + const signedURLResult: string | { error: string } = await getSignedURL(videoResult.gcsUri) + + // Handle potential errors from getting the signed URL + if (typeof signedURLResult === 'object' && signedURLResult['error']) + throw new Error( + `Failed to get signed URL for ${videoResult.gcsUri}: ${ + cleanResult ? cleanResult(signedURLResult['error']) : signedURLResult['error'] + }` + ) + + const signedURL = signedURLResult // Assign if successful + + // 8. Construct the final VideoI object with all metadata + const videoDetails: VideoI = { + src: signedURL as string, + gcsUri: videoResult.gcsUri, + thumbnailGcsUri: '', + format: format, + prompt: usedPrompt, + altText: `Generated ${format} video`, + key: ID || Date.now().toString(), + width: width, + height: height, + ratio: aspectRatio, + duration: duration, + date: formattedDate, + author: userID, + modelVersion: modelVersion, + mode: mode, + } + return videoDetails + } catch (error) { + console.error(`Error processing video result ${videoResult.gcsUri}:`, error) + return { + error: `Error processing video ${videoResult.gcsUri}: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, + } + } + }) + + // Wait for all processing promises to settle + const processedResults = await Promise.all(promises) + + // Filter out nulls, warnings, and errors, keeping only valid VideoI objects + const generatedVideosToDisplay = processedResults.filter( + ( + result + ): result is VideoI => // Type predicate confirms the result is a VideoI object + result !== null && typeof result === 'object' && !('error' in result) && !('warning' in result) + ) + + // Log any errors or warnings encountered during the batch processing + processedResults.forEach((result) => { + if (result && typeof result === 'object') { + if ('error' in result) console.error(`Video Processing Error Skipped: ${result.error}`) + else if ('warning' in result) console.warn(`Video Processing Warning Skipped: ${result.warning}`) + } + }) + + return generatedVideosToDisplay +} + +// Initiates Video generation request, returns long-running operation name needed for polling +export async function generateVideo( + formData: GenerateVideoFormI, + isGeminiRewrite: boolean, + appContext: appContextDataI | null +): Promise { + // 0 - Check requested features + const hasInterpolImageFirst = + formData.interpolImageFirst && + formData.interpolImageFirst.base64Image !== '' && + formData.interpolImageFirst.format !== '' + const hasInterpolImageLast = + formData.interpolImageLast && + formData.interpolImageLast.base64Image !== '' && + formData.interpolImageLast.format !== '' + const isImageToVideo = + (hasInterpolImageFirst && !hasInterpolImageLast) || (hasInterpolImageLast && !hasInterpolImageFirst) + const isInterpolation = hasInterpolImageFirst && hasInterpolImageLast + const isCameraPreset = formData.cameraPreset !== '' + + // 1 - Authenticate to Google Cloud + let client + try { + const auth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform', + }) + client = await auth.getClient() + } catch (error) { + console.error('Authentication Error:', error) + return { error: 'Unable to authenticate your account to access video generation.' } + } + + const location = 'us-central1' //TODO temp - update when not in Preview anymore + const projectId = process.env.NEXT_PUBLIC_PROJECT_ID + let modelVersion = formData.modelVersion || GenerateVideoFormFields.modelVersion.default + if (isInterpolation || isCameraPreset) modelVersion = 'veo-2.0-generate-exp' //TODO temp - update when not in Preview anymore + + // Construct the API URL for initiating long-running video generation + const videoAPIUrl = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${modelVersion}:predictLongRunning` + + // 2 - Build the prompt, potentially rewriting with Gemini + let fullPrompt: string | ErrorResult + if (formData.prompt !== '') { + try { + fullPrompt = await generatePrompt(formData, isGeminiRewrite) + + if (typeof fullPrompt === 'object' && 'error' in fullPrompt) { + throw new Error(fullPrompt.error) + } + } catch (error) { + console.error('Prompt Generation Error:', error) + const errorMessage = error instanceof Error ? error.message : 'An error occurred while generating the prompt.' + return { error: errorMessage } + } + } else fullPrompt = '' + + // 3 - Validate App Context and determine GCS URI + if (!appContext?.gcsURI || !appContext?.userID) { + console.error('Application context error: Missing GCS URI or User ID.') + return { error: 'Application context is missing required information (GCS URI or User ID).' } + } + const generationGcsURI = `${appContext.gcsURI}/${appContext.userID}/generated-videos` + + // 4 - Build Veo request body parameters based on documentation + const parameters = { + sampleCount: parseInt(formData.sampleCount, 10), + aspectRatio: formData.aspectRatio, + durationSeconds: parseInt(formData.durationSeconds, 10), + storageUri: generationGcsURI, + negativePrompt: formData.negativePrompt, + personGeneration: formData.personGeneration, + generateAudio: formData.modelVersion === 'veo-3.0-generate-preview' && formData.isVideoWithAudio, + } + + const reqData: any = { + instances: [ + { + prompt: fullPrompt as string, + }, + ], + parameters: parameters, + } + + if (formData.cameraPreset) + reqData.instances[0].cameraControl = cameraPresetsOptions.find( + (item) => item.label === formData.cameraPreset + )?.value + + // 5 - Handle Image-to-video & interpolation + // 5.1 - Simple ITV use case, either first or last frame provided, API requires the base64 + if (isImageToVideo) { + if (hasInterpolImageFirst) { + const interpolImageFirst = formData.interpolImageFirst.base64Image.startsWith('data:') + ? formData.interpolImageFirst.base64Image.split(',')[1] + : formData.interpolImageFirst.base64Image + reqData.instances[0].image = { + bytesBase64Encoded: interpolImageFirst, + mimeType: formData.interpolImageFirst.format, + } + } else { + const interpolImageLast = formData.interpolImageLast.base64Image.startsWith('data:') + ? formData.interpolImageLast.base64Image.split(',')[1] + : formData.interpolImageLast.base64Image + reqData.instances[0].lastFrame = { + bytesBase64Encoded: interpolImageLast, + mimeType: formData.interpolImageLast.format, + } + } + } + + // 5.2 - Interpolation use case, API requires the GCS URIs of the images + if (isInterpolation) { + try { + // Store base64 image in GCS first + const generationGcsURI = `${appContext.gcsURI}/${appContext.userID}/generated-videos/interpolation-frames` + const bucketName = generationGcsURI.replace('gs://', '').split('/')[0] + const folderName = generationGcsURI.split(bucketName + '/')[1] + + // Handle first frame for interpolation + let interpolImageFirstUri = '' + const objectNameFirst = `${folderName}/${Math.random().toString(36).substring(2, 15)}.${ + formData.interpolImageFirst.format.split('/')[1] + }` + await uploadBase64Image(formData.interpolImageFirst.base64Image.split(',')[1], bucketName, objectNameFirst).then( + (result) => { + if (!result.success) throw Error(cleanResult(result.error ?? 'Could not upload image to GCS')) + interpolImageFirstUri = result.fileUrl ?? '' + } + ) + reqData.instances[0].image = { + gcsUri: interpolImageFirstUri, + mimeType: formData.interpolImageFirst.format, + } + + // Handle Last frame for interpolation + let interpolImageLastUri = '' + const objectNameLast = `${folderName}/${Math.random().toString(36).substring(2, 15)}.${ + formData.interpolImageLast.format.split('/')[1] + }` + await uploadBase64Image(formData.interpolImageLast.base64Image.split(',')[1], bucketName, objectNameLast).then( + (result) => { + if (!result.success) throw Error(cleanResult(result.error ?? 'Could not upload image to GCS')) + interpolImageLastUri = result.fileUrl ?? '' + } + ) + reqData.instances[0].lastFrame = { + gcsUri: interpolImageLastUri, + mimeType: formData.interpolImageLast.format, + } + } catch (error) { + console.error(error) + return { + error: 'Error while getting secured access to content.', + } + } + } + + // 6 - Prepare HTTP request options + const opts = { + url: videoAPIUrl, + method: 'POST', + data: reqData, + } + + // 7 - Initiate video generation request + try { + const res = await client.request(opts) + + // a. Handle successful response + if (res.data?.name) return { operationName: res.data.name, prompt: fullPrompt as string } + + // b. Handle API-returned error (non-exception, structured error in response body) + const apiError = res.data?.error + if (apiError) { + if (isResourceExhaustedError(apiError)) return { error: customRateLimitMessage } + + // For other API errors not caught by isResourceExhaustedError + const errorDetail = apiError.message || 'Unknown error during video generation initiation.' + return { error: `Video initiation failed: ${errorDetail}` } + } + + // c. Fallback for other unexpected response structures from the API call (not an exception) + return { error: 'Video initiation failed: Unknown error structure in response data.' } + } catch (error: any) { + // Log the raw error for debugging + console.error('Video Generation Request Error:', error.response?.data || error.message || error) + const nestedError = error.response?.data?.error + + // Handle specific HTTP status codes that often mean rate limiting or server overload + if ( + error.response?.status === 429 || + error.response?.status === 503 || + isResourceExhaustedError(error) || + isResourceExhaustedError(nestedError) || + isResourceExhaustedError(error.response?.data) || + (error.errors && + Array.isArray(error.errors) && + error.errors.length > 0 && + isResourceExhaustedError(error.errors[0])) + ) + return { error: customRateLimitMessage } + + // Handle HTTP 400 (Bad Request) specifically, if not a resource exhaustion error + if (error.response?.status === 400) + return { error: 'Bad request: There was an issue with the request parameters for video generation.' } + + // Generic error message if none of the above specific conditions were met + return { error: 'An unexpected error occurred while initiating video generation.' } + } +} + +// Polls the status of a long-running video generation operation. +export async function getVideoGenerationStatus( + operationName: string, + appContext: appContextDataI | null, + formData: GenerateVideoFormI, + passedPrompt: string +): Promise { + // 1 - Authenticate to Google Cloud + let client + try { + const auth = new GoogleAuth({ scopes: 'https://www.googleapis.com/auth/cloud-platform' }) + client = await auth.getClient() + } catch (error) { + console.error('Polling Authentication Error:', error) + return { done: true, error: 'Unable to authenticate for polling status.' } + } + + // 2 - Build polling request + // Extract project ID, location, model ID from operationName + // Example operationName: projects/PROJECT_ID/locations/LOCATION_ID/publishers/google/models/MODEL_ID/operations/OPERATION_ID + const parts = operationName.split('/') + if (parts.length < 8) { + console.error(`Invalid operationName format: ${operationName}`) + return { done: true, error: 'Invalid operation name format.' } + } + const projectId = parts[1] + const location = parts[3] + const modelId = parts[7] + + const pollingAPIUrl = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${modelId}:fetchPredictOperation` + + const opts = { + url: pollingAPIUrl, + method: 'POST', + data: { + operationName: operationName, + }, + headers: { + 'Content-Type': 'application/json', + }, + } + + // 3 - Poll for status of video generation operation + try { + const res = await client.request(opts) + const pollingData: PollingResponse = res.data // Assuming PollingResponse matches LRO Get response + + if (!pollingData.done) { + return { done: false, name: operationName } + } else { + if (pollingData.error) { + console.error(`Operation ${operationName} failed:`, pollingData.error) + if ( + pollingData.error.code === 8 && + typeof pollingData.error.message === 'string' && + pollingData.error.message.toLowerCase().includes('resource exhausted') + ) + return { done: true, error: customRateLimitMessage } + + if ( + typeof pollingData.error.message === 'string' && + pollingData.error.message.includes("{ code: 8, message: 'Resource exhausted.' }") + ) + return { done: true, error: customRateLimitMessage } + + return { done: true, error: pollingData.error.message || 'Video generation failed.' } + } else if (pollingData.response && pollingData.response.videos) { + const rawVideoResults = pollingData.response.videos.map((video: any) => ({ + gcsUri: video.gcsUri, + mimeType: video.mimeType, + })) + + const usedRatio = VideoRatioToPixel.find((item) => item.ratio === formData.aspectRatio) + + const enhancedVideoList = await buildVideoListFromURI({ + videosInGCS: rawVideoResults, + aspectRatio: formData.aspectRatio, + duration: parseInt(formData.durationSeconds, 10), + width: usedRatio?.width ?? 1280, + height: usedRatio?.height ?? 720, + usedPrompt: passedPrompt, + userID: appContext?.userID || '', + modelVersion: formData.modelVersion, + mode: 'Generated', + }) + return { done: true, videos: enhancedVideoList } + } else { + console.error(`Operation ${operationName} finished, but response format is unexpected.`, pollingData) + return { done: true, error: 'Operation finished, but the response was not in the expected format.' } + } + } + } catch (error: any) { + if (error.response?.status === 404) { + console.error(`Polling Error 404 for ${operationName}: Operation not found at ${pollingAPIUrl}`) + return { done: true, error: `Operation ${operationName} not found. It might have expired or never existed.` } + } + if (error.response?.status === 429 || error.response?.status === 503) + return { done: true, error: customRateLimitMessage } + + console.error(`Polling Error for ${operationName}:`, error.response?.data || error.message) + let errorMessage = 'An error occurred while polling the video generation status.' + const nestedError = error.response?.data?.error + if (nestedError) { + if ( + nestedError.code === 8 && + typeof nestedError.message === 'string' && + nestedError.message.toLowerCase().includes('resource exhausted') + ) + return { done: true, error: customRateLimitMessage } + + if ( + typeof nestedError.message === 'string' && + nestedError.message.includes("{ code: 8, message: 'Resource exhausted.' }") + ) + return { done: true, error: customRateLimitMessage } + + if (nestedError.message) errorMessage = nestedError.message + } else if (error instanceof Error && error.message) { + // Check error.message directly + errorMessage = error.message + // More robust check for resource exhausted in generic error message + if (errorMessage.toLowerCase().includes('resource exhausted') && errorMessage.includes('code: 8')) { + return { done: true, error: customRateLimitMessage } + } + } + return { done: true, error: errorMessage } + } +} diff --git a/src/app/api/vertex-seg/action.tsx b/src/app/api/vertex-seg/action.tsx new file mode 100644 index 00000000..3e6a53e2 --- /dev/null +++ b/src/app/api/vertex-seg/action.tsx @@ -0,0 +1,97 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use server' + +const { GoogleAuth } = require('google-auth-library') + +export async function segmentImage( + imageBase64: string, + segMode: string, + semanticSelection: string[], + promptSelection: string, + maskImage: string +) { + // 1 - Atempting to authent to Google Cloud & fetch project informations + let client + try { + const auth = new GoogleAuth({ + scopes: 'https://www.googleapis.com/auth/cloud-platform', + }) + client = await auth.getClient() + } catch (error) { + console.error(error) + return { + error: 'Unable to authenticate your account to access model', + } + } + const location = process.env.NEXT_PUBLIC_VERTEX_API_LOCATION + const projectId = process.env.NEXT_PUBLIC_PROJECT_ID + const modelVersion = process.env.NEXT_PUBLIC_SEG_MODEL + const segAPIurl = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${modelVersion}:predict` + + // 2 - Building Imagen request body + const reqData = { + instances: [ + { + image: { + bytesBase64Encoded: imageBase64.startsWith('data:') ? imageBase64.split(',')[1] : imageBase64, + }, + }, + ], + parameters: { + mode: segMode, + maxPredictions: 1, + }, + } + if (segMode === 'prompt') { + ;(reqData.instances[0] as any).prompt = promptSelection + } + if (segMode === 'semantic') { + ;(reqData.instances[0] as any).prompt = semanticSelection.toString().toLocaleLowerCase() + } + if (segMode === 'interactive') { + ;(reqData.instances[0] as any).scribble = {} + ;(reqData.instances[0] as any).scribble.image = { + bytesBase64Encoded: maskImage.startsWith('data:') ? maskImage.split(',')[1] : maskImage, + } + } + + const opts = { + url: segAPIurl, + method: 'POST', + data: reqData, + } + + // 3 - Segment image + try { + const res = await client.request(opts) + + if (res.data.predictions === undefined) { + throw Error('There were an issue, no segmentation were done') + } + + console.log('Image segmented with success') + let segmentation = res.data.predictions[0].bytesBase64Encoded + + if (!segmentation.startsWith('data:')) segmentation = `data:image/png;base64,${segmentation}` + + return segmentation + } catch (error) { + console.error(error) + return { + error: 'Issue while segmenting image.', + } + } +} diff --git a/src/app/context/app-context.tsx b/src/app/context/app-context.tsx new file mode 100644 index 00000000..b3f3e81d --- /dev/null +++ b/src/app/context/app-context.tsx @@ -0,0 +1,168 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use client' + +import { createContext, useState, useEffect, useContext } from 'react' +import { exportStandardFields, ExportMediaFormFieldsI } from '../api/export-utils' +import { fetchJsonFromStorage } from '../api/cloud-storage/action' + +export interface appContextDataI { + gcsURI?: string + userID?: string + exportMetaOptions?: ExportMediaFormFieldsI + isLoading: boolean + imageToEdit?: string + imageToVideo?: string + promptToGenerateImage?: string + promptToGenerateVideo?: string +} + +interface AppContextType { + appContext: appContextDataI | null + setAppContext: React.Dispatch> + error: Error | string | null + setError: React.Dispatch> +} + +export const appContextDataDefault = { + gcsURI: '', + userID: '', + exportMetaOptions: undefined, + isLoading: true, + imageToEdit: '', + imageToVideo: '', + promptToGenerateImage: '', + promptToGenerateVideo: '', +} + +const AppContext = createContext({ + appContext: appContextDataDefault, + setAppContext: () => {}, + error: null, + setError: () => {}, +}) + +export function ContextProvider({ children }: { children: React.ReactNode }) { + const [appContext, setAppContext] = useState(appContextDataDefault) + const [error, setError] = useState(null) + const [retries, setRetries] = useState(0) + + useEffect(() => { + async function fetchAndUpdateContext() { + try { + // 0. Check if required environment variables are available + if ( + !process.env.NEXT_PUBLIC_PROJECT_ID || + !process.env.NEXT_PUBLIC_VERTEX_API_LOCATION || + !process.env.NEXT_PUBLIC_GCS_BUCKET_LOCATION || + !process.env.NEXT_PUBLIC_GEMINI_MODEL || + !process.env.NEXT_PUBLIC_PRINCIPAL_TO_USER_FILTERS || + !process.env.NEXT_PUBLIC_OUTPUT_BUCKET || + !process.env.NEXT_PUBLIC_TEAM_BUCKET || + !process.env.NEXT_PUBLIC_EXPORT_FIELDS_OPTIONS_URI + ) { + throw Error('Missing required environment variables') + } + + if (process.env.NEXT_PUBLIC_EDIT_ENABLED === 'true' && !process.env.NEXT_PUBLIC_SEG_MODEL) { + throw Error('Missing required environment variables for editing') + } + + // 1. Fetch User ID from client-side + let fetchedUserID = '' + + if (process.env.NODE_ENV === 'development' && process.env.NEXT_PUBLIC_TEST_DEV_USER_ID) { + // Locally IAP is not enabled + fetchedUserID = process.env.NEXT_PUBLIC_TEST_DEV_USER_ID + } else { + // Fetching ID via IAP + const response = await fetch('/api/google-auth') + const authParams = await response.json() + if (typeof authParams === 'object' && 'error' in authParams) { + throw Error(authParams.error) + } + + let targetPrincipal: string + + if (authParams !== undefined && authParams['targetPrincipal'] !== undefined) { + targetPrincipal = authParams['targetPrincipal'] + const principalToUserFilters = process.env.NEXT_PUBLIC_PRINCIPAL_TO_USER_FILTERS + ? process.env.NEXT_PUBLIC_PRINCIPAL_TO_USER_FILTERS + : '' + + principalToUserFilters + .split(',') + .forEach((filter) => (targetPrincipal = targetPrincipal.replace(filter, ''))) + } else { + throw Error('An unexpected error occurred while fetching User ID') + } + fetchedUserID = targetPrincipal + } + + // 2. Set GCS URI for all edited/ generated images + let gcsURI = `gs://${process.env.NEXT_PUBLIC_OUTPUT_BUCKET}` + + // 3. Check if export metadata options file exists + let exportMetaOptions: any = {} + const exportMetaOptionsURI = process.env.NEXT_PUBLIC_EXPORT_FIELDS_OPTIONS_URI + try { + exportMetaOptions = await fetchJsonFromStorage(exportMetaOptionsURI) + if (!exportMetaOptions) throw Error('Not found') + } catch (error) { + throw Error('Could not fetch export metadata options') + } + const ExportImageFormFields: ExportMediaFormFieldsI = { ...exportStandardFields, ...exportMetaOptions } + + // 4. Update Context with all fetched data + setAppContext({ + userID: fetchedUserID, + gcsURI: gcsURI?.toString(), + exportMetaOptions: ExportImageFormFields, + isLoading: false, + }) + setRetries(0) + } catch (error: unknown) { + setError('An unexpected error occurred, retrying...') + console.error(error) + + // Maximum 3 retries + if (retries < 3) { + console.error('Retrying fetch in 2 seconds...') + setTimeout(() => { + setRetries(retries + 1) + }, 2000) // Retry after 2 seconds + } else { + setAppContext(appContextDataDefault) + setError('Failed to fetch data after multiple retries') + } + } + } + + fetchAndUpdateContext() + }, [retries]) + + const contextValue = { + appContext, + setAppContext, + error, + setError, + } + + return {children} +} + +export function useAppContext() { + return useContext(AppContext) +} diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 00000000..ec3e0f04 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,24 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +@import '@fontsource/roboto/300.css'; /* Light 300 */ +@import '@fontsource/roboto/400.css'; /* Regular 400 */ +@import '@fontsource/roboto/500.css'; /* Medium 500 */ +@import '@fontsource/roboto/700.css'; /* Bold 700 */ +@import '@fontsource/roboto/900.css'; /* Black 900 */ +@import '@fontsource/roboto/500.css'; /* Medium 500 */ + + diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 00000000..9e14582a --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,46 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react' +import { AppRouterCacheProvider } from '@mui/material-nextjs/v14-appRouter' +import { ThemeProvider } from '@mui/material/styles' +import CssBaseline from '@mui/material/CssBaseline' +import theme from './theme' +import { ContextProvider } from './context/app-context' +import './globals.css' + +export const metadata = { + title: 'ImgStudio', + description: 'Interface to generate & edit images using Google model Imagen', +} + +export default function RootLayout(props: { children: React.ReactNode }) { + return ( + + + + + + + + + + {props.children} + + + + + + ) +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 00000000..1fdae762 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,41 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use client' + +import * as React from 'react' +import Box from '@mui/material/Box' +import Image from 'next/image' +import icon from '../public/ImgStudioLogo.svg' +import GoogleSignInButton from './ui/ux-components/GoogleSignInButton' +import { pages } from './routes' +import { useRouter } from 'next/navigation' + +export default function Page() { + const router = useRouter() + const handleClick = () => { + router.push(pages.Generate.href) + } + + return ( +
+ + ImgStudio + + + + +
+ ) +} diff --git a/src/app/routes.tsx b/src/app/routes.tsx new file mode 100644 index 00000000..c7cdc542 --- /dev/null +++ b/src/app/routes.tsx @@ -0,0 +1,34 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export const pages = { + Generate: { + name: 'Generate', + description: 'Create new content from scratch or with references', + href: '/generate', + status: 'true', + }, + Edit: { + name: 'Edit', + description: 'Import, edit and transform existing content', + href: '/edit', + status: process.env.NEXT_PUBLIC_EDIT_ENABLED, + }, + Library: { + name: 'Browse', + description: "Explore shared creations from your team's Library", + href: '/library', + status: 'true', + }, +} diff --git a/src/app/theme.ts b/src/app/theme.ts new file mode 100644 index 00000000..bee0f339 --- /dev/null +++ b/src/app/theme.ts @@ -0,0 +1,123 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use client' +import { createTheme } from '@mui/material/styles' + +const theme = createTheme({ + palette: { + mode: 'light', + primary: { + main: '#4285F4', + dark: '#1967D2', + light: '#AECBFA', + }, + secondary: { + main: '#5F6368', + dark: '#202124', + light: '#E8EAED', + }, + text: { + primary: '#202124', + secondary: '#3C4043', + disabled: '#5F6368', + }, + background: { + paper: '#202124', + default: '#ffffff', + }, + error: { + main: '#D93025', + dark: '#B31412', + light: '#EE675C', + }, + info: { + main: '#1A73E8', + light: '#669DF6', + dark: '#185ABC', + }, + success: { + main: '#1E8E3E', + light: '#5BB974', + dark: '#137333', + }, + warning: { + main: '#F29900', + light: '#FBBC04', + dark: '#E37400', + }, + }, + typography: { + fontFamily: 'Roboto', + fontSize: 20, + button: { + textTransform: 'none', + }, + h1: { + fontSize: '5rem', + fontWeight: 400, + lineHeight: 0.8, + }, + h2: { + fontSize: '3.5rem', + fontWeight: 400, + lineHeight: 0.8, + }, + h3: { + fontSize: '1.5rem', + fontWeight: 400, + }, + body1: { + fontSize: '1.1rem', + fontWeight: 400, + lineHeight: 1.16, + }, + body2: { + fontSize: '1rem', + fontWeight: 400, + lineHeight: 1.16, + }, + subtitle1: { + fontSize: '0.9rem', + fontWeight: 400, + lineHeight: 1.3, + }, + caption: { + fontSize: '0.6rem', + fontWeight: 500, + lineHeight: 1, + }, + }, + shape: { + borderRadius: 5, + }, + breakpoints: { + values: { + xs: 0, + sm: 600, + md: 900, + lg: 1200, + xl: 1536, + }, + }, + components: { + MuiButtonBase: { + defaultProps: { + disableRipple: true, + }, + }, + }, +}) + +export default theme diff --git a/src/app/ui/edit-components/EditForm.tsx b/src/app/ui/edit-components/EditForm.tsx new file mode 100644 index 00000000..33f8cd1d --- /dev/null +++ b/src/app/ui/edit-components/EditForm.tsx @@ -0,0 +1,350 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use client' + +import * as React from 'react' +import { useEffect, useState } from 'react' +import { useForm, SubmitHandler } from 'react-hook-form' +import { Typography, Button, Box, IconButton, Stack, Alert, Avatar, Icon } from '@mui/material' +import { Send as SendIcon, WatchLater as WatchLaterIcon, Close as CloseIcon, Autorenew } from '@mui/icons-material' + +import { FormInputText } from '../ux-components/InputText' +import FormInputDropdown from '../ux-components/InputDropdown' + +import { ImageI } from '../../api/generate-image-utils' + +import theme from '../../theme' +import { editImage } from '../../api/imagen/action' +import { CustomizedAvatarButton, CustomizedIconButton, CustomizedSendButton } from '../ux-components/Button-SX' +import CustomTooltip from '../ux-components/Tooltip' +import { appContextDataDefault, useAppContext } from '../../context/app-context' +import EditImageDropzone, { getAspectRatio } from './EditImageDropzone' +import { + EditImageFormFields, + EditImageFormI, + editSettingsFields, + formDataEditDefaults, + maskTypes, +} from '../../api/edit-utils' +import FormInputEditSettings from './EditSettings' +import EditModeMenu from './EditModeMenu' +import SetMaskDialog from './SetMaskDialog' +import { downloadMediaFromGcs } from '../../api/cloud-storage/action' +const { palette } = theme + +const editModeField = EditImageFormFields.editMode +const editModeOptions = editModeField.options + +export async function fileToBase64(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.readAsArrayBuffer(file) + reader.onload = () => { + const encoded = Buffer.from(reader.result as ArrayBuffer).toString('base64') + resolve(encoded) + } + reader.onerror = (error) => reject(error) + }) +} + +export default function EditForm({ + isLoading, + onRequestSent, + onImageGeneration, + errorMsg, + onNewErrorMsg, +}: { + isLoading: boolean + onRequestSent: (valid: boolean, count: number) => void + onImageGeneration: (newImages: ImageI[]) => void + errorMsg: string + onNewErrorMsg: (newErrorMsg: string) => void +}) { + const { handleSubmit, watch, control, setValue, getValues, reset } = useForm({ + defaultValues: formDataEditDefaults, + }) + const { appContext } = useAppContext() + const { setAppContext } = useAppContext() + + const [imageToEdit, setImageToEdit] = useState(null) + const [maskImage, setMaskImage] = useState(null) + const [maskPreview, setMaskPreview] = useState(null) + const [outpaintedImage, setOutpaintedImage] = useState(null) + + const [imageWidth, imageHeight, imageRatio] = watch(['width', 'height', 'ratio']) + const [maskSize, setMaskSize] = useState({ width: 0, height: 0 }) + + const [originalImage, setOriginalImage] = useState(null) + const [originalWidth, setOriginalWidth] = useState(null) + const [originalHeight, setOriginalHeight] = useState(null) + + const defaultEditMode = editModeOptions.find((option) => option.value === editModeField.default) + const [selectedEditMode, setSelectedEditMode] = useState(defaultEditMode) + const [openMaskDialog, setOpenMaskDialog] = useState(false) + + const handleNewEditMode = (value: string) => { + resetStates() + setValue('editMode', value) + + const newEditMode = editModeOptions.find((option) => option.value === value) + setSelectedEditMode(newEditMode) + + const defaultMaskDilation = newEditMode?.defaultMaskDilation.toString() + const defaultBaseSteps = newEditMode?.defaultBaseSteps.toString() + defaultMaskDilation && setValue('maskDilation', defaultMaskDilation) + defaultBaseSteps && setValue('baseSteps', defaultBaseSteps) + } + + useEffect(() => { + if (imageWidth !== maskSize.width || imageHeight !== maskSize.height) + setMaskSize({ width: imageWidth, height: imageHeight }) + }, [imageWidth, imageHeight]) + + useEffect(() => { + const fetchAndSetImage = async () => { + handleNewEditMode(defaultEditMode?.value ?? '') + if (appContext && appContext.imageToEdit) { + try { + const { data } = await downloadMediaFromGcs(appContext.imageToEdit) + const newImage = `data:image/png;base64,${data}` + data && setImageToEdit(newImage) + setMaskImage(null) + + // Re-initialize parameter in context + setAppContext((prevContext) => { + if (prevContext) return { ...prevContext, imageToEdit: '' } + else return { ...appContextDataDefault, imageToEdit: '' } + }) + } catch (error) { + console.error('Error fetching image:', error) + } + } + } + + fetchAndSetImage() + }, [appContext?.imageToEdit]) + + const handleMaskDialogOpen = () => { + if (selectedEditMode?.value === 'EDIT_MODE_OUTPAINT') { + if (!outpaintedImage) { + setOriginalImage(getValues('inputImage')) + setOriginalWidth(getValues('width')) + setOriginalHeight(getValues('height')) + } else { + if (originalImage && originalWidth && originalHeight) { + setValue('width', originalWidth) + setValue('height', originalHeight) + setValue('inputImage', originalImage) + } + } + } + + setMaskSize({ width: imageWidth, height: imageHeight }) + + setMaskImage(null) + setMaskPreview(null) + setOpenMaskDialog(true) + } + const handleMaskDialogClose = () => { + setOpenMaskDialog(false) + } + + useEffect(() => { + if (imageToEdit) { + setValue('inputImage', imageToEdit) + } + if (outpaintedImage) setValue('inputImage', outpaintedImage) + if (maskImage) setValue('inputMask', maskImage) + }, [imageToEdit, maskImage, outpaintedImage]) + + const onSubmit: SubmitHandler = async (formData: EditImageFormI) => { + onRequestSent(true, parseInt(formData.sampleCount)) + + try { + if ( + formData['inputImage'] === '' || + (selectedEditMode?.mandatoryPrompt && formData['prompt'] === '') || + (selectedEditMode?.mandatoryMask && formData['inputMask'] === '') + ) + throw Error('Missing either image, prompt or mask') + + const newEditedImage = await editImage(formData, appContext) + + if (newEditedImage !== undefined && typeof newEditedImage === 'object' && 'error' in newEditedImage) { + const errorMsg = newEditedImage['error'].replaceAll('Error: ', '') + throw Error(errorMsg) + } else { + newEditedImage.map((image) => { + if ('warning' in image) onNewErrorMsg(image['warning'] as string) + }) + + onImageGeneration(newEditedImage) + } + } catch (error: any) { + onNewErrorMsg(error.toString()) + } + } + + const onReset = () => { + setImageToEdit(null) + resetStates() + reset() + } + + const resetStates = () => { + setValue('prompt', '') + setMaskImage(null) + setMaskPreview(null) + setOutpaintedImage(null) + setMaskSize({ width: 0, height: 0 }) + onNewErrorMsg('') + } + + return ( + <> +
+ + + + {'Edit with'} + + + + + <> + {errorMsg !== '' && ( + { + onNewErrorMsg('') + }} + sx={{ pt: 0.2 }} + > + + + } + sx={{ height: 'auto', mb: 2, fontSize: 16, fontWeight: 500, pt: 1, color: palette.text.secondary }} + > + {errorMsg} + + )} + + + + + + + + + {selectedEditMode?.promptIndication && ( + + )} + + + + onReset()} + aria-label="Reset form" + disableRipple + sx={{ px: 0.5 }} + > + + + + + + + {selectedEditMode?.mandatoryMask && selectedEditMode?.maskType && ( + + )} + + + + + {selectedEditMode?.maskType && ( + selectedEditMode?.maskType.includes(maskType.value))} + open={openMaskDialog} + selectedEditMode={selectedEditMode} + maskImage={maskImage} + setMaskImage={setMaskImage} + maskPreview={maskPreview} + setMaskPreview={setMaskPreview} + setValue={setValue} + imageToEdit={imageToEdit ?? ''} + imageSize={{ width: originalWidth ?? imageWidth, height: originalHeight ?? imageHeight, ratio: imageRatio }} + maskSize={maskSize} + setMaskSize={setMaskSize} + setOutpaintedImage={setOutpaintedImage} + outpaintedImage={outpaintedImage ?? ''} + /> + )} + + ) +} diff --git a/src/app/ui/edit-components/EditImageDropzone.tsx b/src/app/ui/edit-components/EditImageDropzone.tsx new file mode 100644 index 00000000..7dcb29b1 --- /dev/null +++ b/src/app/ui/edit-components/EditImageDropzone.tsx @@ -0,0 +1,240 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Box, Typography } from '@mui/material' +import React, { useEffect, useMemo, useState } from 'react' +import { useDropzone } from 'react-dropzone' + +import theme from '../../theme' +import { fileToBase64 } from './EditForm' +const { palette } = theme + +export function getAspectRatio(width: number, height: number): string { + const gcd = (a: number, b: number): number => { + return b === 0 ? a : gcd(b, a % b) + } + + const divisor = gcd(width, height) + let widthRatio = width / divisor + let heightRatio = height / divisor + + const tolerance = 0.05 + + const standardRatios = [ + [16, 9], + [9, 16], + [4, 3], + [3, 4], + [1, 1], + [3, 2], + [2, 3], + ] + + for (const [standardWidth, standardHeight] of standardRatios) { + const calculatedRatio = widthRatio / heightRatio + const standardRatio = standardWidth / standardHeight + + if (Math.abs(calculatedRatio - standardRatio) < tolerance) { + return `${standardWidth}:${standardHeight}` + } + } + + // If no standard ratio is found within tolerance, return the simplified ratio + return `${Math.round(widthRatio)}:${Math.round(heightRatio)}` +} + +export function getParentBoxDimensions(selector: string): { maxWidth: number; maxHeight: number } { + const parentBox = document.querySelector(selector) as HTMLDivElement + if (!parentBox) { + console.error('Parent box not found.') + return { maxWidth: 0, maxHeight: 0 } + } + const maxWidth = parentBox.offsetWidth + const maxHeight = parentBox.offsetHeight + return { maxWidth, maxHeight } +} + +export default function EditImageDropzone({ + setImageToEdit, + imageToEdit, + maskImage, + maskPreview, + setValue, + setMaskSize, + setMaskImage, + isOutpaintingMode, + outpaintedImage, + maskSize, + setErrorMsg, +}: { + setImageToEdit: React.Dispatch> + imageToEdit: string | null + maskImage: string | null + maskPreview: string | null + setValue: any + setMaskSize: React.Dispatch> + setMaskImage: React.Dispatch> + isOutpaintingMode: boolean + outpaintedImage: string | null + maskSize: { width: number; height: number } + setErrorMsg: (newErrorMsg: string) => void +}) { + const [canvasSize, setCanvas] = useState({ width: 0, height: 0 }) + + const initiateDimensions = (image: string) => { + const img = new Image() + img.onload = () => { + initializeCanvas(img.width, img.height) + setValue('width', img.width) + setValue('height', img.height) + setValue('ratio', getAspectRatio(img.width, img.height)) + setMaskSize({ width: img.width, height: img.height }) + } + img.src = image + } + + const calculateImageScaleFactor = useMemo( + () => + (originalWidth: number, originalHeight: number, maxWidth: number, maxHeight: number): number => { + const scaleFactorWidth = maxWidth / originalWidth + const scaleFactorHeight = maxHeight / originalHeight + return Math.min(scaleFactorWidth, scaleFactorHeight) + }, + [] + ) + + const initializeCanvas = (width: number, height: number) => { + const { maxWidth, maxHeight } = getParentBoxDimensions('#DropzoneContainer') + const factor = calculateImageScaleFactor(width, height, maxWidth, maxHeight) + setCanvas({ width: width * factor, height: height * factor }) + } + + const resetImagePreview = () => { + setImageToEdit(null) + setMaskImage(null) + setCanvas({ width: 0, height: 0 }) + } + + useEffect(() => { + if (isOutpaintingMode && outpaintedImage) { + initializeCanvas(maskSize.width, maskSize.height) + } else if (imageToEdit) { + initiateDimensions(imageToEdit) + } else if (!imageToEdit && !maskImage) { + resetImagePreview() + setCanvas({ width: 0, height: 0 }) + setMaskSize({ width: 0, height: 0 }) + setValue('width', 0) + setValue('height', 0) + setValue('ratio', '1:1') + } + }, [imageToEdit, maskImage, isOutpaintingMode, outpaintedImage]) + + const onDrop = async (acceptedFiles: File[]) => { + setErrorMsg('') + + const file = acceptedFiles[0] + const allowedTypes = ['image/png', 'image/webp', 'image/jpeg'] + + if (!allowedTypes.includes(file.type)) { + setErrorMsg('Wrong input image format - Only png, jpeg and webp are allowed') + return + } + + const base64 = await fileToBase64(file) + const newImage = `data:${file.type};base64,${base64}` + setImageToEdit(newImage) + initiateDimensions(newImage) + } + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }) + + return ( + <> + + {!imageToEdit && ( + + + {'Drop or select your image'} + + )} + {imageToEdit && ( + + preview image + {(maskImage || maskPreview) && !isOutpaintingMode && ( + mask image + )} + {outpaintedImage && isOutpaintingMode && ( + outpaintedImage image + )} + + )} + + + ) +} diff --git a/src/app/ui/edit-components/EditModeMenu.tsx b/src/app/ui/edit-components/EditModeMenu.tsx new file mode 100644 index 00000000..92e9828f --- /dev/null +++ b/src/app/ui/edit-components/EditModeMenu.tsx @@ -0,0 +1,183 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react' + +import { EditImageFormFields } from '@/app/api/edit-utils' +import { useState } from 'react' +import { + Icon, + List, + ListItemButton, + ListItemText, + ListItem, + ListItemIcon, + Typography, + Menu, + MenuItem, + Box, +} from '@mui/material' + +import theme from '../../theme' +const { palette } = theme + +const CustomizedMenu = { + sx: { + '& .MuiPaper-root': { + background: 'white', + color: palette.text.primary, + boxShadow: 1, + }, + }, +} + +const CustomizedButtonBase = { + p: 0, + width: '45%', + '&:hover': { + bgcolor: palette.secondary.light, + fontWeight: 700, + }, + '& .MuiListItemIcon-root': { + position: 'relative', + left: 7, + bottom: 2, + minWidth: 45, + px: 0, + color: palette.secondary.main, + }, + '& .MuiListitemText-root': { + '& .MuiTypography-root': { + fontWeight: 700, + }, + }, +} + +const CustomizedPrimaryText = { + fontSize: '1.1rem', + fontWeight: 500, + color: palette.primary.main, +} +const CustomizedSecondaryText = { + fontSize: '0.9rem', + fontWeight: 400, + color: palette.secondary.main, +} + +const editModeField = EditImageFormFields.editMode +const editModeOptions = editModeField.options + +export default function EditModeMenu({ + handleNewEditMode, + selectedEditMode, +}: { + handleNewEditMode: any + selectedEditMode: any +}) { + const [anchorEl, setAnchorEl] = useState(null) + + const open = Boolean(anchorEl) + const handleClickListItem = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handleMenuItemClick = (event: React.MouseEvent, value: string) => { + handleNewEditMode(value) + setAnchorEl(null) + } + + const handleClose = () => { + setAnchorEl(null) + } + + return ( + + + {editModeField.label} + + + + + {selectedEditMode?.icon} + + + + + + {editModeOptions.map((option) => ( + handleMenuItemClick(event, option.value)} + sx={{ py: 1 }} + > + + {option.icon} + + + + ))} + + + ) +} diff --git a/src/app/ui/edit-components/EditSettings.tsx b/src/app/ui/edit-components/EditSettings.tsx new file mode 100644 index 00000000..b0d222b5 --- /dev/null +++ b/src/app/ui/edit-components/EditSettings.tsx @@ -0,0 +1,185 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react' +import { IconButton, Typography, Box, Menu, MenuItem, Avatar } from '@mui/material' +import { CustomizedAvatarButton, CustomizedIconButton, CustomizedIconButtonOpen } from '../ux-components/Button-SX' + +import theme from '../../theme' +const { palette } = theme + +import FormInputDropdown from '../ux-components/InputDropdown' +import FormInputChipGroup from '../ux-components/InputChipGroup' +import { EditSettingsFieldsI } from '../../api/edit-utils' +import { FormInputTextSmall } from '../ux-components/InputTextSmall' +import { Settings } from '@mui/icons-material' +import CustomTooltip from '../ux-components/Tooltip' +import { FormInputSlider } from '../ux-components/InputSlider' + +const CustomizedMenu = { + '& .MuiPaper-root': { + background: 'white', + color: palette.text.primary, + boxShadow: 5, + p: 0.5, + width: 250, + '& .MuiMenuItem-root': { + background: 'transparent', + pb: 1, + }, + }, +} + +export default function FormInputEditSettings({ + control, + setValue, + editSettingsFields, +}: { + control: any + setValue: any + editSettingsFields: EditSettingsFieldsI +}) { + const [anchorEl, setAnchorEl] = React.useState(null) + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const open = Boolean(anchorEl) + + const handleClose = () => { + setAnchorEl(null) + } + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {'Negative prompt (content to avoid)'} + + + + + + + + ) +} diff --git a/src/app/ui/edit-components/ManualMaskSelection.tsx b/src/app/ui/edit-components/ManualMaskSelection.tsx new file mode 100644 index 00000000..bddffbad --- /dev/null +++ b/src/app/ui/edit-components/ManualMaskSelection.tsx @@ -0,0 +1,158 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Clear, Draw, DriveFileRenameOutlineOutlined, Redo, Undo } from '@mui/icons-material' +import { Stack, ToggleButtonGroup, ToggleButton, Typography, Divider, Box } from '@mui/material' +import { CustomSlider } from '../ux-components/InputSlider' +import theme from '../../theme' +const { palette } = theme + +const customToggleButtonGroup = { + '& .MuiToggleButton-root': { + border: '1px solid transparent', + backgroundColor: 'transparent', + '&:hover': { + backgroundColor: 'transparent', + }, + '&.Mui-selected': { + backgroundColor: 'transparent', + '&:hover': { + backgroundColor: 'transparent', + }, + }, + }, +} + +const customToggleButton = { + p: 0, + backgroundColor: 'transparent', + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', +} + +const customToggleButtonLabel = { + fontSize: '0.75rem', + fontWeight: 500, + lineHeight: '1.3em', + pr: 1.5, +} + +export default function ManualMaskSelection({ + brushType, + brushSize, + handleChangeBrushType, + handleChangeBrushSize, + handleUndoClick, + handleRedoClick, + handleClearClick, +}: { + brushType: string + brushSize: number + handleChangeBrushType: any + handleChangeBrushSize: any + handleUndoClick: () => void + handleRedoClick: () => void + handleClearClick: () => void +}) { + return ( + + + + + + + {'Brush'} + + + + + + {'Eraser'} + + + + + + + + + {'Undo'} + + + + + + {'Redo'} + + + + + + {'Clear'} + + + + + + + + + ) +} diff --git a/src/app/ui/edit-components/MaskCanvas.tsx b/src/app/ui/edit-components/MaskCanvas.tsx new file mode 100644 index 00000000..8bdffb12 --- /dev/null +++ b/src/app/ui/edit-components/MaskCanvas.tsx @@ -0,0 +1,222 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Box } from '@mui/material' +import React, { useEffect, useState } from 'react' +import { ReactSketchCanvas, ReactSketchCanvasRef } from 'react-sketch-canvas' + +const maxWidth = 350 +const maxHeight = 550 + +const calculateImageScaleFactor = ( + originalWidth: number, + originalHeight: number +): { width: number; height: number } => { + const aspectRatio = originalWidth / originalHeight + + let newWidth = Math.min(originalWidth, maxWidth) + let newHeight = newWidth / aspectRatio + if (newHeight > maxHeight) { + newHeight = maxHeight + newWidth = newHeight * aspectRatio + } + + return { width: newWidth, height: newHeight } +} + +export default function MaskCanvas({ + imageToEdit, + maskSize, + maskImage, + maskPreview, + readOnlyCanvas, + brushSize, + canvasRef, + setIsEmptyCanvas, + isMaskPreview, +}: { + imageToEdit: string | null + maskSize: { width: number; height: number } + maskImage: string | null + maskPreview: string | null + readOnlyCanvas: boolean + brushSize: number + canvasRef: React.RefObject + setIsEmptyCanvas: (value: boolean) => void + isMaskPreview: boolean +}) { + const [imagePreview, setImagePreview] = useState(null) + const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 }) + + useEffect(() => { + setCanvasSize({ width: 0, height: 0 }) + }, []) + + const checkIfCanvasIsEmpty = () => { + if (canvasRef.current) { + canvasRef.current.exportPaths().then((paths) => { + if (paths.length === 0) setIsEmptyCanvas(true) + else setIsEmptyCanvas(false) + }) + } + } + + useEffect(() => { + if (!imageToEdit && !maskImage) { + resetImagePreview() + setCanvasSize({ width: 0, height: 0 }) + checkIfCanvasIsEmpty() + } + }, [imageToEdit, maskImage, maskPreview]) + + useEffect(() => { + checkIfCanvasIsEmpty() + }, [canvasRef]) + + useEffect(() => { + const loadImage = () => { + if (imageToEdit) { + setImagePreview(imageToEdit) + + const img = new Image() + img.onload = () => { + const { width: newWidth, height: newHeight } = calculateImageScaleFactor(img.width, img.height) + setCanvasSize({ width: newWidth, height: newHeight }) + } + img.src = imageToEdit + } + } + + loadImage() + + if (!imageToEdit) { + setCanvasSize({ width: 0, height: 0 }) + setImagePreview(null) + } + }, [imageToEdit, calculateImageScaleFactor]) + + const resetCanvas = () => { + if (canvasRef.current) { + canvasRef.current.resetCanvas() + setIsEmptyCanvas(true) + } + } + + const resetImagePreview = () => { + setImagePreview(null) + resetCanvas() + } + + return ( + + {imagePreview && ( + + {!maskImage && ( + + )} + {maskPreview && !isMaskPreview && ( + console.error('Mask preview loading error:', e)} + alt="mask image" + style={{ position: 'absolute', width: '100%', height: '100%', zIndex: 1 }} + /> + )} + preview image + + )} + + ) +} + +export async function generateMaskFromManualCanvas(canvasMask: string) { + // Create a new canvas with the same dimensions as the mask + const scribbleCanvas = document.createElement('canvas') + const img = new Image() + img.src = canvasMask + + await new Promise((resolve) => (img.onload = resolve)) + + scribbleCanvas.width = img.width + scribbleCanvas.height = img.height + const scribbleCtx = scribbleCanvas.getContext('2d') + if (!scribbleCtx) return + + // Draw the mask onto the canvas + scribbleCtx.drawImage(img, 0, 0) + + // Get the canvas content + const imageData = scribbleCtx.getImageData(0, 0, scribbleCanvas.width, scribbleCanvas.height) + const data = imageData.data + + // Update pixels: transparent becomes black, the rest become white + for (let i = 0; i < data.length; i += 4) { + const a = data[i + 3] + + if (a === 0) { + // Transparent pixel, set to black + data[i] = 0 // R + data[i + 1] = 0 // G + data[i + 2] = 0 // B + data[i + 3] = 255 // A + } else { + // Non-transparent pixel, set to white + data[i] = 255 // R + data[i + 1] = 255 // G + data[i + 2] = 255 // B + data[i + 3] = 255 // A + } + } + + // Draw the modified image data back onto the canvas + scribbleCtx.putImageData(imageData, 0, 0) + + // Convert the canvas content to a data URL + const base64data = scribbleCanvas.toDataURL('image/png') + + return base64data +} diff --git a/src/app/ui/edit-components/OutpaintPreview.tsx b/src/app/ui/edit-components/OutpaintPreview.tsx new file mode 100644 index 00000000..5b08be73 --- /dev/null +++ b/src/app/ui/edit-components/OutpaintPreview.tsx @@ -0,0 +1,287 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use client' + +import { Box } from '@mui/material' +import { Canvas, Rect } from 'fabric' +import React, { useCallback, useEffect, useRef } from 'react' +import theme from '../../theme' + +const { palette } = theme + +const maxWidth = 80 +const maxHeight = 150 + +export function OutpaintPreview({ + maskSize, + imageSize, + outpaintPosition, + outpaintCanvasRef, + setMaskImage, + imageToEdit, + setOutpaintedImage, +}: { + maskSize: { width: number; height: number } + imageSize: { width: number; height: number; ratio: string } + outpaintPosition: { horizontal: string; vertical: string } + outpaintCanvasRef: React.RefObject + setMaskImage: (value: string | null) => void + imageToEdit: string | null + setOutpaintedImage: (value: string | null) => void +}) { + const fabricCanvasRef = useRef(null) + + useEffect(() => { + const canvas = outpaintCanvasRef.current + if (canvas && !fabricCanvasRef.current) + fabricCanvasRef.current = new Canvas(canvas, { devicePixelRatio: window.devicePixelRatio }) + + return () => { + fabricCanvasRef.current?.dispose() + } + }, []) + + const drawMask = useCallback(() => { + const fabricCanvas = fabricCanvasRef.current + if (fabricCanvas) { + // Calculate scaled mask dimensions + const maskAspectRatio = maskSize.width / maskSize.height + let maskWidth = Math.min(maskSize.width, maxWidth) + let maskHeight = maskWidth / maskAspectRatio + + if (maskHeight > maxHeight) { + maskHeight = maxHeight + maskWidth = maskHeight * maskAspectRatio + } + + fabricCanvas.setDimensions({ + width: maskWidth, + height: maskHeight, + }) + + fabricCanvas.clear() + + // Calculate scale factor for inner rectangle + const widthScale = maskWidth / imageSize.width + const heightScale = maskHeight / imageSize.height + + // Use the smaller scale factor if the aspect ratios are the same, + // otherwise use the appropriate scale based on the orientation + let scaleFactor + if (maskAspectRatio === imageSize.width / imageSize.height) { + const downScale = imageSize.width / maskSize.width + scaleFactor = Math.min(widthScale * downScale, heightScale * downScale) + } else { + if (maskAspectRatio > imageSize.width / imageSize.height) scaleFactor = heightScale + else scaleFactor = widthScale + } + + // Calculate inner rectangle dimensions + const innerRectWidth = imageSize.width * scaleFactor + const innerRectHeight = imageSize.height * scaleFactor + + // Calculate inner rectangle position + let innerRectLeft = 0 + if (outpaintPosition.horizontal === 'left') innerRectLeft = 0 + else if (outpaintPosition.horizontal === 'center') innerRectLeft = (maskWidth - innerRectWidth) / 2 + else if (outpaintPosition.horizontal === 'right') innerRectLeft = maskWidth - innerRectWidth + + let innerRectTop = 0 + if (outpaintPosition.vertical === 'top') innerRectTop = 0 + else if (outpaintPosition.vertical === 'center') innerRectTop = (maskHeight - innerRectHeight) / 2 + else if (outpaintPosition.vertical === 'bottom') innerRectTop = maskHeight - innerRectHeight + + // Create a blue rectangle for the mask + const rect = new Rect({ + left: 0, + top: 0, + width: maskWidth, + height: maskHeight, + fill: 'rgba(174, 203, 250, 0.5)', + objectCaching: false, + shadow: null, + }) + fabricCanvas.add(rect) + + // Create a dark grey rectangle + const innerRect = new Rect({ + left: innerRectLeft, + top: innerRectTop, + width: innerRectWidth, + height: innerRectHeight, + fill: 'black', + objectCaching: false, + shadow: null, + }) + fabricCanvas.add(innerRect) + } + }, [maskSize, imageSize, outpaintPosition]) + + useEffect(() => { + const fabricCanvas = fabricCanvasRef.current + if (fabricCanvas) { + drawMask() + + setTimeout(() => { + if (maskSize.width !== imageSize.width || maskSize.height !== imageSize.height) { + generateMaskFromCanvas(maskSize, imageSize, outpaintPosition).then((newMask) => { + newMask && setMaskImage(newMask) + }) + generateOutpaintedImageFromCanvas(maskSize, imageToEdit, outpaintPosition).then((newImage) => { + newImage && setOutpaintedImage(newImage) + }) + } + }, 100) + } + }, [maskSize, outpaintPosition]) + + return ( + + + + ) +} + +async function generateMaskFromCanvas( + maskSize: { width: number; height: number }, + imageSize: { width: number; height: number }, + outpaintPosition: { horizontal: string; vertical: string } +) { + // Create a new canvas with the desired mask size + const maskCanvas = document.createElement('canvas') + maskCanvas.width = maskSize.width + maskCanvas.height = maskSize.height + const maskCtx = maskCanvas.getContext('2d') + + if (!maskCtx) return + + // Fill the canvas with white + maskCtx.fillStyle = 'white' + maskCtx.fillRect(0, 0, maskSize.width, maskSize.height) + + // Calculate position for the black rectangle + let x = 0 + let y = 0 + if (outpaintPosition.horizontal === 'center') { + x = (maskSize.width - imageSize.width) / 2 + } else if (outpaintPosition.horizontal === 'right') { + x = maskSize.width - imageSize.width + } + if (outpaintPosition.vertical === 'center') { + y = (maskSize.height - imageSize.height) / 2 + } else if (outpaintPosition.vertical === 'bottom') { + y = maskSize.height - imageSize.height + } + + // Draw the black rectangle with anti-aliasing disabled + maskCtx.fillStyle = 'black' + maskCtx.fillRect(x, y, imageSize.width, imageSize.height) + + // Get the canvas content + const imageData = maskCtx.getImageData(0, 0, maskSize.width, maskSize.height) + const data = imageData.data + + // Make white pixels transparent + for (let i = 0; i < data.length; i += 4) { + const r = data[i] + const g = data[i + 1] + const b = data[i + 2] + + if (r === 255 && g === 255 && b === 255) { + data[i + 3] = 0 // Set alpha to 0 for transparent + } + } + + // Draw the modified image data back onto the canvas + maskCtx.putImageData(imageData, 0, 0) + + // Convert the canvas content to a data URL + const base64data = maskCanvas.toDataURL('image/png') + + return base64data +} + +async function generateOutpaintedImageFromCanvas( + maskSize: { width: number; height: number }, + imageToEdit: string | null, + outpaintPosition: { horizontal: string; vertical: string } +) { + if (!imageToEdit) return + + // Create a new canvas with the desired mask size + const outpaintCanvas = document.createElement('canvas') + outpaintCanvas.width = maskSize.width + outpaintCanvas.height = maskSize.height + const outpaintCtx = outpaintCanvas.getContext('2d') + if (!outpaintCtx) return + + // Fill the canvas with black + outpaintCtx.fillStyle = 'black' + outpaintCtx.fillRect(0, 0, maskSize.width, maskSize.height) + + // Load the user uploaded image + const img = new Image() + img.src = imageToEdit + + // Ensure the image is loaded before drawing + await new Promise((resolve) => (img.onload = resolve)) + + // Calculate image position based on outpaintPosition + let x = 0 + let y = 0 + + if (outpaintPosition.horizontal === 'center') { + x = (maskSize.width - img.width) / 2 + } else if (outpaintPosition.horizontal === 'right') { + x = maskSize.width - img.width + } + + if (outpaintPosition.vertical === 'center') { + y = (maskSize.height - img.height) / 2 + } else if (outpaintPosition.vertical === 'bottom') { + y = maskSize.height - img.height + } + + // Draw the image on top of the black canvas + outpaintCtx.drawImage(img, x, y) + + // Convert the canvas content to a data URL + const base64data = outpaintCanvas.toDataURL('image/png') + + return base64data +} diff --git a/src/app/ui/edit-components/OutpaintingMaskSettings.tsx b/src/app/ui/edit-components/OutpaintingMaskSettings.tsx new file mode 100644 index 00000000..b0c1f753 --- /dev/null +++ b/src/app/ui/edit-components/OutpaintingMaskSettings.tsx @@ -0,0 +1,378 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + AlignHorizontalCenter, + AlignHorizontalLeft, + AlignHorizontalRight, + AlignVerticalBottom, + AlignVerticalCenter, + AlignVerticalTop, + Autorenew, + Done, +} from '@mui/icons-material' +import { + Stack, + ToggleButtonGroup, + ToggleButton, + Typography, + Divider, + Box, + FormControl, + FormHelperText, + Input, + InputAdornment, + Button, + Avatar, + IconButton, +} from '@mui/material' +import theme from '../../theme' +import { useState } from 'react' +import { RatioToPixel } from '../../api/generate-image-utils' +import { ChipGroup } from '../ux-components/InputChipGroup' +import { OutpaintPreview } from '../edit-components/OutpaintPreview' +import { CustomizedAvatarButton, CustomizedIconButton, CustomizedSendButton } from '../ux-components/Button-SX' +import CustomTooltip from '../ux-components/Tooltip' +const { palette } = theme + +const customToggleButtonGroup = { + '& .MuiToggleButton-root': { + border: '1px solid transparent', + backgroundColor: 'transparent', + '&:hover': { + backgroundColor: 'transparent', + }, + '&.Mui-selected': { + backgroundColor: 'transparent', + '&:hover': { + backgroundColor: 'transparent', + }, + }, + }, +} + +const customToggleButton = { + p: 0, + backgroundColor: 'transparent', + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', +} + +const customToggleButtonLabel = { + fontSize: '0.75rem', + fontWeight: 500, + lineHeight: '1.3em', + pr: 1.5, +} + +export default function OutpaintingMaskSettings({ + alignHorizontal, + alignVertical, + handleHorizontalChange, + handleVerticalChange, + imageSize, + maskSize, + setMaskSize, + outpaintPosition, + outpaintCanvasRef, + setMaskImage, + imageToEdit, + setOutpaintedImage, +}: { + alignHorizontal: string + alignVertical: string + handleHorizontalChange: (event: any, newValue: string) => void + handleVerticalChange: (event: any, newValue: string) => void + imageSize: { width: number; height: number; ratio: string } + maskSize: { width: number; height: number } + setMaskSize: (value: { width: number; height: number }) => void + outpaintPosition: { horizontal: string; vertical: string } + outpaintCanvasRef: React.RefObject + setMaskImage: (value: string | null) => void + imageToEdit: string | null + setOutpaintedImage: (value: string | null) => void +}) { + const [outpaintRatio, setOutpaintRatio] = useState('') + const handleChipClick = ({ clickedValue, currentValue }: { clickedValue: string; currentValue: string }) => { + if (clickedValue !== currentValue) { + setOutpaintRatio(clickedValue) + + // Calculate new dimensions based on the selected ratio + const newRatio = clickedValue.split(':').map(Number) + const newWidth = Math.max(imageSize.width, Math.round(imageSize.height * (newRatio[0] / newRatio[1]))) + const newHeight = Math.max(imageSize.height, Math.round(imageSize.width * (newRatio[1] / newRatio[0]))) + setMaskSize({ width: newWidth, height: newHeight }) + setLocalMaskSize({ width: newWidth, height: newHeight }) + } + } + + const [localMaskSize, setLocalMaskSize] = useState(maskSize) + const isNewValue = localMaskSize.width !== maskSize.width || localMaskSize.height !== maskSize.height + + const handleWidthChange = (event: { target: { value: string } }) => { + setLocalMaskSize({ + // Update local state + ...localMaskSize, + width: parseInt(event.target.value, 10), + }) + } + + const handleHeightChange = (event: { target: { value: string } }) => { + setLocalMaskSize({ + // Update local state + ...localMaskSize, + height: parseInt(event.target.value, 10), + }) + } + + const handleApplyDimensions = () => { + setMaskSize(localMaskSize) // Update the actual maskSize state + } + + return ( + + + {'Manually set new dimensions'} + + + + + {'px'} + + } + inputProps={{ + type: 'number', + }} + sx={{ + '& .MuiInput-input': { + padding: 0, + width: '80%', + color: palette.primary.main, + fontWeight: isNewValue ? 500 : 400, + fontStyle: isNewValue ? 'italic' : 'none', + }, + }} + /> + {'Width'} + + {'/'} + + + {'px'} + + } + inputProps={{ + type: 'number', + }} + sx={{ + '& .MuiInput-input': { + padding: 0, + width: '80%', + color: palette.primary.main, + fontWeight: isNewValue ? 500 : 400, + fontStyle: isNewValue ? 'italic' : 'none', + }, + }} + /> + {'Height'} + + + {isNewValue && ( + + + + + + )} + + + option.ratio)} + value={outpaintRatio} + onChange={handleChipClick} + handleChipClick={handleChipClick} + /> + + + + {'Position the original image'} + + + + + + + {'Left'} + + + + + + {'Center'} + + + + + + {'Right'} + + + + + + + + + {'Bottom'} + + + + + + {'Center'} + + + + + + {'Top'} + + + + + + + {'Output composition preview'} + + + {maskSize.width === imageSize.width && maskSize.height === imageSize.height && ( + + {'Please select new dimensions.'} + + )} + + + ) +} diff --git a/src/app/ui/edit-components/SetMaskDialog.tsx b/src/app/ui/edit-components/SetMaskDialog.tsx new file mode 100644 index 00000000..81cfbbdc --- /dev/null +++ b/src/app/ui/edit-components/SetMaskDialog.tsx @@ -0,0 +1,581 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react' +import { useEffect, useRef, useState } from 'react' + +import { + Dialog, + DialogContent, + IconButton, + Slide, + Box, + Typography, + Stack, + RadioGroup, + Button, + Icon, + Alert, + TextField, + Avatar, +} from '@mui/material' +import { TransitionProps } from '@mui/material/transitions' +import { Autorenew, Close, Done, WatchLater } from '@mui/icons-material' + +import theme from '../../theme' +import MaskCanvas, { generateMaskFromManualCanvas } from './MaskCanvas' +import { CustomRadio } from '../ux-components/InputRadioButton' +import ManualMaskSelection from './ManualMaskSelection' +import { ReactSketchCanvasRef } from 'react-sketch-canvas' +import { CustomizedAvatarButton, CustomizedIconButton, CustomizedSendButton } from '../ux-components/Button-SX' +import FormInputDropdownMultiple from '../ux-components/InputDropdownMultiple' +import OutpaintingMaskSettings from './OutpaintingMaskSettings' +import { segmentImage } from '../../api/vertex-seg/action' +import { getAspectRatio } from './EditImageDropzone' +const { palette } = theme + +const Transition = React.forwardRef(function Transition( + props: TransitionProps & { + children: React.ReactElement + }, + ref: React.Ref +) { + return +}) + +async function addBorderToBase64Image(base64Image: string): Promise { + return new Promise((resolve) => { + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + const img = new Image() + img.src = base64Image + + if (!ctx) { + console.error('Could not get 2D context for canvas.') + resolve('') // Or handle the error as needed + return + } + + img.onload = () => { + canvas.width = img.width + canvas.height = img.height + ctx.drawImage(img, 0, 0) + ctx.strokeStyle = 'black' + ctx.lineWidth = 1 + ctx.strokeRect(1, 1, canvas.width - 2, canvas.height - 2) + + const modifiedBase64Image = canvas.toDataURL() + resolve(modifiedBase64Image) + } + + // Handle potential error in image loading + img.onerror = () => { + console.error('Error loading image.') + resolve('') // Or handle the error as needed + } + }) +} + +export default function SetMaskDialog({ + open, + handleMaskDialogClose, + availableMaskTypes, + selectedEditMode, + maskImage, + maskPreview, + setMaskImage, + setMaskPreview, + setValue, + imageToEdit, + imageSize, + maskSize, + setMaskSize, + setOutpaintedImage, + outpaintedImage, +}: { + open: boolean + handleMaskDialogClose: () => void + availableMaskTypes: any + selectedEditMode: any + maskImage: string | null + maskPreview: string | null + setMaskPreview: React.Dispatch> + setMaskImage: React.Dispatch> + setValue: any + imageToEdit: string | null + imageSize: { width: number; height: number; ratio: string } + maskSize: { width: number; height: number } + setMaskSize: any + setOutpaintedImage: any + outpaintedImage: string +}) { + const [maskType, setMaskType] = useState(availableMaskTypes[0].value) + const [maskOptions, setMaskOptions] = useState( + availableMaskTypes.find((mask: { value: any }) => mask.value === availableMaskTypes[0].value) + ) + const [semanticSelection, setSemanticSelection] = useState([]) + const [promptSelection, setPromptSelection] = useState('') + + const [isEmptyCanvas, setIsEmptyCanvas] = useState(true) + const [brushType, setBrushType] = useState('Brush') + const [brushSize, setBrushSize] = useState(15) + + const [outpaintPosition, setOutpaintPosition] = useState({ horizontal: 'center', vertical: 'center' }) + const [segErrorMsg, setSegErrorMsg] = useState('') + const [segIsLoading, setSegIsLoading] = useState(false) + + const canvasRef = useRef(null) + const outpaintCanvasRef = useRef(null) + + useEffect(() => { + if (!open) { + setMaskType(availableMaskTypes[0].value) + setMaskOptions(availableMaskTypes.find((mask: { value: string }) => mask.value === availableMaskTypes[0].value)) + setBrushType('Brush') + setBrushSize(15) + } + }, [open, availableMaskTypes]) + + const handleChangeMaskType = (event: { target: { value: string } }) => { + setMaskType(event.target.value) + setMaskOptions(availableMaskTypes.find((mask: { value: string }) => mask.value === event.target.value)) + resetSelection() + } + + const onReset = () => { + resetSelection() + } + + const resetSelection = () => { + setSemanticSelection([]) + setPromptSelection('') + setMaskImage(null) + setMaskPreview(null) + setIsEmptyCanvas(true) + } + + const handleChangeBrushType = (event: React.MouseEvent, newBrushType: string) => { + if (newBrushType !== null) { + setBrushType(newBrushType) + if (newBrushType === 'Eraser') canvasRef.current?.eraseMode(true) + else canvasRef.current?.eraseMode(false) + } + } + const handleUndoClick = () => canvasRef.current?.undo() + const handleRedoClick = async () => { + canvasRef.current?.redo() + const paths = await canvasRef.current?.exportPaths() + if (paths && paths.length === 0) setIsEmptyCanvas(true) + } + const handleClearClick = () => { + canvasRef.current?.clearCanvas() + setIsEmptyCanvas(true) + } + + const handleSelect = (event: { target: { value: any } }) => { + const { + target: { value }, + } = event + setSemanticSelection(typeof value === 'string' ? value.split(',') : value) + } + + const requestSent = (valid: boolean) => { + setSegIsLoading(valid) + if (valid && segErrorMsg !== '') setSegErrorMsg('') + } + + const onSegmentation = async () => { + requestSent(true) + + try { + let manualMask = null + if (maskType === 'interactive') { + manualMask = await generateManualMask() + if (!manualMask) throw Error('No scribble provided') + } + + const res = await segmentImage(imageToEdit ?? '', maskType, semanticSelection, promptSelection, manualMask ?? '') + + if (res !== undefined && typeof res === 'object' && 'error' in res) { + const msg = res['error'] as string + const errorMsg = msg.replaceAll('Error: ', '') + throw Error(errorMsg) + } + + setMaskImage(res) + + const newMaskPreview = await createMaskPreview(res, maskSize) + setMaskPreview(newMaskPreview) + } catch (error: any) { + setSegErrorMsg(error.toString()) + } finally { + requestSent(false) + } + } + + const generateManualMask = async () => { + if (!canvasRef.current) return null + + const paths = await canvasRef.current.exportPaths() + + if (paths.length) { + const dataURL = await canvasRef.current.exportImage('png') + const img = new Image() + img.src = dataURL + + await new Promise((resolve) => (img.onload = resolve)) + + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + if (!ctx) return null + + const newWidth = imageSize.width + const newHeight = imageSize.height + + canvas.width = newWidth + canvas.height = newHeight + ctx.drawImage(img, 0, 0, newWidth, newHeight) + + return new Promise((resolve) => { + canvas.toBlob(async (blob) => { + if (blob) { + const reader = new FileReader() + reader.onloadend = async () => { + const base64data = reader.result as string + setMaskPreview(base64data) + + const manualMask = await generateMaskFromManualCanvas(base64data) + if (manualMask) { + setMaskImage(manualMask) + resolve(manualMask) + } else resolve(null) + } + reader.readAsDataURL(blob) + } else resolve(null) + }, 'image/png') + }) + } else return null + } + + const onValidate = async () => { + if (maskType === 'manual') await generateManualMask() + if (selectedEditMode.value === 'EDIT_MODE_OUTPAINT') { + const img = new Image() + img.onload = () => { + setValue('width', img.width) + setValue('height', img.height) + setValue('ratio', getAspectRatio(img.width, img.height)) + } + img.src = outpaintedImage + } + setPromptSelection('') + setSemanticSelection([]) + handleMaskDialogClose() + } + + return ( + { + if (segIsLoading) handleMaskDialogClose + }} + disableEscapeKeyDown={true} + aria-describedby="parameter the export of an image" + TransitionComponent={Transition} + PaperProps={{ + sx: { + display: 'flex', + justifyContent: 'left', + alignItems: 'left', + p: 1, + cursor: 'pointer', + height: 'auto', + minHeight: '75%', + width: 'auto', + maxWidth: '70%', + borderRadius: 1, + background: 'white', + }, + }} + > + + + + + <> + {segErrorMsg !== '' && ( + {}} sx={{ pt: 0.2 }}> + + + } + sx={{ + width: '95%', + height: 'auto', + mb: 2, + fontSize: 16, + fontWeight: 500, + pt: 1, + color: palette.text.secondary, + }} + > + {segErrorMsg} + + )} + + + + + {selectedEditMode.maskDialogTitle} + + + {selectedEditMode.maskDialogIndication} + + + {availableMaskTypes.map((mask: { label: string; description: string; value: string }) => ( + + + {maskOptions?.requires === 'manualSelection' && maskType === mask.value && !segIsLoading && ( + setBrushSize(newValue)} + handleUndoClick={handleUndoClick} + handleRedoClick={handleRedoClick} + handleClearClick={handleClearClick} + /> + )} + {maskOptions?.requires === 'semanticDropdown' && maskType === mask.value && !segIsLoading && ( + + setSemanticSelection([])} + /> + + )} + {maskOptions?.requires === 'promptInput' && maskType === mask.value && !segIsLoading && ( + + setPromptSelection(event.target.value)} + value={promptSelection} + fullWidth + required + multiline + rows={2} + sx={{ pl: 3.5, pt: 1, '& .MuiInputBase-root': { p: 1, fontSize: '0.9rem' } }} + /> + + )} + {maskType !== 'manual' && maskType !== 'outpaint' && maskType === mask.value && ( + + + onReset()} + aria-label="Reset selection" + disableRipple + sx={{ px: 0.5 }} + > + + + + + + {'(using Vertex Segmentation model)'} + + + )} + {maskType === 'outpaint' && maskType === mask.value && ( + <> + + setOutpaintPosition({ + ...outpaintPosition, + horizontal: newValue, + }) + } + handleVerticalChange={(event: any, newValue: string) => + setOutpaintPosition({ + ...outpaintPosition, + vertical: newValue, + }) + } + imageSize={imageSize} + maskSize={maskSize} + setMaskSize={setMaskSize} + outpaintPosition={outpaintPosition} + outpaintCanvasRef={outpaintCanvasRef} + setMaskImage={setMaskImage} + imageToEdit={imageToEdit} + setOutpaintedImage={setOutpaintedImage} + /> + + )} + + ))} + + + + + + + + ) +} + +export async function createMaskPreview( + base64Image: string, + maskSize: { width: number; height: number } +): Promise { + return new Promise((resolve) => { + // 1. Decode the Base64 image data (and get the MIME type) + const [header, imageDataUri] = base64Image.split(',') + const mimeType = header.match(/:(.*?);/)![1] + + // 2. Create a canvas element with the specified maskSize + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + + if (!ctx) { + console.error('Could not get 2D context for canvas') + resolve('') // Resolve with an empty string in case of error + return + } + + canvas.width = maskSize.width + canvas.height = maskSize.height + + // 3. Create an Image element and set its src + const imgElement = new Image() + + // 4. Use onload to ensure the image is loaded before drawing + imgElement.onload = () => { + ctx.drawImage(imgElement, 0, 0) + + // 5. Get the image data + const imgData = ctx.getImageData(0, 0, maskSize.width, maskSize.height) + const pixels = imgData.data + + // 6. Iterate over the pixels and apply the transformation + for (let i = 0; i < pixels.length; i += 4) { + const r = pixels[i] + const g = pixels[i + 1] + const b = pixels[i + 2] + const a = pixels[i + 3] + + if (r === 0 && g === 0 && b === 0 && a === 255) { + pixels[i + 3] = 128 + } else if (r === 255 && g === 255 && b === 255) { + pixels[i] = 174 + pixels[i + 1] = 203 + pixels[i + 2] = 250 + pixels[i + 3] = 128 + } + } + + // 7. Put the modified pixel data back onto the canvas + ctx.putImageData(imgData, 0, 0) + + // 8. Get the new Base64 encoded image + const newBase64Image = canvas.toDataURL(mimeType) + + resolve(newBase64Image) // Resolve with the new Base64 image + } + + imgElement.src = base64Image + }) +} diff --git a/src/app/ui/generate-components/GenerateForm.tsx b/src/app/ui/generate-components/GenerateForm.tsx new file mode 100644 index 00000000..d7b6ba78 --- /dev/null +++ b/src/app/ui/generate-components/GenerateForm.tsx @@ -0,0 +1,738 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use client' + +import * as React from 'react' +import { useEffect, useState } from 'react' + +import { Control, SubmitHandler, useForm, useWatch } from 'react-hook-form' + +import { + Accordion, + AccordionDetails, + AccordionSummary, + Alert, + Avatar, + Box, + Button, + IconButton, + Stack, + Typography, +} from '@mui/material' +import { + ArrowDownward as ArrowDownwardIcon, + ArrowLeft, + ArrowRight, + Autorenew, + Close as CloseIcon, + Lightbulb, + Mms, + Send as SendIcon, + WatchLater as WatchLaterIcon, +} from '@mui/icons-material' + +import { CustomizedAccordion, CustomizedAccordionSummary } from '../ux-components/Accordion-SX' +import { CustomizedAvatarButton, CustomizedIconButton, CustomizedSendButton } from '../ux-components/Button-SX' +import FormInputChipGroup from '../ux-components/InputChipGroup' +import FormInputDropdown from '../ux-components/InputDropdown' +import { FormInputText } from '../ux-components/InputText' +import { GeminiSwitch } from '../ux-components/GeminiButton' +import CustomTooltip from '../ux-components/Tooltip' + +import GenerateSettings from './GenerateSettings' +import ImageToPromptModal from './ImageToPromptModal' +import { ReferenceBox } from './ReferenceBox' + +import theme from '../../theme' +const { palette } = theme + +import { useAppContext } from '../../context/app-context' +import { generateImage } from '../../api/imagen/action' +import { + chipGroupFieldsI, + GenerateImageFormFields, + GenerateImageFormI, + ImageGenerationFieldsI, + ImageI, + maxReferences, + ReferenceObjectDefaults, + ReferenceObjectInit, + selectFieldsI, +} from '../../api/generate-image-utils' +import { EditImageFormFields } from '@/app/api/edit-utils' +import { + GenerateVideoFormFields, + GenerateVideoFormI, + InterpolImageI, + OperationMetadataI, + tempVeo3specificSettings, + VideoGenerationFieldsI, + videoGenerationUtils, +} from '@/app/api/generate-video-utils' +import { generateVideo } from '@/app/api/veo/action' +import { getOrientation, VideoInterpolBox } from './VideoInterpolBox' +import { AudioSwitch } from '../ux-components/AudioButton' + +export default function GenerateForm({ + generationType, + isLoading, + onRequestSent, + errorMsg, + onNewErrorMsg, + generationFields, + randomPrompts, + onImageGeneration, + onVideoPollingStart, + initialPrompt, + initialITVimage, + promptIndication, +}: { + generationType: 'Image' | 'Video' + isLoading: boolean + onRequestSent: (loading: boolean, count: number) => void + errorMsg: string + onNewErrorMsg: (newErrorMsg: string) => void + generationFields: ImageGenerationFieldsI | VideoGenerationFieldsI + randomPrompts: string[] + onImageGeneration?: (newImages: ImageI[]) => void + onVideoPollingStart?: (operationName: string, metadata: OperationMetadataI) => void + initialPrompt?: string + initialITVimage?: InterpolImageI + promptIndication?: string +}) { + const { handleSubmit, resetField, control, setValue, getValues, watch } = useForm< + GenerateVideoFormI | GenerateImageFormI + >({ + defaultValues: generationFields.defaultValues, + }) + const { appContext } = useAppContext() + + // Manage accordions + const [expanded, setExpanded] = React.useState('attributes') + const handleChange = (panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => { + setExpanded(isExpanded ? panel : false) + } + useEffect(() => { + if (generationType === 'Video') { + if (initialITVimage && initialITVimage.base64Image !== '') setExpanded('interpolation') + else setExpanded('attributes') + } else if (generationType === 'Image') setExpanded('attributes') + }, [initialITVimage, generationType]) + + // Manage if prompt should be generated with Gemini + const [isGeminiRewrite, setIsGeminiRewrite] = useState(true) + const handleGeminiRewrite = (event: React.ChangeEvent) => { + setIsGeminiRewrite(event.target.checked) + } + + // Veo 3: manage audio in output + const isVideoWithAudio = watch('isVideoWithAudio') + const handleVideoAudioCheck = (event: React.ChangeEvent) => { + setValue('isVideoWithAudio', event.target.checked) + } + + // Imagen reference management logic + const referenceObjects = watch('referenceObjects') + const [hasReferences, setHasReferences] = useState(false) + const [modelOptionField, setModelOptionField] = useState(GenerateImageFormFields.modelVersion) + useEffect(() => { + if (generationType === 'Image') { + if (referenceObjects.some((obj) => obj.base64Image !== '')) { + setHasReferences(true) + setModelOptionField(EditImageFormFields.modelVersion) + setValue('modelVersion', EditImageFormFields.modelVersion.default) + } else { + setHasReferences(false) + setModelOptionField(GenerateImageFormFields.modelVersion) + setValue('modelVersion', GenerateImageFormFields.modelVersion.default) + } + } + if (generationType === 'Video') { + setModelOptionField(GenerateVideoFormFields.modelVersion) + setValue('modelVersion', GenerateVideoFormFields.modelVersion.default) + } + }, [JSON.stringify(referenceObjects), generationType]) + const removeReferenceObject = (objectKey: string) => { + // Find the reference object to be removed + const removeReference = referenceObjects.find((obj) => obj.objectKey === objectKey) + if (!removeReference) return + + let updatedReferenceObjects = [...referenceObjects] + + // If reference is an AdditionalImage, remove only it, + // otherwise remove all references with the same ID and update the refId of the remaining ones + if (removeReference.isAdditionalImage) { + updatedReferenceObjects = referenceObjects.filter((obj) => obj.objectKey !== objectKey) + } else { + updatedReferenceObjects = referenceObjects.filter((obj) => obj.refId !== removeReference.refId) + // Update refId of remaining objects + updatedReferenceObjects = updatedReferenceObjects.map((obj) => { + if (obj.refId > removeReference.refId) return { ...obj, refId: obj.refId - 1 } + return obj + }) + } + + if (updatedReferenceObjects.length === 0) setValue('referenceObjects', ReferenceObjectInit) + else setValue('referenceObjects', updatedReferenceObjects) + } + + const addNewRefObject = () => { + if (referenceObjects.length >= maxReferences) return + + let highestId = referenceObjects[0].refId + for (let i = 1; i < referenceObjects.length; i++) + if (referenceObjects[i].refId > highestId) highestId = referenceObjects[i].refId + + const updatedReferenceObjects = [ + ...referenceObjects, + { + ...ReferenceObjectDefaults, + isAdditionalImage: false, + objectKey: Math.random().toString(36).substring(2, 15), + refId: highestId + 1, + }, + ] + + setValue('referenceObjects', updatedReferenceObjects) + } + + const addAdditionalRefObject = (objectKey: string) => { + if (referenceObjects.length >= maxReferences) return + + const associatedObjectIndex = referenceObjects.findIndex((obj) => obj.objectKey === objectKey) + const associatedObject = referenceObjects.find((obj) => obj.objectKey === objectKey) + if (!associatedObject) return + + // Use slice to place the Additional Ref object after its parent ref + const updatedReferenceObjects = [ + ...referenceObjects.slice(0, associatedObjectIndex + 1), + { + ...associatedObject, + isAdditionalImage: true, + base64Image: '', + objectKey: Math.random().toString(36).substring(2, 15), + }, + ...referenceObjects.slice(associatedObjectIndex + 1), + ] + + setValue('referenceObjects', updatedReferenceObjects) + } + + // Veo interpolation image management logic + const interpolImageFirst = watch('interpolImageFirst') + const interpolImageLast = watch('interpolImageLast') + const optionalVeoPrompt = // Prompt optional if either only first frame, OR if both first and last frame + (interpolImageFirst && interpolImageFirst.base64Image !== '') || + (interpolImageFirst && + interpolImageFirst.base64Image !== '' && + interpolImageLast && + interpolImageLast.base64Image !== '') + let orientation = 'horizontal' + orientation = + interpolImageFirst && interpolImageFirst.base64Image !== '' + ? getOrientation(interpolImageFirst.ratio) + : interpolImageLast && interpolImageLast.base64Image !== '' + ? getOrientation(interpolImageLast.ratio) + : '' + useEffect(() => { + if (orientation === 'horizontal') setValue('aspectRatio', '16:9') + else if (orientation === 'vertical') setValue('aspectRatio', '9:16') + }, [orientation]) + + //TODO temp - remove when Veo 3 is fully released + const currentModel = watch('modelVersion') + + //TODO temp - remove when models are GA + // Transforms a "Publisher Model not found" error message into a user-friendly message. + interface ModelOption { + value: string + label: string + indication?: string + type?: string + } + function manageModelNotFoundError(errorMessage: string, modelOptions: ModelOption[]): string { + const modelNotFoundRegex = + /Publisher Model `projects\/[^/]+\/locations\/[^/]+\/publishers\/google\/models\/([^`]+)` not found\./ + const match = errorMessage.match(modelNotFoundRegex) + + if (match && match[1]) { + const modelValue = match[1] + const correspondingModel = modelOptions.find((model) => model.value === modelValue) + + const modelLabel = correspondingModel ? correspondingModel.label : modelValue + + return `You don't have access to the model '${modelLabel}', please select another one in the top dropdown menu for now, and reach out to your IT Admin to request access to '${modelLabel}'.` + } + + return errorMessage + } + + // Image to prompt generator logic + const [imageToPromptOpen, setImageToPromptOpen] = useState(false) + + // Provide random prompt + const getRandomPrompt = () => { + return randomPrompts[Math.floor(Math.random() * randomPrompts.length)] + } + + // Handle 'Replay prompt' from Library + useEffect(() => { + if (initialPrompt) setValue('prompt', initialPrompt) + }, [initialPrompt, setValue]) + + // Handle Image to video from generated or edited image + useEffect(() => { + if (initialITVimage) setValue('interpolImageFirst', initialITVimage) + }, [initialITVimage, setValue]) + + // Update Secondary style dropdown depending on picked primary style + const subImgStyleField = (control: Control) => { + const currentPrimaryStyle: string = useWatch({ control, name: 'style' }) + + var currentAssociatedSubId = generationFields.styleOptions.defaultSub + if (currentPrimaryStyle !== '') { + currentAssociatedSubId = generationFields.styleOptions.options.filter( + (option) => option.value === currentPrimaryStyle + )[0].subID + } + + const subImgStyleField = generationFields.subStyleOptions.options.filter( + (option) => option.subID === currentAssociatedSubId + )[0] + + const currentSecondaryStyle: string = getValues('secondary_style') + useEffect(() => { + if (!subImgStyleField.options.includes(currentSecondaryStyle)) { + setValue('secondary_style', '') + } + }, [currentSecondaryStyle, subImgStyleField.options]) + + return subImgStyleField + } + + // Does not reset settings - only prompt, prompt parameters, image references and negative prompt + const onReset = () => { + generationFields.resetableFields.forEach((field) => + resetField(field as keyof GenerateImageFormI | keyof GenerateVideoFormI) + ) + + if (generationType === 'Video') { + setValue('interpolImageFirst', generationFields.defaultValues.interpolImageFirst) + setValue('interpolImageLast', generationFields.defaultValues.interpolImageLast) + } + + onNewErrorMsg('') + } + + const onImageSubmit: SubmitHandler = async (formData) => { + onRequestSent(true, parseInt(formData.sampleCount)) + + try { + const areAllRefValid = formData['referenceObjects'].every( + (reference) => + reference.base64Image !== '' && + reference.description !== '' && + reference.refId !== null && + reference.referenceType !== '' + ) + if (hasReferences && !areAllRefValid) + throw Error('Incomplete reference(s) information provided, either image type or description missing.') + + if (hasReferences && areAllRefValid) setIsGeminiRewrite(false) + + const newGeneratedImages = await generateImage(formData, areAllRefValid, isGeminiRewrite, appContext) + + if (newGeneratedImages !== undefined && typeof newGeneratedImages === 'object' && 'error' in newGeneratedImages) { + let errorMsg = newGeneratedImages['error'].replaceAll('Error: ', '') + + errorMsg = manageModelNotFoundError(errorMsg, generationFields.model.options as ModelOption[]) + + throw Error(errorMsg) + } else { + newGeneratedImages.map((image) => { + if ('warning' in image) onNewErrorMsg(image['warning'] as string) + }) + + onImageGeneration && onImageGeneration(newGeneratedImages) + } + } catch (error: any) { + onNewErrorMsg(error.toString()) + } + } + + const onVideoSubmit: SubmitHandler = async (formData) => { + onRequestSent(true, parseInt(formData.sampleCount)) + + try { + if (formData.interpolImageLast && formData.interpolImageLast.base64Image !== '' && formData.cameraPreset !== '') + throw Error( + `You can't have both a last frame and a camera preset selected. Please leverage only one of the two feature at once.` + ) + + if (formData.prompt === '') setIsGeminiRewrite(false) + const result = await generateVideo(formData, isGeminiRewrite, appContext) + + if ('error' in result) { + let errorMsg = result.error.replace('Error: ', '') + errorMsg = manageModelNotFoundError(errorMsg, generationFields.model.options as ModelOption[]) + + throw new Error(errorMsg) + } else if ('operationName' in result && 'prompt' in result) + onVideoPollingStart && onVideoPollingStart(result.operationName, { formData: formData, prompt: result.prompt }) + else throw new Error('Failed to initiate video generation: Invalid response from server.') + } catch (error: any) { + onNewErrorMsg(error.toString().replace('Error: ', '')) + } + } + + // Single handler for submitting generation + const onSubmit: SubmitHandler = async (formData) => { + if (generationType === 'Image') await onImageSubmit(formData as GenerateImageFormI) + else if (generationType === 'Video') await onVideoSubmit(formData as GenerateVideoFormI) + } + + return ( + <> +
+ + + + + {'Generate with'} + + + + + <> + {errorMsg !== '' && ( + { + onRequestSent(false, 0) + }} + sx={{ pt: 0.2 }} + > + + + } + sx={{ height: 'auto', mb: 2, fontSize: 16, fontWeight: 500, pt: 1, color: palette.text.secondary }} + > + {errorMsg} + + )} + + + + + + + setImageToPromptOpen(true)} + aria-label="Prompt Generator" + disableRipple + sx={{ px: 0.5 }} + > + + + + + + + setValue('prompt', getRandomPrompt())} + aria-label="Random prompt" + disableRipple + sx={{ px: 0.5 }} + > + + + + + + + onReset()} + aria-label="Reset form" + disableRipple + sx={{ px: 0.5 }} + > + + + + + + + {currentModel === 'veo-3.0-generate-preview' && ( + + + + )} + + + + + + {generationType === 'Image' && process.env.NEXT_PUBLIC_EDIT_ENABLED === 'true' && ( + + } + aria-controls="panel1-content" + id="panel1-header" + sx={CustomizedAccordionSummary} + > + + {'Subject & Style reference(s)'} + + + + + {referenceObjects.map((referenceObject, index) => { + return ( + + ) + })} + + {referenceObjects.length < maxReferences && ( + + + + )} + + + )} + {generationType === 'Video' && + process.env.NEXT_PUBLIC_VEO_ENABLED === 'true' && + process.env.NEXT_PUBLIC_VEO_ITV_ENABLED === 'true' && ( + + } + aria-controls="panel1-content" + id="panel1-header" + sx={CustomizedAccordionSummary} + > + + {`Image(s) to video ${ + process.env.NEXT_PUBLIC_VEO_ADVANCED_ENABLED === 'true' ? ' & Camera presets' : '' + }`} + + + + + + {process.env.NEXT_PUBLIC_VEO_ADVANCED_ENABLED === 'true' && ( + <> + + + + )} + + {process.env.NEXT_PUBLIC_VEO_ADVANCED_ENABLED === 'true' && ( + + + + )} + + + )} + + } + aria-controls="panel1-content" + id="panel1-header" + sx={CustomizedAccordionSummary} + > + + {generationType + ' / prompt attributes'} + + + + + + + + + {Object.entries(generationFields.compositionOptions).map(function ([param, field]) { + return ( + + + + ) + })} + + + + +
+ + setValue('prompt', string)} + setImageToPromptOpen={setImageToPromptOpen} + target={generationType} + /> + + ) +} diff --git a/src/app/ui/generate-components/GenerateSettings.tsx b/src/app/ui/generate-components/GenerateSettings.tsx new file mode 100644 index 00000000..6de6d886 --- /dev/null +++ b/src/app/ui/generate-components/GenerateSettings.tsx @@ -0,0 +1,166 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react' +import { IconButton, Typography, Box, Menu, MenuItem, Avatar } from '@mui/material' +import { CustomizedAvatarButton, CustomizedIconButton, CustomizedIconButtonOpen } from '../ux-components/Button-SX' + +import theme from '../../theme' +const { palette } = theme + +import FormInputDropdown from '../ux-components/InputDropdown' +import FormInputChipGroup from '../ux-components/InputChipGroup' +import { GenerateSettingsI } from '../ux-components/InputInterface' +import { FormInputTextSmall } from '../ux-components/InputTextSmall' +import { Settings } from '@mui/icons-material' +import CustomTooltip from '../ux-components/Tooltip' + +const CustomizedMenu = { + '& .MuiPaper-root': { + background: 'white', + color: palette.text.primary, + boxShadow: 5, + p: 0.5, + width: 250, + '& .MuiMenuItem-root': { + background: 'transparent', + pb: 1, + }, + }, +} + +export default function GenerateSettings({ + control, + setValue, + generalSettingsFields, + advancedSettingsFields, + warningMessage, +}: GenerateSettingsI) { + const [anchorEl, setAnchorEl] = React.useState(null) + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const open = Boolean(anchorEl) + + const handleClose = () => { + setAnchorEl(null) + } + + return ( + <> + + + + + + + + + {warningMessage !== '' && ( + + {warningMessage} + + )} + {Object.entries(generalSettingsFields).map(function ([param, field]) { + return ( + + + + ) + })} + + {Object.entries(advancedSettingsFields).map(function ([param, field]) { + return ( + + + + ) + })} + + + + {'Negative prompt (content to avoid)'} + + + + + + + + ) +} diff --git a/src/app/ui/generate-components/ImageDropzone.tsx b/src/app/ui/generate-components/ImageDropzone.tsx new file mode 100644 index 00000000..69f9b2f0 --- /dev/null +++ b/src/app/ui/generate-components/ImageDropzone.tsx @@ -0,0 +1,197 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Box, IconButton } from '@mui/material' +import React from 'react' +import { useDropzone } from 'react-dropzone' +import Image from 'next/image' + +import theme from '../../theme' +import { fileToBase64 } from '../edit-components/EditForm' +import { Add, AddPhotoAlternate, ControlPointDuplicate } from '@mui/icons-material' +import { getAspectRatio } from '../edit-components/EditImageDropzone' +const { palette } = theme + +export default function ImageDropzone({ + setImage, + image, + onNewErrorMsg, + size, + maxSize, + object, + setValue, + addAdditionalRefObject, + isNewImagePossible, +}: { + setImage: (base64Image: string) => void + image: string | null + onNewErrorMsg: (msg: string) => void + size: { + width: string + height: string + } + maxSize: { + width: number + height: number + } + object: string + setValue?: any + addAdditionalRefObject?: () => void + isNewImagePossible?: boolean + refPosition?: number +}) { + const onDrop = async (acceptedFiles: File[]) => { + onNewErrorMsg('') + + const file = acceptedFiles[0] + const allowedTypes = ['image/png', 'image/webp', 'image/jpeg'] + + if (!allowedTypes.includes(file.type)) { + onNewErrorMsg('Wrong input image format - Only png, jpeg and webp are allowed') + return + } + + const base64 = await fileToBase64(file) + const newImage = `data:${file.type};base64,${base64}` + setImage(newImage) + + if (setValue) { + const img = new window.Image() + img.onload = () => { + setValue(`${object}.width`, img.width) + setValue(`${object}.height`, img.height) + setValue(`${object}.ratio`, getAspectRatio(img.width, img.height)) + } + img.src = newImage + } + } + + const { getRootProps, getInputProps } = useDropzone({ onDrop }) + + return ( + <> + + {!image && ( + + + + + )} + {image && ( + + {'temp'} + {isNewImagePossible && addAdditionalRefObject && ( + + + + + + )} + + )} + + + ) +} diff --git a/src/app/ui/generate-components/ImageToPromptModal.tsx b/src/app/ui/generate-components/ImageToPromptModal.tsx new file mode 100644 index 00000000..166a42f3 --- /dev/null +++ b/src/app/ui/generate-components/ImageToPromptModal.tsx @@ -0,0 +1,224 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react' +import { useEffect, useState } from 'react' + +import { + Box, + Button, + CircularProgress, + Dialog, + DialogContent, + DialogTitle, + IconButton, + Slide, + Stack, + TextField, + Typography, +} from '@mui/material' +import { TransitionProps } from '@mui/material/transitions' +import { Check, Close, Replay } from '@mui/icons-material' + +import theme from '../../theme' +import ImageDropzone from './ImageDropzone' +import { set } from 'react-hook-form' +import { getPromptFromImageFromGemini } from '@/app/api/gemini/action' +import { CustomizedSendButton } from '../ux-components/Button-SX' +const { palette } = theme + +const Transition = React.forwardRef(function Transition( + props: TransitionProps & { + children: React.ReactElement + }, + ref: React.Ref +) { + return +}) + +export default function ImageToPromptModal({ + open, + setNewPrompt, + setImageToPromptOpen, + target, +}: { + open: boolean + setNewPrompt: (newPormpt: string) => void + setImageToPromptOpen: (state: boolean) => void + target: 'Image' | 'Video' +}) { + const [image, setImage] = useState(null) + const [prompt, setPrompt] = useState('') + const [isGeneratingPrompt, setIsGeneratingPrompt] = useState(false) + const [errorMsg, setErrorMsg] = useState('') + + useEffect(() => { + if (image !== '' && image !== null && prompt === '') { + getPromptFromImage() + } + }, [image]) + + const getPromptFromImage = async () => { + setIsGeneratingPrompt(true) + try { + const geminiReturnedPrompt = await getPromptFromImageFromGemini(image as string, target) + + if (!(typeof geminiReturnedPrompt === 'object' && 'error' in geminiReturnedPrompt)) + setPrompt(geminiReturnedPrompt as string) + } catch (error) { + console.error(error) + error && setErrorMsg(error.toString()) + } finally { + setIsGeneratingPrompt(false) + } + } + + const onValidate = () => { + if (prompt) setNewPrompt(prompt) + onClose() + } + + const onReset = () => { + setErrorMsg('') + setIsGeneratingPrompt(false) + setImage(null) + setPrompt('') + } + + const onClose = () => { + setImageToPromptOpen(false) + onReset() + } + + return ( + + + + + + + + {'Image-to-prompt generator'} + + + + setImage(base64Image)} + image={image} + onNewErrorMsg={setErrorMsg} + size={{ width: '110vw', height: '110vw' }} + maxSize={{ width: 280, height: 280 }} + object={'contain'} + /> + + + {isGeneratingPrompt && ( + + )} + setPrompt(e.target.value)} + multiline + rows={11} + defaultValue="Upload image first" + sx={{ width: '98%' }} + /> + + + + + + + + + + ) +} diff --git a/src/app/ui/generate-components/ReferenceBox.tsx b/src/app/ui/generate-components/ReferenceBox.tsx new file mode 100644 index 00000000..13d6a1e7 --- /dev/null +++ b/src/app/ui/generate-components/ReferenceBox.tsx @@ -0,0 +1,173 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React, { useState } from 'react' +import { Box, IconButton, Stack, CircularProgress } from '@mui/material' +import theme from '../../theme' +import ImageDropzone from './ImageDropzone' +import { maxReferences, ReferenceObjectI, referenceTypeField } from '@/app/api/generate-image-utils' +import { FormInputTextLine } from '../ux-components/InputTextLine' +import FormInputChipGroup from '../ux-components/InputChipGroup' +import { Clear, ForkLeftSharp } from '@mui/icons-material' +import { GeminiButton } from '../ux-components/GeminiButton' +import { cleanResult, getDescriptionFromGemini } from '@/app/api/gemini/action' +const { palette } = theme + +export const ReferenceBox = ({ + objectKey, + currentReferenceObject, + onNewErrorMsg, + control, + setValue, + removeReferenceObject, + addAdditionalRefObject, + refPosition, + refCount, +}: { + objectKey: string + currentReferenceObject: ReferenceObjectI + onNewErrorMsg: (msg: string) => void + control: any + setValue: any + removeReferenceObject: (objectKey: string) => void + addAdditionalRefObject: (objectKey: string) => void + refPosition: number + refCount: number +}) => { + const noImageSet = + currentReferenceObject.base64Image === '' || + currentReferenceObject.base64Image === null || + currentReferenceObject.base64Image === undefined + const noDescriptionSet = + currentReferenceObject.description === '' || + currentReferenceObject.description === null || + currentReferenceObject.description === undefined + const noReferenceTypeSet = + currentReferenceObject.referenceType === '' || + currentReferenceObject.referenceType === null || + currentReferenceObject.referenceType === undefined + const isNewRef = noImageSet && noReferenceTypeSet && noDescriptionSet + const isRefIncomplete = noImageSet || noReferenceTypeSet || noDescriptionSet + + let IDoptions = [] + for (let i = 1; i <= maxReferences; i++) IDoptions.push({ value: i.toString(), label: i.toString() }) + + const [isGettingDescription, setIsGettingDescription] = useState(false) + + const getDescription = async () => { + setIsGettingDescription(true) + if (!noReferenceTypeSet && !noImageSet) + try { + const geminiReturnedDescription = await getDescriptionFromGemini( + currentReferenceObject.base64Image, + currentReferenceObject.referenceType + ) + + if (!(typeof geminiReturnedDescription === 'object' && 'error' in geminiReturnedDescription)) + setValue(`referenceObjects.${refPosition}.description`, geminiReturnedDescription as string) + } catch (error) { + console.error(error) + } finally { + setIsGettingDescription(false) + } + } + + return ( + + removeReferenceObject(objectKey)} + disabled={isNewRef && refCount === 1} + disableRipple + sx={{ + border: 0, + boxShadow: 0, + p: 0, + '&:hover': { + color: palette.primary.main, + backgroundColor: 'transparent', + border: 0, + boxShadow: 0, + }, + }} + > + + + setValue(`referenceObjects.${refPosition}.base64Image`, base64Image)} + image={currentReferenceObject.base64Image} + onNewErrorMsg={onNewErrorMsg} + size={{ width: '5vw', height: '5vw' }} + maxSize={{ width: 70, height: 70 }} + object={`referenceObjects.${refPosition}`} + setValue={setValue} + addAdditionalRefObject={() => addAdditionalRefObject(objectKey)} + isNewImagePossible={!isRefIncomplete && !currentReferenceObject.isAdditionalImage && refCount < maxReferences} + /> + {!currentReferenceObject.isAdditionalImage && ( + <> + + + + + {!noReferenceTypeSet && ( + + + {isGettingDescription ? ( + + ) : ( + + )} + + )} + + + )} + {currentReferenceObject.isAdditionalImage && ( + + + + )} + + ) +} diff --git a/src/app/ui/generate-components/ReferencePicker.tsx b/src/app/ui/generate-components/ReferencePicker.tsx new file mode 100644 index 00000000..44e91396 --- /dev/null +++ b/src/app/ui/generate-components/ReferencePicker.tsx @@ -0,0 +1,139 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react' +import { IconButton, Typography, Box, Menu, MenuItem, Avatar, Chip } from '@mui/material' +import { CustomizedAvatarButton, CustomizedIconButton, CustomizedIconButtonOpen } from '../ux-components/Button-SX' + +import theme from '../../theme' +const { palette } = theme +import { LibraryAdd } from '@mui/icons-material' +import CustomTooltip from '../ux-components/Tooltip' + +const CustomizedMenu = { + '& .MuiPaper-root': { + background: 'white', + color: palette.text.primary, + boxShadow: 5, + p: 0.5, + width: 250, + '& .MuiMenuItem-root': { + background: 'transparent', + pb: 1, + }, + }, +} + +const CustomizedChip = { + fontSize: '0.9rem', + mb: 0.2, + border: 1, + borderColor: palette.secondary.light, + letterSpacing: '0.05px', + '&:hover': { + borderColor: palette.primary.main, + bgcolor: palette.primary.main, + transition: 'none', + color: palette.text.primary, + fontWeight: 500, + letterSpacing: '0px', + }, + '&:active': { + boxShadow: 0, + }, + '&.MuiChip-filled': { + color: 'white', + letterSpacing: '0.05px', + '&:hover': { + letterSpacing: '0px', + }, + }, + '& .MuiChip-label': { + px: 1, + }, +} + +const referenceColors = ['#4285F4', '#0F9D58', '#F4B400', '#DB4437'] + +const references = [ + { + id: 1, + name: 'Cat', + iconImage: + 'data:image/png;base64,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', + referenceColor: '#4285F4', + }, + { + id: 2, + name: 'Dog', + iconImage: + 'data:image/png;base64,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', + referenceColor: '#0F9D58', + }, +] + +export default function ReferencePicker({}) { + const [anchorEl, setAnchorEl] = React.useState(null) + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const open = Boolean(anchorEl) + + const handleClose = () => { + setAnchorEl(null) + } + + return ( + <> + + + + + + + + + {references.map((reference) => ( + } + label={reference.name} + key={reference.name} + variant="outlined" + sx={{ ...CustomizedChip, ...{ cursor: 'pointer' } }} + /> + ))} + + + ) +} diff --git a/src/app/ui/generate-components/VideoInterpolBox.tsx b/src/app/ui/generate-components/VideoInterpolBox.tsx new file mode 100644 index 00000000..e043159c --- /dev/null +++ b/src/app/ui/generate-components/VideoInterpolBox.tsx @@ -0,0 +1,143 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React, { useEffect, useState } from 'react' +import { IconButton, Stack, Typography } from '@mui/material' +import theme from '../../theme' +import ImageDropzone from './ImageDropzone' +import { Clear } from '@mui/icons-material' +import { InterpolImageI } from '@/app/api/generate-video-utils' +const { palette } = theme + +export function getOrientation(aspectRatio: string): 'horizontal' | 'vertical' { + const parts = aspectRatio.split(':') + const width = parseFloat(parts[0]) + const height = parseFloat(parts[1]) + + if (width >= height) return 'horizontal' + else return 'vertical' +} + +export const VideoInterpolBox = ({ + label, + sublabel, + objectKey, + setValue, + onNewErrorMsg, + interpolImage, + orientation, +}: { + label: string + sublabel: string + objectKey: string + onNewErrorMsg: (msg: string) => void + setValue: any + interpolImage: InterpolImageI + orientation: string +}) => { + const initSize = { + width: '5vw', + height: '5vw', + } + const initMaxSize = { + width: 70, + height: 70, + } + const [size, setSize] = useState(initSize) + const [maxSize, setMaxSize] = useState(initMaxSize) + + useEffect(() => { + if (orientation === '') { + setSize(initSize) + setMaxSize(initMaxSize) + } else { + if (orientation === 'horizontal') { + setSize({ width: '7vw', height: '5vw' }) + setMaxSize({ width: 100, height: 70 }) + } + if (orientation === 'vertical') { + setSize({ width: '5vw', height: '7vw' }) + setMaxSize({ width: 70, height: 100 }) + } + } + }, [orientation]) + + return ( + + + + {label} + + + {sublabel} + + {interpolImage.base64Image !== '' && ( + setValue(`${objectKey}.base64Image`, '')} + disabled={interpolImage.base64Image === ''} + disableRipple + sx={{ + border: 0, + boxShadow: 0, + color: palette.secondary.main, + p: 0, + '&:hover': { + backgroundColor: 'transparent', + border: 0, + boxShadow: 1, + }, + }} + > + + + )} + + setValue(`${objectKey}.base64Image`, base64Image)} + image={interpolImage.base64Image} + onNewErrorMsg={onNewErrorMsg} + size={size} + maxSize={maxSize} + object={objectKey} + setValue={setValue} + /> + + ) +} diff --git a/src/app/ui/library-components/ExploreDialog.tsx b/src/app/ui/library-components/ExploreDialog.tsx new file mode 100644 index 00000000..86326531 --- /dev/null +++ b/src/app/ui/library-components/ExploreDialog.tsx @@ -0,0 +1,252 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use client' + +import * as React from 'react' +import { useState } from 'react' + +import { Dialog, DialogContent, DialogTitle, IconButton, Slide, Box, Button, Typography, Stack } from '@mui/material' +import { TransitionProps } from '@mui/material/transitions' +import { ArrowRight, AutoAwesome, Close, Download, Edit, VideocamRounded } from '@mui/icons-material' +import { useAppContext, appContextDataDefault } from '../../context/app-context' + +import theme from '../../theme' +import { MediaMetadataI } from '../../api/export-utils' +import { CustomizedSendButton } from '../ux-components/Button-SX' +import { downloadMediaFromGcs } from '../../api/cloud-storage/action' +import { useRouter } from 'next/navigation' +import { downloadBase64Media } from '../transverse-components/ExportDialog' +const { palette } = theme + +const Transition = React.forwardRef(function Transition( + props: TransitionProps & { + children: React.ReactElement + }, + ref: React.Ref +) { + return +}) + +export default function ExploreDialog({ + open, + documentToExplore, + handleMediaExploreClose, +}: { + open: boolean + documentToExplore: MediaMetadataI | undefined + handleMediaExploreClose: () => void +}) { + const [downloadStatus, setDownloadStatus] = useState('Download') + + const { setAppContext } = useAppContext() + const router = useRouter() + + const handleEditClick = (uri: string) => { + setAppContext((prevContext) => { + if (prevContext) return { ...prevContext, imageToEdit: uri } + else return { ...appContextDataDefault, imageToEdit: uri } + }) + router.push('/edit') + } + const handleITVClick = (imageGcsURI: string) => { + setAppContext((prevContext) => { + if (prevContext) return { ...prevContext, imageToVideo: imageGcsURI } + else return { ...appContextDataDefault, imageToVideo: imageGcsURI } + }) + router.push('/generate') + } + + const handleRegenerateClick = (prompt: string, format: string) => { + if (format === 'MP4') { + setAppContext((prevContext) => { + if (prevContext) return { ...prevContext, promptToGenerateVideo: prompt, promptToGenerateImage: '' } + else return { ...appContextDataDefault, promptToGenerateVideo: prompt, promptToGenerateImage: '' } + }) + } else { + setAppContext((prevContext) => { + if (prevContext) return { ...prevContext, promptToGenerateImage: prompt, promptToGenerateVideo: '' } + else return { ...appContextDataDefault, promptToGenerateImage: prompt, promptToGenerateVideo: '' } + }) + } + + router.push('/generate') + } + + const handleDownload = async (documentToExplore: MediaMetadataI) => { + try { + setDownloadStatus('Preparing download...') + const res = await downloadMediaFromGcs(documentToExplore.gcsURI) + const mediaName = `${documentToExplore.id}.${documentToExplore.format.toLowerCase()}` + downloadBase64Media(res.data, mediaName, documentToExplore.format) + + if (typeof res === 'object' && res['error']) { + throw Error(res['error'].replaceAll('Error: ', '')) + } + } catch (error: any) { + console.error(error) + } finally { + setDownloadStatus('Download') + } + } + + const { appContext } = useAppContext() + const exportMetaOptions = appContext ? appContext.exportMetaOptions : appContextDataDefault.exportMetaOptions + + if (exportMetaOptions) + return ( + + + + + + + + {'Explore media metadata'} + + + + {documentToExplore && + Object.entries(exportMetaOptions).map(([key, fieldConfig]) => { + const value = documentToExplore[key] + let displayValue = value ? `${value}` : null + + if (displayValue && typeof value === 'object') { + displayValue = Object.keys(value) + .filter((val) => value[val]) + .map((val) => { + const matchingOption = fieldConfig.options?.find( + (option: { value: string }) => option.value === val + ) + return matchingOption ? matchingOption.label : val + }) + .join(', ') + } + + const displayLabel = fieldConfig.name || fieldConfig.label + + if (displayValue && displayValue !== '' && fieldConfig.isExploreVisible) { + return ( + + + + + {`${displayLabel}: `} + + + {displayValue} + + + + ) + } else { + return null + } + })} + + + {documentToExplore && ( + <> + + + + + + + {process.env.NEXT_PUBLIC_EDIT_ENABLED === 'true' && documentToExplore.format !== 'MP4' && ( + + + + )} + {process.env.NEXT_PUBLIC_VEO_ENABLED === 'true' && + process.env.NEXT_PUBLIC_VEO_ITV_ENABLED === 'true' && + documentToExplore.format !== 'MP4' && ( + + + + )} + + )} + + + + ) +} diff --git a/src/app/ui/library-components/LibraryFiltering.tsx b/src/app/ui/library-components/LibraryFiltering.tsx new file mode 100644 index 00000000..2c11eacb --- /dev/null +++ b/src/app/ui/library-components/LibraryFiltering.tsx @@ -0,0 +1,180 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use client' + +import * as React from 'react' +import { useForm, SubmitHandler, useWatch } from 'react-hook-form' +import { + Typography, + Accordion, + AccordionSummary, + AccordionDetails, + Button, + Box, + IconButton, + Stack, + Avatar, +} from '@mui/material' +import { + Send as SendIcon, + WatchLater as WatchLaterIcon, + ArrowDownward as ArrowDownwardIcon, + Autorenew, +} from '@mui/icons-material' + +import theme from '../../theme' +import { CustomizedAvatarButton, CustomizedIconButton, CustomizedSendButton } from '../ux-components/Button-SX' +import { useEffect, useRef } from 'react' +import CustomTooltip from '../ux-components/Tooltip' +import { CustomizedAccordion, CustomizedAccordionSummary } from '../ux-components/Accordion-SX' +import { FilterMediaFormI } from '../../api/export-utils' +import FormInputChipGroupMultiple from '../ux-components/InputChipGroupMultiple' +import { useAppContext, appContextDataDefault } from '../../context/app-context' + +const { palette } = theme + +export default function LibraryFiltering({ + isMediasLoading, + setIsMediasLoading, + setErrorMsg, + submitFilters, + openFilters, + setOpenFilters, +}: { + isMediasLoading: boolean + setIsMediasLoading: any + setErrorMsg: any + submitFilters: any + openFilters: boolean + setOpenFilters: any +}) { + const { handleSubmit, reset, control, setValue } = useForm() + + const { appContext } = useAppContext() + const ExportImageFormFields = appContext ? appContext.exportMetaOptions : appContextDataDefault.exportMetaOptions + let temp2: any = [] + if (ExportImageFormFields) { + Object.entries(ExportImageFormFields).forEach(([name, field]) => { + if (field.isExportVisible && field.options !== undefined) { + temp2.push({ key: name, field: field }) + } + }) + } + + const MetadataFilterFields = temp2 + + const watchedFields = useWatch({ control }) + const watchedFieldValues = useWatch({ + control, + name: MetadataFilterFields.map((field: { key: any }) => field.key), + }) + + const prevSelectedFields = useRef>(new Set()) + + useEffect(() => { + const activeFieldKeys = MetadataFilterFields.filter( + (field: { key: string | number }) => watchedFields[field.key]?.length > 0 + ).map((field: { key: any }) => field.key) + + // Check if any new fields have been selected + const newSelections = activeFieldKeys.filter((key: any) => !prevSelectedFields.current.has(key)) + + if (newSelections.length > 0) + // If there are new selections, reset other fields + MetadataFilterFields.forEach((field: { key: any }) => { + if (!newSelections.includes(field.key)) setValue(field.key, []) + }) + + // Update the set of previously selected fields + prevSelectedFields.current = new Set(activeFieldKeys) + }, [watchedFieldValues, setValue]) + + const onSubmit: SubmitHandler = async (formData: FilterMediaFormI) => { + setIsMediasLoading(true) + setOpenFilters(false) + + try { + submitFilters(formData) + } catch (error: any) { + setErrorMsg(error.toString()) + } + } + + return ( +
+ setOpenFilters(!openFilters)} + > + } + aria-controls="panel1-content" + id="panel1-header" + sx={CustomizedAccordionSummary} + > + + {'Setup filters'} + + + + + {'Filters can have multiple values, but only one filter can be used at once'} + + + {MetadataFilterFields.map(function ({ key, field }: any) { + return ( + + + + ) + })} + + + + + reset()} aria-label="Reset form" disableRipple sx={{ px: 0.5 }}> + + + + + + + + +
+ ) +} diff --git a/src/app/ui/library-components/LibraryMediasDisplay.tsx b/src/app/ui/library-components/LibraryMediasDisplay.tsx new file mode 100644 index 00000000..6a695f63 --- /dev/null +++ b/src/app/ui/library-components/LibraryMediasDisplay.tsx @@ -0,0 +1,372 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use client' + +import * as React from 'react' +import Box from '@mui/material/Box' +import { useEffect, useState, useMemo } from 'react' +import Image from 'next/image' + +import { + Button, + IconButton, + ImageList, + ImageListItem, + ImageListItemBar, + Modal, + Skeleton, + Typography, +} from '@mui/material' + +import theme from '../../theme' +import { MediaMetadataI, MediaMetadataWithSignedUrl } from '@/app/api/export-utils' +import { blurDataURL } from '@/app/ui/ux-components/BlurImage' +import { + ArrowBackIos, + ArrowForwardIos, + Info, + PlayArrowRounded, + RadioButtonUnchecked, + RemoveCircle, +} from '@mui/icons-material' +import { CustomizedIconButton } from '../ux-components/Button-SX' +import ExploreDialog from './ExploreDialog' +import { CustomWhiteTooltip } from '../ux-components/Tooltip' +import { VideoI } from '@/app/api/generate-video-utils' +const { palette } = theme + +const CustomizedNavButtons = { + '&:hover': { bgcolor: 'transparent', fontWeight: 700 }, + fontSize: '1.1rem', + fontWeight: 500, + '&.Mui-disabled': { fontWeight: 500, fontSize: '1.05rem' }, +} + +export default function LibraryMediasDisplay({ + isMediasLoading, + fetchedMediasByPage, + handleLoadMore, + isMorePageToLoad, + isDeleteSelectActive, + selectedDocIdsForDelete, + onToggleDeleteSelect, +}: { + isMediasLoading: boolean + fetchedMediasByPage: MediaMetadataWithSignedUrl[][] + handleLoadMore: () => void + isMorePageToLoad: boolean + isDeleteSelectActive: boolean + selectedDocIdsForDelete: string[] + onToggleDeleteSelect: (docId: string) => void +}) { + const [page, setPage] = useState(1) + const [maxPage, setMaxPage] = useState(0) + const [currentPageImages, setCurrentPageImages] = useState([]) + + useEffect(() => { + if (fetchedMediasByPage[page - 1]) setCurrentPageImages(fetchedMediasByPage[page - 1]) + else if (page > fetchedMediasByPage.length && isMorePageToLoad) handleLoadMore() + + const calculatedMaxPage = isMorePageToLoad ? fetchedMediasByPage.length + 1 : fetchedMediasByPage.length + setMaxPage(calculatedMaxPage) + }, [page, fetchedMediasByPage]) + + // Full screen & Explore handlers + const [mediaFullScreen, setMediaFullScreen] = useState() + const [mediaToExplore, setMediaToExplore] = useState() + + const handleContextMenu = (event: React.MouseEvent | React.MouseEvent) => { + event.preventDefault() + } + + function ImageDisplay({ doc }: { doc: MediaMetadataWithSignedUrl }) { + return ( + <> + {!isDeleteSelectActive && ( + setMediaFullScreen(doc)} + > + + Click to see full screen + + + )} + + ) + } + + function VideoDisplay({ doc }: { doc: MediaMetadataWithSignedUrl }) { + return ( + <> + + {!isDeleteSelectActive && ( + setMediaFullScreen(doc)} + /> + )} + + ) + } + + const mediaListItems = useMemo(() => { + return currentPageImages.map((doc: MediaMetadataWithSignedUrl) => ( + + {'temp'} + + {doc.format === 'MP4' ? : } + onToggleDeleteSelect(doc.id)} + aria-label="Explore media" + sx={{ + px: 0.5, + '&:hover': { + backgroundColor: 'transparent', + border: 0, + boxShadow: 0, + }, + }} + > + {selectedDocIdsForDelete.includes(doc.id) ? ( + + ) : ( + + )} + + ) : ( + + setMediaToExplore(doc)} aria-label="Explore media" sx={{ px: 0.5 }}> + + + + ) + } + /> + + )) + }, [currentPageImages, isDeleteSelectActive, selectedDocIdsForDelete]) + + return ( + <> + + {isMediasLoading ? ( + + ) : ( + <> + {currentPageImages !== undefined && currentPageImages.length !== 0 && ( + <> + + {mediaListItems} + + + + + )} + + )} + + + {mediaFullScreen !== undefined && mediaFullScreen.format !== 'MP4' && !isDeleteSelectActive && ( + setMediaFullScreen(undefined)} + sx={{ + display: 'flex', + alignContent: 'center', + justifyContent: 'center', + m: 5, + cursor: 'pointer', + maxHeight: '90vh', + maxWidth: '90vw', + }} + disableAutoFocus={true} + > + {'displayed-image'} setMediaFullScreen(undefined)} + onContextMenu={handleContextMenu} + /> + + )} + + {mediaFullScreen !== undefined && mediaFullScreen.format === 'MP4' && !isDeleteSelectActive && ( + setMediaFullScreen(undefined)} + sx={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }} + onClick={() => setMediaFullScreen(undefined)} + disableAutoFocus={true} + > + e.stopPropagation()} + > + + + )} + + setMediaToExplore(undefined)} + /> + + ) +} diff --git a/src/app/ui/transverse-components/DownloadDialog.tsx b/src/app/ui/transverse-components/DownloadDialog.tsx new file mode 100644 index 00000000..d2f423fa --- /dev/null +++ b/src/app/ui/transverse-components/DownloadDialog.tsx @@ -0,0 +1,242 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react' +import { useState } from 'react' + +import { + Dialog, + DialogContent, + DialogTitle, + IconButton, + RadioGroup, + Slide, + Box, + Button, + Typography, + FormControlLabel, + FormControl, + FormLabel, + Radio, +} from '@mui/material' +import { ImageI } from '../../api/generate-image-utils' +import { TransitionProps } from '@mui/material/transitions' +import { CustomizedSendButton } from '../ux-components/Button-SX' +import { Close, Send, WatchLater } from '@mui/icons-material' +import { CustomRadioButton, CustomRadioLabel } from '../ux-components/InputRadioButton' + +import theme from '../../theme' +import { downloadMediaFromGcs } from '../../api/cloud-storage/action' +import { upscaleImage } from '../../api/imagen/action' +import { useAppContext } from '../../context/app-context' +import { ExportAlerts } from './ExportAlerts' + +const { palette } = theme + +const Transition = React.forwardRef(function Transition( + props: TransitionProps & { + children: React.ReactElement + }, + ref: React.Ref +) { + return +}) + +export const downloadBase64Media = (base64Data: any, filename: string, format: string) => { + const link = document.createElement('a') + link.href = `data:${format};base64,${base64Data}` + link.download = filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) +} + +export default function DownloadDialog({ + open, + mediaToDL, + handleMediaDLClose, +}: { + open: boolean + mediaToDL: ImageI | undefined + handleMediaDLClose: () => void +}) { + const [upscaleFactor, setUpscaleFactor] = useState('no') + const handleChange = (event: React.ChangeEvent) => { + setUpscaleFactor((event.target as HTMLInputElement).value) + } + + const [status, setStatus] = useState('') + const [errorMsg, setErrorMsg] = useState('') + + const { appContext } = useAppContext() + + const handleImageDL = async () => { + setStatus('Starting...') + + const media = mediaToDL + + if (media) { + try { + // 1. Upscale if needed + let upscaledGcsUri + if (upscaleFactor === 'x2' || upscaleFactor === 'x4') { + try { + setStatus('Upscaling...') + + upscaledGcsUri = await upscaleImage(media.gcsUri, upscaleFactor, appContext) + if (typeof upscaledGcsUri === 'object' && 'error' in upscaledGcsUri) + throw Error(upscaledGcsUri.error.replaceAll('Error: ', '')) + + media.gcsUri = upscaledGcsUri + } catch (error: any) { + throw Error(error) + } + } + + // 2. DL locally + try { + setStatus('Preparing download...') + const res = await downloadMediaFromGcs(media.gcsUri) + const name = `${media.key}.${media.format.toLowerCase()}` + downloadBase64Media(res.data, name, media.format) + + if (typeof res === 'object' && res.error) throw Error(res.error.replaceAll('Error: ', '')) + } catch (error: any) { + throw Error(error) + } + + setStatus('') + onClose() + } catch (error: any) { + console.log(error) + setErrorMsg('Error while upscaleing your image') + } + } + } + + const onClose = () => { + handleMediaDLClose() + setErrorMsg('') + } + + return ( + + + + + + + + {'Download image'} + + + + {'You can upscale resolution to have a sharper and clearer look.'} + + + + } + label={CustomRadioLabel( + 'no', + 'No upscaling', + mediaToDL ? `${mediaToDL.width} x ${mediaToDL.height} px` : '', + upscaleFactor, + true + )} + /> + } + label={CustomRadioLabel( + 'x2', + 'Scale x2', + mediaToDL ? `${mediaToDL.width * 2} x ${mediaToDL.height * 2} px` : '', + upscaleFactor, + true + )} + /> + } + label={CustomRadioLabel( + 'x4', + 'Scale x4', + mediaToDL ? `${mediaToDL.width * 4} x ${mediaToDL.height * 4} px` : '', + upscaleFactor, + true + )} + /> + + + + + + + + + {errorMsg !== '' && ( + { + setErrorMsg('') + setStatus('') + }} + /> + )} + + ) +} diff --git a/src/app/ui/transverse-components/ExportAlerts.tsx b/src/app/ui/transverse-components/ExportAlerts.tsx new file mode 100644 index 00000000..bf598cdc --- /dev/null +++ b/src/app/ui/transverse-components/ExportAlerts.tsx @@ -0,0 +1,98 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react' +import { Alert, Box, Button, IconButton } from '@mui/material' +import theme from '../../theme' +import { ArrowForwardIos, Close } from '@mui/icons-material' +const { palette } = theme + +export const CloseWithoutSubmitWarning = ({ onClose, onKeepOpen }: { onClose: any; onKeepOpen: any }) => { + return ( + + {'Are you sure you want to exit now ?'} + + + + + + ) +} + +export const ExportAlerts = ({ + onClose, + message, + style, +}: { + onClose: any + message: string + style: 'error' | 'success' +}) => { + return ( + + + + } + sx={{ height: 'auto', mb: 2, fontSize: 16, fontWeight: 500, pt: 1, color: palette.text.secondary }} + > + {message} + + ) +} diff --git a/src/app/ui/transverse-components/ExportDialog.tsx b/src/app/ui/transverse-components/ExportDialog.tsx new file mode 100644 index 00000000..fb3454d4 --- /dev/null +++ b/src/app/ui/transverse-components/ExportDialog.tsx @@ -0,0 +1,606 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react' +import { useEffect, useState } from 'react' + +import { + Dialog, + DialogContent, + DialogProps, + DialogTitle, + IconButton, + RadioGroup, + Slide, + StepIconProps, + Box, + Stepper, + Step, + StepLabel, + StepContent, + Button, + Typography, + Checkbox, + FormControlLabel, +} from '@mui/material' +import { ImageI } from '../../api/generate-image-utils' +import { TransitionProps } from '@mui/material/transitions' +import { CustomizedSendButton } from '../ux-components/Button-SX' +import { + ArrowForwardIos, + ArrowRight, + Close, + DownloadForOfflineRounded, + RadioButtonUncheckedRounded, + Send, + WatchLater, +} from '@mui/icons-material' +import { CustomRadio } from '../ux-components/InputRadioButton' + +import { ExportMediaFormFieldsI, ExportMediaFormI } from '../../api/export-utils' +import { Controller, set, SubmitHandler, useForm } from 'react-hook-form' +import FormInputChipGroupMultiple from '../ux-components/InputChipGroupMultiple' +import { CloseWithoutSubmitWarning, ExportAlerts } from '../transverse-components/ExportAlerts' + +import theme from '../../theme' +import { + copyImageToTeamBucket, + downloadMediaFromGcs, + getVideoThumbnailBase64, + uploadBase64Image, +} from '../../api/cloud-storage/action' +import { upscaleImage } from '../../api/imagen/action' +import { addNewFirestoreEntry } from '../../api/firestore/action' +import { useAppContext, appContextDataDefault } from '../../context/app-context' +import { VideoI } from '@/app/api/generate-video-utils' + +const { palette } = theme + +const Transition = React.forwardRef(function Transition( + props: TransitionProps & { + children: React.ReactElement + }, + ref: React.Ref +) { + return +}) + +export const downloadBase64Media = (base64Data: any, filename: string, format: string) => { + const link = document.createElement('a') + link.href = `data:${format};base64,${base64Data}` + link.download = filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) +} + +export default function ExportStepper({ + open, + upscaleAvailable, + mediaToExport, + handleMediaExportClose, +}: { + open: boolean + upscaleAvailable: boolean + mediaToExport: ImageI | VideoI | undefined + handleMediaExportClose: () => void +}) { + const [activeStep, setActiveStep] = useState(0) + const [isCloseWithoutSubmit, setIsCloseWithoutSubmit] = useState(false) + const [isExporting, setIsExporting] = useState(false) + const [exportStatus, setExportStatus] = useState('') + const [errorMsg, setErrorMsg] = useState('') + const [isDownload, setIsDownload] = useState(false) + const { + handleSubmit, + resetField, + control, + setValue, + getValues, + formState: { errors }, + } = useForm({ + defaultValues: { upscaleFactor: mediaToExport?.ratio === '1:1' ? 'x4' : 'no' }, + }) + + useEffect(() => { + if (mediaToExport) setValue('mediaToExport', mediaToExport) + }, [mediaToExport]) + + const handleNext = () => { + setActiveStep((prevActiveStep) => prevActiveStep + 1) + setIsCloseWithoutSubmit(false) + } + const handleBack = () => { + setActiveStep((prevActiveStep) => prevActiveStep - 1) + setIsCloseWithoutSubmit(false) + } + + const handleCheckDownload = (event: React.ChangeEvent) => { + setIsDownload(event.target.checked) + } + + const { appContext } = useAppContext() + const exportMediaFormFields = appContext ? appContext.exportMetaOptions : appContextDataDefault.exportMetaOptions + + let metadataReviewFields: any + var infoToReview: { label: string; value: string }[] = [] + let temp: { [key: string]: ExportMediaFormFieldsI[keyof ExportMediaFormFieldsI] }[] = [] + if (exportMediaFormFields) { + const exportMediaFieldList: (keyof ExportMediaFormFieldsI)[] = Object.keys(exportMediaFormFields).map( + (key) => key as keyof ExportMediaFormFieldsI + ) + + metadataReviewFields = exportMediaFieldList.filter( + (field) => + exportMediaFormFields[field].type === 'text-info' && + exportMediaFormFields[field].isExportVisible && + !exportMediaFormFields[field].isUpdatable + ) + mediaToExport && + metadataReviewFields.forEach((field: any) => { + const prop = exportMediaFormFields[field].prop + const value = mediaToExport[prop as keyof (ImageI | VideoI)] + if (prop && value) + infoToReview.push({ + label: exportMediaFormFields[field].label, + value: value.toString(), + }) + }) + + Object.entries(exportMediaFormFields).forEach(([name, field]) => { + if (field.isUpdatable && field.isExportVisible) temp.push({ [name]: field }) + }) + } + const MetadataImproveFields = temp + + const handleImageExportSubmit: SubmitHandler = React.useCallback( + async (formData: ExportMediaFormI) => { + setIsExporting(true) + setExportStatus('Starting...') + + const media = formData.mediaToExport + + try { + // 1. Upscale if needed + let upscaledGcsUri + const upscaleFactor = formData.upscaleFactor + if (upscaleFactor === 'x2' || upscaleFactor === 'x4') { + try { + setExportStatus('Upscaling...') + + upscaledGcsUri = await upscaleImage(media.gcsUri, upscaleFactor, appContext) + if (typeof upscaledGcsUri === 'object' && 'error' in upscaledGcsUri) + throw Error(upscaledGcsUri.error.replaceAll('Error: ', '')) + + media.gcsUri = upscaledGcsUri + + media.width = media.width * parseInt(upscaleFactor.replace(/[^0-9]/g, '')) + media.height = media.height * parseInt(upscaleFactor.replace(/[^0-9]/g, '')) + } catch (error: any) { + throw Error(error) + } + } + + // 2. Copy media to team library + const currentGcsUri = media.gcsUri + const id = media.key + try { + setExportStatus('Exporting...') + const res = await copyImageToTeamBucket(currentGcsUri, id) + + if (typeof res === 'object' && 'error' in res) throw Error(res.error.replaceAll('Error: ', '')) + + const movedGcsUri = res + media.gcsUri = movedGcsUri + } catch (error: any) { + throw Error(error) + } + + // 2.5. If media is a video, upload its thumbnail + if (media.format === 'MP4') { + setExportStatus('Generating thumbnail...') + + const result = await getVideoThumbnailBase64(media.gcsUri, media.ratio) + if (!result.thumbnailBase64Data) console.error('Failed to generate thumbnail:', result.error) + const thumbnailBase64Data = result.thumbnailBase64Data + + if (thumbnailBase64Data && process.env.NEXT_PUBLIC_TEAM_BUCKET) { + try { + const uploadResult = await uploadBase64Image( + thumbnailBase64Data, + process.env.NEXT_PUBLIC_TEAM_BUCKET, + `${id}_thumbnail.png`, + 'image/png' + ) + + if (uploadResult.success && uploadResult.fileUrl) formData.videoThumbnailGcsUri = uploadResult.fileUrl + else { + formData.videoThumbnailGcsUri = '' + console.warn('Video thumbnail upload failed:', uploadResult.error) + } + } catch (thumbError: any) { + console.error('Video thumbnail upload exception:', thumbError) + } + } else + console.warn(`Video ${id} is a video format but has no thumbnailBase64Data. Skipping thumbnail upload.`) + } + + // 3. Upload metadata to firestore + try { + setExportStatus('Saving data...') + + let res + if (exportMediaFormFields) res = await addNewFirestoreEntry(id, formData, exportMediaFormFields) + else throw Error("Can't find exportMediaFormFields") + + if (typeof res === 'object' && 'error' in res) throw Error(res.error.replaceAll('Error: ', '')) + } catch (error: any) { + throw Error(error) + } + + // 4. DL locally if asked to + if (isDownload) { + try { + setExportStatus('Preparing download...') + const res = await downloadMediaFromGcs(media.gcsUri) + const name = `${media.key}.${media.format.toLowerCase()}` + downloadBase64Media(res.data, name, media.format) + + if (typeof res === 'object' && res.error) throw Error(res.error.replaceAll('Error: ', '')) + } catch (error: any) { + throw Error(error) + } + } + + setExportStatus('') + setIsExporting(false) + onClose() + } catch (error: any) { + console.log(error) + setErrorMsg('Error while exporting your image') + } + }, + [isDownload] + ) + + const onCloseTry: DialogProps['onClose'] = ( + event: React.MouseEvent | React.KeyboardEvent, + reason: string + ) => { + if (reason && (reason === 'backdropClick' || reason === 'escapeKeyDown')) { + event?.stopPropagation() + setIsCloseWithoutSubmit(true) + } else { + onClose() + } + } + const onClose = () => { + setIsCloseWithoutSubmit(false) + setActiveStep(0) + handleMediaExportClose() + setErrorMsg('') + setIsExporting(false) + setExportStatus('') + setIsDownload(false) + resetField('mediaToExport') + } + + function CustomStepIcon(props: StepIconProps) { + const { active, completed, icon } = props + + return ( + + {icon} + + ) + } + + function CustomStepLabel({ text, step }: { text: string; step: number }) { + return ( + + {text} + + ) + } + + const ReviewStep = () => { + return ( + + {infoToReview.map(({ label, value }) => ( + + + + {`${label}: `} + {`${value}`} + + + ))} + + ) + } + + const TagStep = () => { + return ( + <> + + {'Set up metadata to ensure discoverabilty within shared Library.'} + + + + {MetadataImproveFields.map((fieldObject) => { + const param = Object.keys(fieldObject)[0] + const field = fieldObject[param] + + return ( + + + + ) + })} + + + ) + } + + const UpscaleStep = () => { + return ( + <> + + {'Upscale resolution to have a sharper and clearer look.'} + + ( + + + + + + )} + /> + + ) + } + + function NextBackBox({ backAvailable }: { backAvailable: boolean }) { + return ( + + + {backAvailable && ( + + )} + + ) + } + + const SubmitBox = () => { + return ( + <> + } + checkedIcon={} + sx={{ + '&:hover': { backgroundColor: 'transparent' }, + '&.MuiCheckbox-root:hover': { color: palette.primary.main }, + }} + /> + } + label="Download this media locally while exporting" + disableTypography + sx={{ + px: 1.5, + pt: 3, + '&.MuiFormControlLabel-root': { + fontSize: '1.1rem', + alignContent: 'center', + color: isExporting ? palette.secondary.main : isDownload ? palette.primary.main : palette.text.secondary, + fontStyle: isExporting ? 'italic' : 'normal', + }, + }} + /> + + + + + + + + ) + } + + return ( + + setIsCloseWithoutSubmit(true)} + sx={{ + position: 'absolute', + right: 8, + top: 8, + color: palette.secondary.dark, + }} + > + + + + + + {'Export to internal Library'} + + +
+ + + + + + + + + + + + + + + + + + {upscaleAvailable && } + {!upscaleAvailable && } + + + + {upscaleAvailable && ( + + + + + + + + + + )} + +
+
+ + {isCloseWithoutSubmit && ( + setIsCloseWithoutSubmit(false)} /> + )} + + {errorMsg !== '' && ( + { + setIsExporting(false) + setErrorMsg('') + setExportStatus('') + }} + /> + )} +
+ ) +} diff --git a/src/app/ui/transverse-components/ImagenOutputImagesDisplay.tsx b/src/app/ui/transverse-components/ImagenOutputImagesDisplay.tsx new file mode 100644 index 00000000..647354aa --- /dev/null +++ b/src/app/ui/transverse-components/ImagenOutputImagesDisplay.tsx @@ -0,0 +1,284 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use client' + +import * as React from 'react' +import { useState } from 'react' + +import { AutoAwesome, CreateNewFolderRounded, Download, Edit, Favorite, VideocamRounded } from '@mui/icons-material' + +import Image from 'next/image' +import { + Avatar, + Box, + IconButton, + Modal, + Skeleton, + ImageListItem, + ImageList, + ImageListItemBar, + Typography, + Stack, +} from '@mui/material' + +import { ImageI } from '../../api/generate-image-utils' +import { CustomizedAvatarButton, CustomizedIconButton } from '../ux-components/Button-SX' +import ExportStepper from './ExportDialog' +import DownloadDialog from './DownloadDialog' + +import theme from '../../theme' +import { blurDataURL } from '../ux-components/BlurImage' +import { CustomWhiteTooltip } from '../ux-components/Tooltip' +import { appContextDataDefault, useAppContext } from '../../context/app-context' +import { useRouter } from 'next/navigation' +const { palette } = theme + +export default function OutputImagesDisplay({ + isLoading, + generatedImagesInGCS, + generatedCount, + isPromptReplayAvailable, +}: { + isLoading: boolean + generatedImagesInGCS: ImageI[] + generatedCount: number + isPromptReplayAvailable: boolean +}) { + // Full screen, Export & Download states + const [imageFullScreen, setImageFullScreen] = useState() + const [imageToExport, setImageToExport] = useState() + const [imageToDL, setImageToDL] = useState() + + const { setAppContext } = useAppContext() + const router = useRouter() + + const handleMoreLikeThisClick = (prompt: string) => { + setAppContext((prevContext) => { + if (prevContext) return { ...prevContext, promptToGenerateImage: prompt, promptToGenerateVideo: '' } + else return { ...appContextDataDefault, promptToGenerateImage: prompt, promptToGenerateVideo: '' } + }) + } + const handleEditClick = (imageGcsURI: string) => { + setAppContext((prevContext) => { + if (prevContext) return { ...prevContext, imageToEdit: imageGcsURI } + else return { ...appContextDataDefault, imageToEdit: imageGcsURI } + }) + router.push('/edit') + } + const handleITVClick = (imageGcsURI: string) => { + setAppContext((prevContext) => { + if (prevContext) return { ...prevContext, imageToVideo: imageGcsURI } + else return { ...appContextDataDefault, imageToVideo: imageGcsURI } + }) + router.push('/generate') + } + + return ( + <> + + {isLoading ? ( + + ) : ( + 1 ? 2 : 1} + gap={8} + sx={{ cursor: 'pointer', '&.MuiImageList-root': { pb: 5, px: 1 } }} + > + {generatedImagesInGCS.map((image) => + image.src ? ( + + {image.altText}) => { + event.preventDefault() + }} + /> + setImageFullScreen(image)} + > + + Click to see full screen + + + + { + // If there is a replayable prompt, display the "More like this" button + isPromptReplayAvailable && !image.prompt.includes('[1]') && ( + + handleMoreLikeThisClick(image.prompt)} + aria-label="More like this!" + sx={{ pr: 0.2, zIndex: 10 }} + disableRipple + > + + + + + + ) + } + {process.env.NEXT_PUBLIC_EDIT_ENABLED === 'true' && ( + + handleEditClick(image.gcsUri)} + aria-label="Edit image" + sx={{ px: 0.2, zIndex: 10 }} + disableRipple + > + + + + + + )} + {process.env.NEXT_PUBLIC_VEO_ENABLED === 'true' && + process.env.NEXT_PUBLIC_VEO_ITV_ENABLED === 'true' && ( + + handleITVClick(image.gcsUri)} + aria-label="Image to video" + sx={{ px: 0.2, zIndex: 10 }} + disableRipple + > + + + + + + )} + + setImageToExport(image)} + aria-label="Export image" + sx={{ px: 0.2, zIndex: 10 }} + disableRipple + > + + + + + + + setImageToDL(image)} + aria-label="Download image" + sx={{ pr: 1, pl: 0.2, zIndex: 10 }} + disableRipple + > + + + + + + + } + /> + + ) : null + )} + + )} + + {imageFullScreen !== undefined && ( + setImageFullScreen(undefined)} + sx={{ + display: 'flex', + alignContent: 'center', + justifyContent: 'center', + m: 5, + cursor: 'pointer', + maxHeight: '90vh', + maxWidth: '90vw', + }} + disableAutoFocus={true} + > + {'displayed-image'} setImageFullScreen(undefined)} + onContextMenu={(event: React.MouseEvent) => { + event.preventDefault() + }} + /> + + )} + setImageToExport(undefined)} + /> + setImageToDL(undefined)} + /> + + ) +} diff --git a/src/app/ui/transverse-components/SideNavigation.tsx b/src/app/ui/transverse-components/SideNavigation.tsx new file mode 100644 index 00000000..227a4314 --- /dev/null +++ b/src/app/ui/transverse-components/SideNavigation.tsx @@ -0,0 +1,182 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use client' + +import * as React from 'react' +import { usePathname, useRouter } from 'next/navigation' + +import { Drawer, List, ListItem, Typography, ListItemButton, Stack, IconButton, Box } from '@mui/material' + +import Image from 'next/image' +import icon from '../../../public/ImgStudioLogoReversedMini.svg' +import { pages } from '../../routes' + +import theme from '../../theme' +import { useState } from 'react' +import { ChevronLeft, ChevronRight } from '@mui/icons-material' +const { palette } = theme + +const drawerWidth = 265 +const drawerWidthClosed = 75 + +const CustomizedDrawer = { + background: palette.background.paper, + width: drawerWidth, + flexShrink: 0, + '& .MuiDrawer-paperAnchorLeft': { + width: drawerWidth, + border: 0, + }, +} + +const CustomizedDrawerClosed = { + background: palette.background.paper, + width: drawerWidthClosed, + flexShrink: 0, + '& .MuiDrawer-paperAnchorLeft': { + width: drawerWidthClosed, + border: 0, + }, +} + +const CustomizedMenuItem = { + px: 3, + py: 2, + '&:hover': { bgcolor: 'rgba(0,0,0,0.25)' }, + '&.Mui-selected, &.Mui-selected:hover': { + bgcolor: 'rgba(0,0,0,0.5)', + }, + '&.Mui-disabled': { bgcolor: 'transparent' }, + '&:hover, &.Mui-selected, &.Mui-selected:hover, &.Mui-disabled': { + transition: 'none', + }, +} + +export default function SideNav() { + const router = useRouter() + const pathname = usePathname() + + const [open, setOpen] = useState(true) + + return ( + + {!open && ( + setOpen(!open)} + sx={{ + pt: 6, + cursor: 'pointer', + }} + > + ImgStudio + + )} + {open && ( + + setOpen(!open)} sx={{ px: 2.5, pt: 2, cursor: 'pointer' }}> + ImgStudio + + + {Object.values(pages).map(({ name, description, href, status }) => ( + router.push(href)} + sx={CustomizedMenuItem} + > + + + + {name} + + + {status == 'false' ? '/ SOON' : ''} + + + + {description} + + + + ))} + + )} + + {open && ( + + / Made with by + + @Agathe + + + )} + setOpen(!open)} + sx={{ + position: 'absolute', + bottom: 5, + p: 0, + right: 15, + fontSize: '0.6rem', + fontWeight: 400, + color: palette.secondary.light, + }} + > + {open ? : } + + + ) +} diff --git a/src/app/ui/transverse-components/VeoOutputVideosDisplay.tsx b/src/app/ui/transverse-components/VeoOutputVideosDisplay.tsx new file mode 100644 index 00000000..0869100f --- /dev/null +++ b/src/app/ui/transverse-components/VeoOutputVideosDisplay.tsx @@ -0,0 +1,284 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use client' + +import * as React from 'react' +import { useRef, useState } from 'react' + +import { CreateNewFolderRounded, Download, FileUpload, PlayArrowRounded } from '@mui/icons-material' // Removed Edit icon + +import { + Avatar, + Box, + IconButton, + Modal, + Skeleton, + ImageListItem, + ImageList, + ImageListItemBar, + Stack, + CircularProgress, +} from '@mui/material' + +import { VideoI } from '../../api/generate-video-utils' +import { CustomizedAvatarButton, CustomizedIconButton } from '../ux-components/Button-SX' +import ExportStepper, { downloadBase64Media } from './ExportDialog' + +import theme from '../../theme' +import { CustomWhiteTooltip } from '../ux-components/Tooltip' +import { downloadMediaFromGcs } from '@/app/api/cloud-storage/action' +const { palette } = theme + +export default function OutputVideosDisplay({ + isLoading, + generatedVideosInGCS, + generatedCount, +}: { + isLoading: boolean + generatedVideosInGCS: VideoI[] + generatedCount: number +}) { + // State for full screen video display + const [videoFullScreen, setVideoFullScreen] = useState() + const handleOpenVideoFullScreen = (video: VideoI) => setVideoFullScreen(video) + const handleCloseVideoFullScreen = () => { + if (fullScreenVideoRef.current) { + fullScreenVideoRef.current.pause() + fullScreenVideoRef.current.currentTime = 0 + } + setVideoFullScreen(undefined) + } + const handleContextMenuVideo = (event: React.MouseEvent) => { + event.preventDefault() + } + + // Create a ref for the full-screen video element + const fullScreenVideoRef = useRef(null) + + // State for export form and handlers + const [videoExportOpen, setVideoExportOpen] = useState(false) + const [videoToExport, setVideoToExport] = useState() + const handleVideoExportOpen = (video: VideoI) => { + setVideoToExport(video) + setVideoExportOpen(true) + } + const handleVideoExportClose = () => { + setVideoToExport(undefined) + setVideoExportOpen(false) + } + + const [isDLloading, setIsDLloading] = useState(false) + const handleDLvideo = async (video: VideoI) => { + setIsDLloading(true) + try { + const res = await downloadMediaFromGcs(video.gcsUri) + const name = `${video.key}.${video.format.toLowerCase()}` + downloadBase64Media(res.data, name, video.format) + + if (typeof res === 'object' && res.error) throw Error(res.error.replaceAll('Error: ', '')) + } catch (error: any) { + throw Error(error) + } finally { + setIsDLloading(false) + } + } + + return ( + <> + + {isLoading ? ( + + ) : ( + 1 ? 2 : 1} + gap={12} + sx={{ cursor: 'pointer', '&.MuiImageList-root': { pb: 5, px: 1 } }} + > + {generatedVideosInGCS.map((video) => + video.src ? ( + + + ) : null + )} + + )} + + + {videoFullScreen !== undefined && ( + + e.stopPropagation()} + > + + + )} + + + ) +} diff --git a/src/app/ui/ux-components/Accordion-SX.tsx b/src/app/ui/ux-components/Accordion-SX.tsx new file mode 100644 index 00000000..47b09dc7 --- /dev/null +++ b/src/app/ui/ux-components/Accordion-SX.tsx @@ -0,0 +1,47 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Height } from '@mui/icons-material' +import theme from '../../theme' +const { palette } = theme + +export const CustomizedAccordion = { + py: 0.5, + bgcolor: 'transparent', + boxShadow: 0, + flexWrap: 'wrap', + '&:before': { + display: 'none', + }, + '&.Mui-expanded': { + borderRadius: 1, + border: 1, + borderWidth: 1, + borderColor: palette.secondary.light, + minHeight: 0, + margin: 0, + '&:hover': { + borderColor: palette.secondary.main, + }, + }, +} + +export const CustomizedAccordionSummary = { + backgroundColor: palette.background.paper, + color: 'white', + '&.Mui-expanded': { + backgroundColor: 'white', + color: palette.primary.main, + }, +} diff --git a/src/app/ui/ux-components/AudioButton.tsx b/src/app/ui/ux-components/AudioButton.tsx new file mode 100644 index 00000000..911e1c37 --- /dev/null +++ b/src/app/ui/ux-components/AudioButton.tsx @@ -0,0 +1,97 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react' +import { styled } from '@mui/material/styles' +import IconButton from '@mui/material/IconButton' +import { Box, Icon, Switch } from '@mui/material' +import theme from '../../theme' +const { palette } = theme + +const volumeOff = 'https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/volume_off/default/20px.svg' +const volumeOn = 'https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/volume_up/default/20px.svg' + +const MaterialUISwitch = styled(Switch)(({ theme }) => ({ + width: 50, + height: 31, + padding: 8, + border: 1, + + '&.MuiSwitch-root': { + marginLeft: 0, + left: 0, + }, + + '& .MuiSwitch-switchBase': { + margin: 2.5, + padding: 1, + transform: 'translateX(4px)', + '&.Mui-checked': { + transform: 'translateX(15px)', + '& .MuiSwitch-thumb': { + border: 0, + background: 'rgba(33,123,254,0.8)', + '&:before': { + backgroundImage: `url('${volumeOn}')`, + backgroundRepeat: 'no-repeat', + backgroundPosition: 'center', + content: '""', + filter: 'invert(100%)', + }, + }, + '& + .MuiSwitch-track': { + opacity: 1, + backgroundColor: palette.secondary.light, + }, + }, + }, + '& .MuiSwitch-thumb': { + border: `1px solid rgba(0, 0, 0, 0.3)`, + boxShadow: + '0px 2px 1px -1px rgba(0, 0, 0, 0.5), 0px 1px 1px 0px rgba(0, 0, 0, 0.3), 0px 1px 3px 0px rgba(0, 0, 0, 0.2)', + '&:hover': { + background: palette.primary.light, + border: `1px solid rgba(0, 0, 0, 0)`, + }, + width: 24, + height: 24, + '&::before': { + content: "''", + position: 'absolute', + width: '100%', + height: '100%', + left: 0, + top: 0, + backgroundImage: `url('${volumeOff}')`, + backgroundRepeat: 'no-repeat', + backgroundPosition: 'center', + }, + }, + '& .MuiSwitch-track': { + opacity: 1, + backgroundColor: palette.secondary.light, + borderRadius: 20 / 2, + width: '100%', + }, +})) + +export const AudioSwitch = ({ + checked, + onChange, +}: { + checked: boolean + onChange: (event: React.ChangeEvent) => void +}) => { + return +} diff --git a/src/app/ui/ux-components/BlurImage.ts b/src/app/ui/ux-components/BlurImage.ts new file mode 100644 index 00000000..03398ecb --- /dev/null +++ b/src/app/ui/ux-components/BlurImage.ts @@ -0,0 +1,16 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export const blurDataURL = + 'data:image/jpeg;base64,/9j/4Q/+RXhpZgAATU0AKgAAAAgABgESAAMAAAABAAEAAAEaAAUAAAABAAAAVgEbAAUAAAABAAAAXgEoAAMAAAABAAIAAAITAAMAAAABAAEAAIdpAAQAAAABAAAAZgAAAAAAAABIAAAAAQAAAEgAAAABAAeQAAAHAAAABDAyMjGRAQAHAAAABAECAwCgAAAHAAAABDAxMDCgAQADAAAAAQABAACgAgAEAAAAAQAAAgCgAwAEAAAAAQAAAgCkBgADAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9sAhAABAQEBAQECAQECAwICAgMEAwMDAwQGBAQEBAQGBwYGBgYGBgcHBwcHBwcHCAgICAgICQkJCQkLCwsLCwsLCwsLAQICAgMDAwUDAwULCAYICwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwv/3QAEACD/wAARCAIAAgADASIAAhEBAxEB/8QBogAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoLEAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+foBAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKCxEAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD/AD/6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD//0P8AP/ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP//R/wA/+iiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//9L/AD/6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD//0/8AP/ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP//U/wA/+iiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//9X/AD/6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD//1v8AP/ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAor0/w18E/jJ4yVX8JeE9Y1NX6Na2M0q4+qoRivYtM/YX/a31ZQ1r4E1FB/02CQfpIy1y1Mbh6elSpFerSPXwvD+aYlXw2EqTX92En+SPk6ivtZf+Cdf7ZbDI8Ey/8AgXaD/wBrVkX/AOwN+1/poJuPA162P+eUkMv/AKBI1ZLNcE9FXh/4FH/M7pcGcQRV5ZdWS/69T/8AkT5Aor2jxD+zh+0D4URpvEXgjXLSJesj2E3l/wDfWzb+teOz289rM1vco0cicMrDBH4V106tOavTkn6HiYnA4jDPlxFKUH/eTX5ohooorQ5QooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//X/wA/+iiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACitbQtA1zxRq9v4f8NWc2oX10wjht7eMySyMeyqoJP4Cv2E/Zy/4JMeJfEUNv4o/aIvm0a2fDrpNkVa6YeksvKR/7qhjjuprz8wzTC4KHPiJ27Lq/Rf0j6fhjg3N8/rexyug5W3ltCPrLZem/ZH5AeHvDXiLxdq0WgeFLC41K+nOI7e1iaaVz7IgJP5V+j3wh/4JU/tD+P0i1Lx3Ja+ELF8HF0fPu9p7iGM4H0eRD7V/QX8K/gj8KPgnow0L4XaFa6RCQA7xJmaXH/PSVsu5/wB5jXqlfn+Ycc1p+7g4cq7vV/dsvxP6Y4Y+jtl9BRq55XdWX8kPdh6X+J/Lk9D8yfhr/wAEpP2aPByx3PjI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n/8AJH9AH/D8a1/6Jk3/AIOB/wDIlH/D8a1/6Jk3/g4H/wAiV/P/AEV7n/EVuKf+gv8A8p0//kC/9SMl/wCfH/k0v8z+gD/h+Na/9Eyb/wAHA/8AkSj/AIfjWv8A0TJv/BwP/kSv5/6KP+IrcU/9Bf8A5Tp//IB/qRkv/Pj/AMml/mf0Af8AD8a1/wCiZN/4OB/8iUf8PxrX/omTf+Dgf/Ilfz/0Uf8AEVuKf+gv/wAp0/8A5AP9SMl/58f+TS/zP6AP+H41r/0TJv8AwcD/AORKzrz/AILi3jx7dP8AhmkbdjJq5cfkLRf51+CFFJ+KvFLVvrn/AJJT/wDkBrgnJV/y4/8AJpf/ACR+ynif/gtT8fNQRovCfhjQ9NDcBphNcuv0xJGv5r+FfIvxD/4KK/ti/ElJLXU/Gdzp1s/Hk6WqWIA9N8KrJ+bmviWivFx3G+fYtctfGzt2T5V90bI9DDcOZZQ1pYeP3X/O5e1LVNT1q+k1TWLiW7uZjuklmcu7H1LHk1Roor5dtt3Z7KVtEFFFFIYUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAH//1/8AP/ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP//Q/wA/+iiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//9H/AD/6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD//0v8AP/ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP//T/wA/+iiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//9T/AD/6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD//1f8AP/ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP//W/wA/+iiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//9k=' diff --git a/src/app/ui/ux-components/Button-SX.tsx b/src/app/ui/ux-components/Button-SX.tsx new file mode 100644 index 00000000..1ad7aa01 --- /dev/null +++ b/src/app/ui/ux-components/Button-SX.tsx @@ -0,0 +1,66 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import theme from '../../theme' +const { palette } = theme + +const onHover = { + bgcolor: palette.primary.main, + color: 'white', + border: `0.5px solid rgba(0, 0, 0, 0)`, + boxShadow: 0, +} + +export const CustomizedIconButton = { + fontSize: '17px', + color: 'black', + position: 'center', + pt: 0.1, + pr: 0, + '&:hover': onHover, +} + +export const CustomizedAvatarButton = { + width: 24, + height: 24, + ml: 0.2, + bgcolor: 'rgba(255,255,255,0.9)', + border: `1px solid rgba(0, 0, 0, 0.3)`, + '&:hover': onHover, + boxShadow: + '0px 2px 1px -1px rgba(0, 0, 0, 0.5), 0px 1px 1px 0px rgba(0, 0, 0, 0.3), 0px 1px 3px 0px rgba(0, 0, 0, 0.2)', +} + +export const CustomizedIconButtonOpen = { + ...onHover, +} + +export const CustomizedSendButton = { + borderRadius: 7, + border: 1, + borderColor: 'white', + boxShadow: 0, + my: 1.5, + ml: 1, + py: 0.5, + pr: 1.5, + fontSize: '1rem', + '&:hover': { + background: 'white', + color: palette.primary.main, + border: 1, + borderColor: palette.primary.main, + boxShadow: 0, + }, +} diff --git a/src/app/ui/ux-components/GeminiButton.tsx b/src/app/ui/ux-components/GeminiButton.tsx new file mode 100644 index 00000000..961fdab9 --- /dev/null +++ b/src/app/ui/ux-components/GeminiButton.tsx @@ -0,0 +1,135 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react' +import { styled } from '@mui/material/styles' +import IconButton from '@mui/material/IconButton' +import { Box, Icon, Switch } from '@mui/material' +import theme from '../../theme' +const { palette } = theme + +export const sparkIcon = 'https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/spark/default/20px.svg' +const sparkOffIcon = 'https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/spark_off/default/20px.svg' + +const MaterialUISwitch = styled(Switch)(({ theme }) => ({ + width: 50, + height: 31, + padding: 8, + border: 1, + + '&.MuiSwitch-root': { + marginLeft: 0, + left: 0, + }, + + '& .MuiSwitch-switchBase': { + margin: 2.5, + padding: 1, + transform: 'translateX(4px)', + '&.Mui-checked': { + color: '#fff', + transform: 'translateX(15px)', + '& .MuiSwitch-thumb': { + border: 0, + background: 'linear-gradient(130deg, rgba(33,123,254,0.9) 10%, rgba(172,135,235,0.9) 70%)', + + '&:before': { + backgroundImage: `url('${sparkIcon}')`, + backgroundRepeat: 'no-repeat', + backgroundPosition: 'center', + content: '""', + }, + }, + '& + .MuiSwitch-track': { + opacity: 1, + backgroundColor: palette.secondary.light, + }, + }, + }, + '& .MuiSwitch-thumb': { + background: 'rgba(255,255,255,0.9)', + border: `1px solid rgba(0, 0, 0, 0.3)`, + boxShadow: + '0px 2px 1px -1px rgba(0, 0, 0, 0.5), 0px 1px 1px 0px rgba(0, 0, 0, 0.3), 0px 1px 3px 0px rgba(0, 0, 0, 0.2)', + '&:hover': { + background: palette.primary.light, + border: `1px solid rgba(0, 0, 0, 0)`, + }, + width: 24, + height: 24, + '&::before': { + content: "''", + position: 'absolute', + width: '100%', + height: '100%', + left: 0, + top: 0, + backgroundImage: `url('${sparkOffIcon}')`, + backgroundRepeat: 'no-repeat', + backgroundPosition: 'center', + }, + }, + '& .MuiSwitch-track': { + opacity: 1, + backgroundColor: palette.secondary.light, + borderRadius: 20 / 2, + width: '100%', + }, +})) + +export const GeminiSwitch = ({ + checked, + onChange, +}: { + checked: boolean + onChange: (event: React.ChangeEvent) => void +}) => { + return +} + +export const GeminiButton = ({ onClick }: { onClick: () => void }) => { + return ( + + + + + + ) +} diff --git a/src/app/ui/ux-components/GoogleSignInButton.module.css b/src/app/ui/ux-components/GoogleSignInButton.module.css new file mode 100644 index 00000000..4a0514ed --- /dev/null +++ b/src/app/ui/ux-components/GoogleSignInButton.module.css @@ -0,0 +1,115 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +.button { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + -webkit-appearance: none; + background-color: WHITE; + background-image: none; + border: 1px solid #747775; + -webkit-border-radius: 20px; + border-radius: 20px; + -webkit-box-sizing: border-box; + box-sizing: border-box; + color: #1f1f1f; + cursor: pointer; + font-family: 'Roboto'; + font-size: 14px; + height: 40px; + letter-spacing: 0.25px; + outline: none; + overflow: hidden; + padding: 0 12px; + position: relative; + text-align: center; + -webkit-transition: background-color 0.218s, border-color 0.218s, + box-shadow 0.218s; + transition: background-color 0.218s, border-color 0.218s, box-shadow 0.218s; + vertical-align: middle; + white-space: nowrap; + width: auto; + max-width: 400px; + min-width: min-content; +} + +.button .buttonicon { + height: 20px; + margin-right: 12px; +} + +.button .buttoncontentwrapper { + -webkit-align-items: center; + align-items: center; + display: flex; + -webkit-flex-direction: row; + flex-direction: row; + -webkit-flex-wrap: nowrap; + flex-wrap: nowrap; + height: 100%; + justify-content: space-between; + position: relative; + width: 100%; +} + +.button .buttoncontents { + -webkit-flex-grow: 1; + flex-grow: 1; + font-family: 'Roboto'; + font-weight: 400; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: top; +} + +.button .buttonstate { + -webkit-transition: opacity 0.218s; + transition: opacity 0.218s; + bottom: 0; + left: 0; + opacity: 0; + position: absolute; + right: 0; + top: 0; +} + +.button:disabled { + cursor: default; + background-color: #ffffff61; + border-color: #1f1f1f1f; +} + +.button:disabled .buttoncontents { + opacity: 38%; +} + +.button:disabled .buttonicon { + opacity: 38%; +} + +.button:not(:disabled):active .buttonstate, +.button:not(:disabled):focus .buttonstate { + background-color: #303030; + opacity: 12%; + font-weight: 500; +} + +.button:not(:disabled):hover .buttonstate { + background-color: #303030; + opacity: 8%; + font-weight: 500; +} diff --git a/src/app/ui/ux-components/GoogleSignInButton.tsx b/src/app/ui/ux-components/GoogleSignInButton.tsx new file mode 100644 index 00000000..7ff8ce90 --- /dev/null +++ b/src/app/ui/ux-components/GoogleSignInButton.tsx @@ -0,0 +1,31 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import icon from '../../../public/cloudicon.svg' +import Image from 'next/image' +import styles from './GoogleSignInButton.module.css' + +export default function GoogleSignInButton({ onClick }: { onClick: () => void }) { + return ( + + ) +} diff --git a/src/app/ui/ux-components/InputChipGroup.tsx b/src/app/ui/ux-components/InputChipGroup.tsx new file mode 100644 index 00000000..e1b7e7ce --- /dev/null +++ b/src/app/ui/ux-components/InputChipGroup.tsx @@ -0,0 +1,166 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react' +import { Controller } from 'react-hook-form' +import { Chip, Stack, Box, Typography, FormControl } from '@mui/material' + +import theme from '../../theme' +const { palette } = theme + +import { FormChipGroupInputI } from './InputInterface' + +const CustomizedChip = { + fontSize: '0.85rem', + mb: 0.2, + border: 1, + borderColor: palette.secondary.light, + letterSpacing: '0.06px', + '&:hover': { + borderColor: palette.primary.main, + bgcolor: palette.primary.main, + transition: 'none', + color: palette.text.primary, + fontWeight: 500, + letterSpacing: '0px', + px: 0.05, + }, + '&:active': { + boxShadow: 0, + }, + '&.MuiChip-filled': { + color: 'white', + letterSpacing: '0.05px', + '&:hover': { + letterSpacing: '0px', + }, + }, + '& .MuiChip-label': { + px: 0.5, + }, +} + +export const ChipGroup = ({ + width, + label, + required, + options, + value, + onChange, + handleChipClick, + disabled, + weight, +}: { + width: string + label?: string + required: boolean + options: string[] + value: string + onChange: any + handleChipClick: any + disabled?: boolean + weight?: number +}) => { + return ( + + {label !== undefined && ( + + {label + (required ? ' *' : '')} + + )} + + {options.map((chipValue) => ( + handleChipClick({ clickedValue: chipValue, currentValue: value })} + onChange={onChange} + variant={value === chipValue ? 'filled' : 'outlined'} + color={value === chipValue ? 'primary' : 'secondary'} + sx={{ ...CustomizedChip, ...(weight !== undefined ? { fontWeight: weight } : { fontWeight: 400 }) }} + /> + ))} + + + ) +} + +export default function FormInputChipGroup({ + name, + label, + control, + width, + setValue, + field, + required, + disabled, +}: FormChipGroupInputI) { + const handleChipClick = ({ clickedValue, currentValue }: { clickedValue: string; currentValue: string }) => { + required + ? setValue(name, clickedValue) + : clickedValue !== currentValue + ? setValue(name, clickedValue) + : setValue(name, '') + } + + return field !== undefined ? ( + + ( + + )} + /> + + ) : null +} diff --git a/src/app/ui/ux-components/InputChipGroupMultiple.tsx b/src/app/ui/ux-components/InputChipGroupMultiple.tsx new file mode 100644 index 00000000..7c1e44c1 --- /dev/null +++ b/src/app/ui/ux-components/InputChipGroupMultiple.tsx @@ -0,0 +1,127 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react' +import { Controller } from 'react-hook-form' +import { Chip, Stack, Box, Typography } from '@mui/material' + +import theme from '../../theme' +const { palette } = theme + +import { FormChipGroupMultipleInputI } from './InputInterface' + +const CustomizedChip = { + fontSize: '0.9rem', + mb: 0.2, + border: 1, + borderColor: palette.secondary.light, + letterSpacing: '0.05px', + '&:hover': { + borderColor: palette.primary.main, + bgcolor: palette.primary.main, + transition: 'none', + color: palette.text.primary, + fontWeight: 500, + letterSpacing: '0px', + }, + '&:active': { + boxShadow: 0, + }, + '&.MuiChip-filled': { + color: 'white', + letterSpacing: '0.05px', + '&:hover': { + letterSpacing: '0px', + }, + }, + '& .MuiChip-label': { + px: 1, + }, +} + +export default function FormInputChipGroupMultiple({ + name, + label, + control, + width, + setValue, + options, + required, +}: FormChipGroupMultipleInputI) { + const handleChipClick = (clickedValue: string, value: string[]) => { + const newValue = value.includes(clickedValue) + ? required && value.length === 1 // Prevent removal if required and it's the last item + ? value + : value.filter((v) => v !== clickedValue) + : [...value, clickedValue] + + setValue(name, newValue) + } + + return ( + ( + + + {label + (required ? ' *' : '')} + + + + {options && + options.map((chip) => ( + handleChipClick(chip.value, value)} + onChange={onChange} + variant={value.includes(chip.value) ? 'filled' : 'outlined'} + color={value.includes(chip.value) ? 'primary' : 'secondary'} + sx={CustomizedChip} + /> + ))} + + + )} + /> + ) +} diff --git a/src/app/ui/ux-components/InputDropdown.tsx b/src/app/ui/ux-components/InputDropdown.tsx new file mode 100644 index 00000000..ad074337 --- /dev/null +++ b/src/app/ui/ux-components/InputDropdown.tsx @@ -0,0 +1,154 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react' +import { useState } from 'react' +import { Controller } from 'react-hook-form' +import { FormDropdownInputI } from './InputInterface' + +import { TextField, MenuItem, FormControl } from '@mui/material' + +import theme from '../../theme' +import CustomTooltip from './Tooltip' +const { palette } = theme + +const CustomizedInput = (styleSize: string) => { + var style = { color: palette.primary.main } + if (styleSize === 'big') { + style = { ...style, ...{ fontWeight: 700, fontSize: '2.5rem', pr: 2 } } + } + if (styleSize === 'small') { + style = { ...style, ...{ fontWeight: 400, fontSize: '1rem', pr: 0.5 } } + } + + return style +} + +const CustomizedTextField = (styleSize: string, width: string) => { + var customizedFont + var customizedWidth = {} + if (styleSize === 'big') { + customizedFont = '37px' + } + if (styleSize === 'small') { + ;(customizedFont = '24px'), (customizedWidth = { width: width }) + } + + return { + ...{ + '& .MuiSvgIcon-root': { + color: palette.text.secondary, + fontSize: customizedFont, + }, + }, + ...customizedWidth, + } +} + +const CustomizedSelected = { + background: 'transparent', + color: palette.primary.main, + fontWeight: 500, + '&:hover &:active': { background: 'transparent' }, +} + +const CustomizedMenu = { + sx: { + '& .MuiPaper-root': { + background: 'white', + color: palette.text.primary, + boxShadow: 1, + '& .MuiMenu-list': { + pt: 0.5, + pb: 1, + background: 'transparent', + }, + '& .MuiMenuItem-root': { + background: 'transparent', + py: 0.5, + '&:hover': { + fontWeight: 500, + pl: 2.5, + }, + '&.Mui-selected': CustomizedSelected, + }, + }, + }, +} + +const CustomizedMenuItem = { + '&.Mui-selected': CustomizedSelected, +} + +export default function FormInputDropdown({ + styleSize, + width, + name, + control, + label, + field, + required, +}: FormDropdownInputI) { + const [selectedItem, setSelectedItem] = useState(String) + const [itemIndication, setItemIndication] = useState(String) + + const handleClick = (value: string, indication: string | undefined) => { + setSelectedItem(value) + setItemIndication(indication !== undefined ? indication : '') + } + + return ( + + + ( + + {field.options.map((field: { value: string; label: string; indication?: string }) => { + return ( + handleClick(field.value, field.indication)} + key={field.value} + value={field.value} + selected={selectedItem === field.value} + sx={CustomizedMenuItem} + > + {field.label} + + ) + })} + + )} + control={control} + name={name} + rules={{ required: required }} + /> + + + ) +} diff --git a/src/app/ui/ux-components/InputDropdownMultiple.tsx b/src/app/ui/ux-components/InputDropdownMultiple.tsx new file mode 100644 index 00000000..87c13a7a --- /dev/null +++ b/src/app/ui/ux-components/InputDropdownMultiple.tsx @@ -0,0 +1,136 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react' +import { useState } from 'react' + +import { TextField, MenuItem, ListItemText, OutlinedInput, IconButton, Stack } from '@mui/material' + +import theme from '../../theme' +import { semanticClasses } from '@/app/api/edit-utils' +import { Autorenew } from '@mui/icons-material' +const { palette } = theme + +const CustomizedInput = { color: palette.primary.main, fontWeight: 400, fontSize: '1rem', pr: 0.5 } + +const CustomizedTextField = (width: string) => { + return { + ...{ + '& .MuiSvgIcon-root': { + color: palette.text.secondary, + fontSize: '1.5rem', + }, + }, + width: width, + color: 'red', + } +} + +const CustomizedSelected = { + background: 'transparent', + color: palette.primary.main, + fontWeight: 500, + '&:hover &:active': { + backgroundColor: palette.primary.dark, + }, +} + +const CustomizedMenu = { + sx: { + transformOrigin: { + vertical: 'top', + horizontal: 'center', + }, + '& .MuiPaper-root': { + background: 'white', + color: palette.text.primary, + boxShadow: 1, + height: 200, + '& .MuiMenu-list': { + pt: 0.5, + pb: 1, + background: 'transparent', + }, + '& .MuiMenuItem-root': { + background: 'transparent', + py: 0.5, + '&:hover': { + fontWeight: 500, + pl: 2.5, + }, + '&.Mui-selected': CustomizedSelected, + }, + }, + }, +} + +const CustomizedMenuItem = { + '&.Mui-selected': CustomizedSelected, +} + +export default function FormInputDropdownMultiple({ + width, + label, + selectedItems, + handleSelect, + handleReset, +}: { + width: string + label: string + selectedItems: string[] + handleSelect: any + handleReset: any +}) { + return ( + + { + const selectedItems = value as string[] + if (selectedItems.length === 0) return <>{label} + else return selectedItems.join(', ') + }, + input: , + }} + InputProps={{ sx: CustomizedInput }} + sx={CustomizedTextField(width)} + > + {semanticClasses.map((option) => ( + + + + ))} + + handleReset()} aria-label="Reset form" disableRipple sx={{ px: 0 }}> + + + + ) +} diff --git a/src/app/ui/ux-components/InputInterface.tsx b/src/app/ui/ux-components/InputInterface.tsx new file mode 100644 index 00000000..b1e0c8ae --- /dev/null +++ b/src/app/ui/ux-components/InputInterface.tsx @@ -0,0 +1,72 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { advancedSettingsI, chipGroupFieldsI, generalSettingsI, selectFieldsI } from '../../api/generate-image-utils' + +export interface FormTextInputI { + name: string + label: string + control: any + required: boolean + rows: number + promptIndication?: string +} + +export interface FormDropdownInputI { + name: string + label: string + control: any + styleSize: string + width: string + setValue?: any + field: selectFieldsI + required: boolean +} + +export interface FormChipGroupInputI { + name: string + label: string + control: any + width: string + setValue?: any + field?: chipGroupFieldsI + required: boolean + disabled?: boolean +} + +export interface FormChipGroupMultipleInputI { + name: string + label: string + control: any + width: string + setValue?: any + options?: { value: string; label: string }[] + required: boolean +} + +export interface GenerateSettingsI { + control: any + setValue?: any + generalSettingsFields: generalSettingsI + advancedSettingsFields: advancedSettingsI + warningMessage?: string +} + +export interface FormInputRadioButtonI { + label: string + subLabel: string + value: string + currentSelectedValue: string + enabled: boolean +} diff --git a/src/app/ui/ux-components/InputRadioButton.tsx b/src/app/ui/ux-components/InputRadioButton.tsx new file mode 100644 index 00000000..d22bc388 --- /dev/null +++ b/src/app/ui/ux-components/InputRadioButton.tsx @@ -0,0 +1,95 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CheckCircle, RadioButtonUnchecked } from '@mui/icons-material' +import { FormControlLabel, Radio, RadioProps, Typography } from '@mui/material' + +import theme from '../../theme' +import { FormInputRadioButtonI } from './InputInterface' +const { palette } = theme + +export const CustomRadioButton = (props: RadioProps) => { + return ( + } + icon={} + sx={{ + py: 1.5, + '&:hover': { + color: palette.primary.main, + backgroundColor: 'transparent', + cursor: 'pointer', + }, + }} + /> + ) +} + +export const CustomRadioLabel = ( + value: string, + label: string, + subLabel: string, + currentSelectedValue: string, + enabled: boolean +) => { + return ( + <> + + {label} + + + {subLabel} + + + ) +} + +export const CustomRadio = ({ label, subLabel, value, currentSelectedValue, enabled }: FormInputRadioButtonI) => { + return ( + } + label={CustomRadioLabel(value, label, subLabel, currentSelectedValue, enabled)} + disabled={!enabled} + /> + ) +} diff --git a/src/app/ui/ux-components/InputSlider.tsx b/src/app/ui/ux-components/InputSlider.tsx new file mode 100644 index 00000000..17300b3f --- /dev/null +++ b/src/app/ui/ux-components/InputSlider.tsx @@ -0,0 +1,122 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react' +import { Controller } from 'react-hook-form' +import { Box, Slider, TextField, Typography } from '@mui/material' +import { FormTextInputI } from './InputInterface' +import theme from '../../theme' +import { EditImageFieldStyleI } from '@/app/api/edit-utils' +const { palette } = theme + +export const CustomSlider = ({ + label, + value, + onChange, + min, + max, + step, +}: { + label: string + value: number + onChange: any + min: number + max: number + step: number +}) => { + return ( + + + {label} + + + + {value} + + + ) +} + +export const FormInputSlider = ({ + name, + control, + field, + required, +}: { + name: string + control: any + field: EditImageFieldStyleI + required: boolean +}) => { + return ( + ( + + )} + /> + ) +} diff --git a/src/app/ui/ux-components/InputText.tsx b/src/app/ui/ux-components/InputText.tsx new file mode 100644 index 00000000..6c46064c --- /dev/null +++ b/src/app/ui/ux-components/InputText.tsx @@ -0,0 +1,59 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react' +import { Controller } from 'react-hook-form' +import { TextField } from '@mui/material' +import { FormTextInputI } from './InputInterface' +import theme from '../../theme' +const { palette } = theme + +export const FormInputText = ({ name, control, label, required, rows, promptIndication }: FormTextInputI) => { + return ( + ( + + )} + /> + ) +} diff --git a/src/app/ui/ux-components/InputTextLine.tsx b/src/app/ui/ux-components/InputTextLine.tsx new file mode 100644 index 00000000..980a1fb8 --- /dev/null +++ b/src/app/ui/ux-components/InputTextLine.tsx @@ -0,0 +1,76 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React, { useEffect, useRef } from 'react' +import { Controller } from 'react-hook-form' +import { Box, IconButton, TextField } from '@mui/material' +import theme from '../../theme' +import { Close, Height } from '@mui/icons-material' +const { palette } = theme + +const customTextField = { + '& .MuiInputBase-root': { + fontSize: '1rem', + color: palette.primary.main, + lineHeight: '110%', + }, +} + +export const FormInputTextLine = ({ + name, + value, + control, + label, + required, + disabled, + key, +}: { + name: string + value: string + control: any + label: string + required: boolean + disabled?: boolean + key: string +}) => { + return ( + ( + + )} + /> + ) +} diff --git a/src/app/ui/ux-components/InputTextSmall.tsx b/src/app/ui/ux-components/InputTextSmall.tsx new file mode 100644 index 00000000..f64a0778 --- /dev/null +++ b/src/app/ui/ux-components/InputTextSmall.tsx @@ -0,0 +1,40 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react' +import { Controller } from 'react-hook-form' +import { TextField } from '@mui/material' +import { FormTextInputI } from './InputInterface' + +export const FormInputTextSmall = ({ name, control }: FormTextInputI) => { + return ( + ( + + )} + /> + ) +} diff --git a/src/app/ui/ux-components/Tooltip.tsx b/src/app/ui/ux-components/Tooltip.tsx new file mode 100644 index 00000000..41c77ade --- /dev/null +++ b/src/app/ui/ux-components/Tooltip.tsx @@ -0,0 +1,160 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react' +import { Box, Fade, Tooltip } from '@mui/material' +import theme from '../../theme' +const { palette } = theme + +const CustomizedSmallTooltip = { + sx: { + '& .MuiTooltip-tooltip': { + backgroundColor: 'transparent', + color: palette.text.primary, + width: 85, + fontWeight: 400, + fontSize: 12, + lineHeight: 0.9, + pt: 1, + textAlign: 'center', + }, + }, + modifiers: [ + { + name: 'offset', + options: { offset: [1, -35] }, + }, + ], +} + +const CustomizedSmallWhiteTooltip = { + sx: { + '& .MuiTooltip-tooltip': { + backgroundColor: 'white', + color: palette.text.primary, + width: 80, + fontWeight: 400, + fontSize: 12, + lineHeight: 0.9, + textAlign: 'center', + }, + }, + modifiers: [ + { + name: 'offset', + options: { offset: [1, -17] }, + }, + ], +} + +const CustomizedBigTooltip = { + sx: { + '& .MuiTooltip-tooltip': { + backgroundColor: 'transparent', + color: palette.text.primary, + }, + }, + modifiers: [ + { + name: 'offset', + options: { offset: [-10, -25] }, + }, + ], +} + +export default function CustomTooltip({ + children, + title, + size, +}: { + children: React.ReactElement + title: string + size: string +}) { + const [open, setOpen] = React.useState(false) + + const handleTooltipOpen = () => { + setOpen(true) + } + + const handleTooltipClose = () => { + setOpen(false) + } + + return ( + + + {children ? children : null} + + + ) +} + +export function CustomWhiteTooltip({ + children, + title, + size, +}: { + children: React.ReactElement + title: string + size: string +}) { + const [open, setOpen] = React.useState(false) + + const handleTooltipOpen = () => { + setOpen(true) + } + + const handleTooltipClose = () => { + setOpen(false) + } + + return ( + + + {children ? children : null} + + + ) +} diff --git a/src/generate-third-party.js b/src/generate-third-party.js new file mode 100644 index 00000000..10df360f --- /dev/null +++ b/src/generate-third-party.js @@ -0,0 +1,129 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const thirdPartyDir = path.join(__dirname, 'third_party'); + +// List of built-in Node.js modules (add more if needed) +const builtInModules = ['fs', 'path', 'http', 'https', 'os', 'crypto', 'zlib', 'events', 'stream', 'url', 'util']; + +// Function to create the third_party directory if it doesn't exist +function createThirdPartyDir() { + if (!fs.existsSync(thirdPartyDir)) { + fs.mkdirSync(thirdPartyDir); + } +} + +// Function to copy the license file and create the METADATA file +function processDependency(name, packageJsonPath) { + // Ignore built-in modules + if (builtInModules.includes(name)) { + return false; // Indicates that the dependency was skipped + } + + const packageJson = require(packageJsonPath); + const version = packageJson.version; + const homepage = packageJson.homepage || (packageJson.repository ? (typeof packageJson.repository === 'string' ? packageJson.repository : packageJson.repository.url) : null); + const licenseType = packageJson.license || 'License Not Found'; + + const licensePath = path.join(path.dirname(packageJsonPath), 'LICENSE'); + const readmePath = path.join(path.dirname(packageJsonPath), 'README.md'); + + // Handle scoped package names (e.g., @types/react) + const [scope, packageName] = name.startsWith('@') ? name.split('/') : [null, name]; + const dependencyDir = scope + ? path.join(thirdPartyDir, scope, packageName) + : path.join(thirdPartyDir, name); + + // Create the dependency directory if it doesn't exist + if (!fs.existsSync(dependencyDir)) { + fs.mkdirSync(dependencyDir, { recursive: true }); + } + + // Copy the LICENSE file if it exists + if (fs.existsSync(licensePath)) { + fs.copyFileSync(licensePath, path.join(dependencyDir, 'LICENSE')); + } else { + console.warn(`LICENSE not found for ${name}`); + fs.writeFileSync(path.join(dependencyDir, 'LICENSE'), `${licenseType} - see package.json`); + } + + // Create the METADATA file + const metadataContent = `Name: ${name} +Version: ${version} +Homepage: ${homepage || 'Not Found'} +License: ${licenseType} + +This package was sourced from npm. +`; + fs.writeFileSync(path.join(dependencyDir, 'METADATA'), metadataContent); + + return true; // Indicates that the dependency was processed +} + +// Main function +function main() { + createThirdPartyDir(); + + // Get the list of installed dependencies using pnpm + const dependenciesListOutput = execSync('pnpm ls --json --long', { encoding: 'utf-8' }); + const dependenciesList = JSON.parse(dependenciesListOutput); + + // Counter for processed dependencies + let processedDependenciesCount = 0; + let totalDependenciesCount = 0; + let dependencies = {}; + let failedDependencies = []; // List for failed dependencies + + // Iterate over the dependencies. Supports projects with one or more packages + const packages = dependenciesList.length ? dependenciesList : [{ dependencies: dependenciesList }] + + packages.forEach(project => { + if (!project.dependencies) { + console.warn('No dependencies found.'); + return; + } + + // Merge dependencies from all packages + dependencies = { ...dependencies, ...project.dependencies }; + }); + + totalDependenciesCount = Object.keys(dependencies).length; + + for (const name in dependencies) { + const packageJsonPath = path.join(__dirname, 'node_modules', name, 'package.json'); + const dependencyProcessed = processDependency(name, packageJsonPath); + if (dependencyProcessed) { + processedDependenciesCount++; + } else { + failedDependencies.push(name); // Add the dependency to the failed list + } + } + + // Global confirmation message + if (processedDependenciesCount === (totalDependenciesCount - builtInModules.filter(module => dependencies.hasOwnProperty(module)).length)) { + console.log('All dependencies have been successfully processed and added to the `third_party` directory.'); + } else { + console.warn(`Some dependencies might be missing in the \`third_party\` directory.`); + if (failedDependencies.length > 0) { + console.warn("The following dependencies were not processed correctly:"); + failedDependencies.forEach(dep => console.warn(`- ${dep}`)); + } + } +} + +main(); diff --git a/src/next.config.mjs b/src/next.config.mjs new file mode 100644 index 00000000..4b287d42 --- /dev/null +++ b/src/next.config.mjs @@ -0,0 +1,63 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** @type {import('next').NextConfig} */ + +const nextConfig = { + reactStrictMode: false, + staticPageGenerationTimeout: 500, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'storage.googleapis.com', + }, + { + protocol: 'https', + hostname: 'storage.mtls.cloud.google.com', + } + ], + + minimumCacheTTL: 60, // Minimum cache time in seconds (e.g., 1 minute) + deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], // Common device widths + imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], // Sizes for generated images + }, + experimental: { + serverActions: { + bodySizeLimit: '30mb', + }, + }, + webpack: (config, { isServer, webpack }) => { + if (isServer) { + if (!config.externals) { + config.externals = []; + } + const externalsToAdd = ['@ffmpeg-installer/ffmpeg', '@ffprobe-installer/ffprobe']; + for (const ext of externalsToAdd) + if (!config.externals.includes(ext)) + config.externals.push(ext); + + config.plugins.push( + new webpack.IgnorePlugin({ + resourceRegExp: /^(.\/README\.md|.\/types\/.*\.d\.ts|.\/tsconfig\.json)$/, + contextRegExp: /@ffprobe-installer[\\/]ffprobe$/, + }) + ); + } + + return config; + }, +} + +export default nextConfig diff --git a/src/package.json b/src/package.json new file mode 100644 index 00000000..ecfdde04 --- /dev/null +++ b/src/package.json @@ -0,0 +1,60 @@ +{ + "private": true, + "scripts": { + "build": "next build", + "dev": "next dev", + "start": "next start", + "generate-third-party": "node generate-third-party.js" + }, + "dependencies": { + "@emotion/cache": "latest", + "@emotion/react": "latest", + "@emotion/styled": "latest", + "@ffmpeg-installer/ffmpeg": "^1.1.0", + "@ffprobe-installer/ffprobe": "^2.1.2", + "@fontsource/roboto": "^5.1.0", + "@google-cloud/firestore": "^7.10.0", + "@google-cloud/iap": "^3.4.0", + "@google-cloud/storage": "^7.12.1", + "@google-cloud/vertexai": "^1.6.0", + "@mui/icons-material": "^6.1.0", + "@mui/material": "^6.1.0", + "@mui/material-nextjs": "^6.1.0", + "@tailwindcss/forms": "^0.5.7", + "autoprefixer": "^10.4.20", + "fabric": "^6.4.3", + "fluent-ffmpeg": "^2.1.3", + "fs": "0.0.1-security", + "google-auth-library": "^9.13.0", + "net": "^1.0.2", + "next-auth": "5.0.0-beta.19", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-dropzone": "^14.2.10", + "react-hook-form": "^7.52.2", + "react-mask-editor": "^0.0.2", + "react-sketch-canvas": "7.0.0-next.4", + "sharp": "^0.33.5", + "tailwindcss": "3.4.4", + "tls": "^0.0.1" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/fabric": "^5.3.10", + "@types/fluent-ffmpeg": "^2.1.27", + "@types/node": "20.14.8", + "@types/node-fetch": "^2.6.11", + "@types/react": "^18.3.3", + "@types/react-dom": "18.3.0", + "@types/react-window": "^1.8.8", + "html-loader": "^5.1.0", + "install": "^0.13.0", + "next": "^14.2.16", + "npm": "^10.9.2", + "ts-loader": "^9.5.1", + "typescript": "^5.6.2" + }, + "engines": { + "node": ">=20.12.0" + } +} diff --git a/src/pnpm-lock.yaml b/src/pnpm-lock.yaml new file mode 100644 index 00000000..3b90aea5 --- /dev/null +++ b/src/pnpm-lock.yaml @@ -0,0 +1,4972 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@emotion/cache': + specifier: latest + version: 11.13.1 + '@emotion/react': + specifier: latest + version: 11.13.3(@types/react@18.3.3)(react@18.3.1) + '@emotion/styled': + specifier: latest + version: 11.13.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) + '@ffmpeg-installer/ffmpeg': + specifier: ^1.1.0 + version: 1.1.0 + '@ffprobe-installer/ffprobe': + specifier: ^2.1.2 + version: 2.1.2 + '@fontsource/roboto': + specifier: ^5.1.0 + version: 5.1.0 + '@google-cloud/firestore': + specifier: ^7.10.0 + version: 7.10.0 + '@google-cloud/iap': + specifier: ^3.4.0 + version: 3.4.0 + '@google-cloud/storage': + specifier: ^7.12.1 + version: 7.12.1 + '@google-cloud/vertexai': + specifier: ^1.6.0 + version: 1.6.0 + '@mui/icons-material': + specifier: ^6.1.0 + version: 6.1.0(@mui/material@6.1.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) + '@mui/material': + specifier: ^6.1.0 + version: 6.1.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mui/material-nextjs': + specifier: ^6.1.0 + version: 6.1.0(@emotion/cache@11.13.1)(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(next@14.2.16(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@tailwindcss/forms': + specifier: ^0.5.7 + version: 0.5.7(tailwindcss@3.4.4) + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.4.38) + fabric: + specifier: ^6.4.3 + version: 6.4.3 + fluent-ffmpeg: + specifier: ^2.1.3 + version: 2.1.3 + fs: + specifier: 0.0.1-security + version: 0.0.1-security + google-auth-library: + specifier: ^9.13.0 + version: 9.13.0 + net: + specifier: ^1.0.2 + version: 1.0.2 + next-auth: + specifier: 5.0.0-beta.19 + version: 5.0.0-beta.19(next@14.2.16(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-dropzone: + specifier: ^14.2.10 + version: 14.2.10(react@18.3.1) + react-hook-form: + specifier: ^7.52.2 + version: 7.52.2(react@18.3.1) + react-mask-editor: + specifier: ^0.0.2 + version: 0.0.2(react@18.3.1) + react-sketch-canvas: + specifier: 7.0.0-next.4 + version: 7.0.0-next.4(react@18.3.1) + sharp: + specifier: ^0.33.5 + version: 0.33.5 + tailwindcss: + specifier: 3.4.4 + version: 3.4.4 + tls: + specifier: ^0.0.1 + version: 0.0.1 + devDependencies: + '@types/bcrypt': + specifier: ^5.0.2 + version: 5.0.2 + '@types/fabric': + specifier: ^5.3.10 + version: 5.3.10 + '@types/fluent-ffmpeg': + specifier: ^2.1.27 + version: 2.1.27 + '@types/node': + specifier: 20.14.8 + version: 20.14.8 + '@types/node-fetch': + specifier: ^2.6.11 + version: 2.6.11 + '@types/react': + specifier: ^18.3.3 + version: 18.3.3 + '@types/react-dom': + specifier: 18.3.0 + version: 18.3.0 + '@types/react-window': + specifier: ^1.8.8 + version: 1.8.8 + html-loader: + specifier: ^5.1.0 + version: 5.1.0(webpack@5.95.0) + install: + specifier: ^0.13.0 + version: 0.13.0 + next: + specifier: ^14.2.16 + version: 14.2.16(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + npm: + specifier: ^10.9.2 + version: 10.9.2 + ts-loader: + specifier: ^9.5.1 + version: 9.5.1(typescript@5.6.2)(webpack@5.95.0) + typescript: + specifier: ^5.6.2 + version: 5.6.2 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@auth/core@0.32.0': + resolution: {integrity: sha512-3+ssTScBd+1fd0/fscAyQN1tSygXzuhysuVVzB942ggU4mdfiTbv36P0ccVnExKWYJKvu3E2r3/zxXCCAmTOrg==} + peerDependencies: + '@simplewebauthn/browser': ^9.0.1 + '@simplewebauthn/server': ^9.0.2 + nodemailer: ^6.8.0 + peerDependenciesMeta: + '@simplewebauthn/browser': + optional: true + '@simplewebauthn/server': + optional: true + nodemailer: + optional: true + + '@babel/code-frame@7.24.7': + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.25.0': + resolution: {integrity: sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.24.7': + resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.24.8': + resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.24.7': + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.24.7': + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.25.3': + resolution: {integrity: sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.25.0': + resolution: {integrity: sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.25.6': + resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.25.0': + resolution: {integrity: sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.25.3': + resolution: {integrity: sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.25.2': + resolution: {integrity: sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==} + engines: {node: '>=6.9.0'} + + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + + '@emotion/babel-plugin@11.12.0': + resolution: {integrity: sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==} + + '@emotion/cache@11.13.1': + resolution: {integrity: sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/is-prop-valid@1.3.0': + resolution: {integrity: sha512-SHetuSLvJDzuNbOdtPVbq6yMMMlLoW5Q94uDqJZqy50gcmAjxFkVqmzqSGEFq9gT2iMuIeKV1PXVWmvUhuZLlQ==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.13.3': + resolution: {integrity: sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.0': + resolution: {integrity: sha512-jACuBa9SlYajnpIVXB+XOXnfJHyckDfe6fOpORIM6yhBDlqGuExvDdZYHDQGoDf3bZXGv7tNr+LpLjJqiEQ6EA==} + + '@emotion/serialize@1.3.1': + resolution: {integrity: sha512-dEPNKzBPU+vFPGa+z3axPRn8XVDetYORmDC0wAiej+TNcOZE70ZMJa0X7JdeoM6q/nWTMZeLpN/fTnD9o8MQBA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/styled@11.13.0': + resolution: {integrity: sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/unitless@0.9.0': + resolution: {integrity: sha512-TP6GgNZtmtFaFcsOgExdnfxLLpRDla4Q66tnenA9CktvVSdNKDvMVuUah4QvWPIpNjrWsGg3qeGo9a43QooGZQ==} + + '@emotion/use-insertion-effect-with-fallbacks@1.1.0': + resolution: {integrity: sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.0': + resolution: {integrity: sha512-spEnrA1b6hDR/C68lC2M7m6ALPUHZC0lIY7jAS/B/9DuuO1ZP04eov8SMv/6fwRd8pzmsn2AuJEznRREWlQrlQ==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@ffmpeg-installer/darwin-arm64@4.1.5': + resolution: {integrity: sha512-hYqTiP63mXz7wSQfuqfFwfLOfwwFChUedeCVKkBtl/cliaTM7/ePI9bVzfZ2c+dWu3TqCwLDRWNSJ5pqZl8otA==} + cpu: [arm64] + os: [darwin] + + '@ffmpeg-installer/darwin-x64@4.1.0': + resolution: {integrity: sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==} + cpu: [x64] + os: [darwin] + + '@ffmpeg-installer/ffmpeg@1.1.0': + resolution: {integrity: sha512-Uq4rmwkdGxIa9A6Bd/VqqYbT7zqh1GrT5/rFwCwKM70b42W5gIjWeVETq6SdcL0zXqDtY081Ws/iJWhr1+xvQg==} + + '@ffmpeg-installer/linux-arm64@4.1.4': + resolution: {integrity: sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==} + cpu: [arm64] + os: [linux] + + '@ffmpeg-installer/linux-arm@4.1.3': + resolution: {integrity: sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==} + cpu: [arm] + os: [linux] + + '@ffmpeg-installer/linux-ia32@4.1.0': + resolution: {integrity: sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==} + cpu: [ia32] + os: [linux] + + '@ffmpeg-installer/linux-x64@4.1.0': + resolution: {integrity: sha512-Y5BWhGLU/WpQjOArNIgXD3z5mxxdV8c41C+U15nsE5yF8tVcdCGet5zPs5Zy3Ta6bU7haGpIzryutqCGQA/W8A==} + cpu: [x64] + os: [linux] + + '@ffmpeg-installer/win32-ia32@4.1.0': + resolution: {integrity: sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==} + cpu: [ia32] + os: [win32] + + '@ffmpeg-installer/win32-x64@4.1.0': + resolution: {integrity: sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==} + cpu: [x64] + os: [win32] + + '@ffprobe-installer/darwin-arm64@5.0.1': + resolution: {integrity: sha512-vwNCNjokH8hfkbl6m95zICHwkSzhEvDC3GVBcUp5HX8+4wsX10SP3B+bGur7XUzTIZ4cQpgJmEIAx6TUwRepMg==} + cpu: [arm64] + os: [darwin] + + '@ffprobe-installer/darwin-x64@5.1.0': + resolution: {integrity: sha512-J+YGscZMpQclFg31O4cfVRGmDpkVsQ2fZujoUdMAAYcP0NtqpC49Hs3SWJpBdsGB4VeqOt5TTm1vSZQzs1NkhA==} + cpu: [x64] + os: [darwin] + + '@ffprobe-installer/ffprobe@2.1.2': + resolution: {integrity: sha512-ZNvwk4f2magF42Zji2Ese16SMj9BS7Fui4kRjg6gTYTxY3gWZNpg85n4MIfQyI9nimHg4x/gT6FVkp/bBDuBwg==} + engines: {node: '>=14.21.2'} + + '@ffprobe-installer/linux-arm64@5.2.0': + resolution: {integrity: sha512-X1VvWtlLs6ScP73biVLuHD5ohKJKsMTa0vafCESOen4mOoNeLAYbxOVxDWAdFz9cpZgRiloFj5QD6nDj8E28yQ==} + cpu: [arm64] + os: [linux] + + '@ffprobe-installer/linux-arm@5.2.0': + resolution: {integrity: sha512-PF5HqEhCY7WTWHtLDYbA/+rLS+rhslWvyBlAG1Fk8VzVlnRdl93o6hy7DE2kJgxWQbFaR3ZktPQGEzfkrmQHvQ==} + cpu: [arm] + os: [linux] + + '@ffprobe-installer/linux-ia32@5.2.0': + resolution: {integrity: sha512-TFVK5sasXyXhbIG7LtPRDmtkrkOsInwKcL43iEvEw+D9vCS2rc//mn9/0Q+BR0UoJEiMK4+ApYr/3LLVUBPOCQ==} + cpu: [ia32] + os: [linux] + + '@ffprobe-installer/linux-x64@5.2.0': + resolution: {integrity: sha512-D3UeqTLYPNs7pBWPLUYGehPdRVqU8eACox4OZy3pZUZatxye2YKlvBwEfaLdL1v2Z4FOAlLUhms0kY8m8kqSRA==} + cpu: [x64] + os: [linux] + + '@ffprobe-installer/win32-ia32@5.1.0': + resolution: {integrity: sha512-5O3vOoNRxmut0/Nu9vSazTdSHasrr+zPT2B3Hm7kjmO3QVFcIfVImS6ReQnZeSy8JPJOqXts5kX5x/3KOX54XQ==} + cpu: [ia32] + os: [win32] + + '@ffprobe-installer/win32-x64@5.1.0': + resolution: {integrity: sha512-jMGYeAgkrdn4e2vvYt/qakgHRE3CPju4bn5TmdPfoAm1BlX1mY9cyMd8gf5vSzI8gH8Zq5WQAyAkmekX/8TSTg==} + cpu: [x64] + os: [win32] + + '@fontsource/roboto@5.1.0': + resolution: {integrity: sha512-cFRRC1s6RqPygeZ8Uw/acwVHqih8Czjt6Q0MwoUoDe9U3m4dH1HmNDRBZyqlMSFwgNAUKgFImncKdmDHyKpwdg==} + + '@google-cloud/firestore@7.10.0': + resolution: {integrity: sha512-VFNhdHvfnmqcHHs6YhmSNHHxQqaaD64GwiL0c+e1qz85S8SWZPC2XFRf8p9yHRTF40Kow424s1KBU9f0fdQa+Q==} + engines: {node: '>=14.0.0'} + + '@google-cloud/iap@3.4.0': + resolution: {integrity: sha512-3NK1MqYiX2Y8qIWtKryqheHL05YjGOHn/BGw5qe5AnxqeZOwW/0/KNiHdOM7RfwO3UVzS+DlZUoMj3ioLcAq6Q==} + engines: {node: '>=14.0.0'} + + '@google-cloud/paginator@5.0.2': + resolution: {integrity: sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==} + engines: {node: '>=14.0.0'} + + '@google-cloud/projectify@4.0.0': + resolution: {integrity: sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==} + engines: {node: '>=14.0.0'} + + '@google-cloud/promisify@4.0.0': + resolution: {integrity: sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==} + engines: {node: '>=14'} + + '@google-cloud/storage@7.12.1': + resolution: {integrity: sha512-Z3ZzOnF3YKLuvpkvF+TjQ6lztxcAyTILp+FjKonmVpEwPa9vFvxpZjubLR4sB6bf19i/8HL2AXRjA0YFgHFRmQ==} + engines: {node: '>=14'} + + '@google-cloud/vertexai@1.6.0': + resolution: {integrity: sha512-4GUJ0YmEQ9bWmc5f6EdTgSfCGCokhtG6qQyEzwPtSEgDzu3rYzTp8aq5x7MXw6lKIBqNxXqSUXvNIPNdHfmq5Q==} + engines: {node: '>=18.0.0'} + + '@grpc/grpc-js@1.11.2': + resolution: {integrity: sha512-DWp92gDD7/Qkj7r8kus6/HCINeo3yPZWZ3paKgDgsbKbSpoxKg1yvN8xe2Q8uE3zOsPe3bX8FQX2+XValq2yTw==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.13': + resolution: {integrity: sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==} + engines: {node: '>=6'} + hasBin: true + + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.6': + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + + '@jridgewell/sourcemap-codec@1.4.15': + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + + '@mapbox/node-pre-gyp@1.0.11': + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + hasBin: true + + '@mui/core-downloads-tracker@6.1.0': + resolution: {integrity: sha512-covEnIn/2er5YdtuukDRA52kmARhKrHjOvPsyTFMQApZdrTBI4h8jbEy2mxZqwMwcAFS9coonQXnEZKL1rUNdQ==} + + '@mui/icons-material@6.1.0': + resolution: {integrity: sha512-HxfB0jxwiMTYMN8gAnYn3avbF1aDrqBEuGIj6JDQ3YkLl650E1Wy8AIhwwyP47wdrv0at9aAR0iOO6VLb74A9w==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@mui/material': ^6.1.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/material-nextjs@6.1.0': + resolution: {integrity: sha512-sltDMnCm/AbLsuKEOTyMHXoHKSKCsr6miYc+izOX6LTfyyD1ahuJj4+bNM35+NJjAgCESMG8TOxiLOjirZ5rHg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/cache': ^11.11.0 + '@emotion/react': ^11.11.4 + '@emotion/server': ^11.11.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + next: ^13.0.0 || ^14.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/cache': + optional: true + '@emotion/server': + optional: true + '@types/react': + optional: true + + '@mui/material@6.1.0': + resolution: {integrity: sha512-4MJ46vmy1xbm8x+ZdRcWm8jEMMowdS8pYlhKQzg/qoKhOcLhImZvf2Jn6z9Dj6gl+lY+C/0MxaHF/avAAGys3Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@mui/material-pigment-css': ^6.1.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@mui/material-pigment-css': + optional: true + '@types/react': + optional: true + + '@mui/private-theming@6.1.0': + resolution: {integrity: sha512-+L5qccs4gwsR0r1dgjqhN24QEQRkqIbfOdxILyMbMkuI50x6wNyt9XrV+J3WtjtZTMGJCrUa5VmZBE6OEPGPWA==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/styled-engine@6.1.0': + resolution: {integrity: sha512-MZ+vtaCkjamrT41+b0Er9OMenjAtP/32+L6fARL9/+BZKuV2QbR3q3TmavT2x0NhDu35IM03s4yKqj32Ziqnyg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + + '@mui/system@6.1.0': + resolution: {integrity: sha512-NumkGDqT6EdXfcoFLYQ+M4XlTW5hH3+aK48xAbRqKPXJfxl36CBt4DLduw/Voa5dcayGus9T6jm1AwU2hoJ5hQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + + '@mui/types@7.2.16': + resolution: {integrity: sha512-qI8TV3M7ShITEEc8Ih15A2vLzZGLhD+/UPNwck/hcls2gwg7dyRjNGXcQYHKLB5Q7PuTRfrTkAoPa2VV1s67Ag==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/utils@6.1.0': + resolution: {integrity: sha512-oT8ZzMISRUhTVpdbYzY0CgrCBb3t/YEdcaM13tUnuTjZ15pdA6g5lx15ZJUdgYXV6PbJdw7tDQgMEr4uXK5TXQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@next/env@14.2.16': + resolution: {integrity: sha512-fLrX5TfJzHCbnZ9YUSnGW63tMV3L4nSfhgOQ0iCcX21Pt+VSTDuaLsSuL8J/2XAiVA5AnzvXDpf6pMs60QxOag==} + + '@next/swc-darwin-arm64@14.2.16': + resolution: {integrity: sha512-uFT34QojYkf0+nn6MEZ4gIWQ5aqGF11uIZ1HSxG+cSbj+Mg3+tYm8qXYd3dKN5jqKUm5rBVvf1PBRO/MeQ6rxw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@14.2.16': + resolution: {integrity: sha512-mCecsFkYezem0QiZlg2bau3Xul77VxUD38b/auAjohMA22G9KTJneUYMv78vWoCCFkleFAhY1NIvbyjj1ncG9g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@14.2.16': + resolution: {integrity: sha512-yhkNA36+ECTC91KSyZcgWgKrYIyDnXZj8PqtJ+c2pMvj45xf7y/HrgI17hLdrcYamLfVt7pBaJUMxADtPaczHA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@14.2.16': + resolution: {integrity: sha512-X2YSyu5RMys8R2lA0yLMCOCtqFOoLxrq2YbazFvcPOE4i/isubYjkh+JCpRmqYfEuCVltvlo+oGfj/b5T2pKUA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@14.2.16': + resolution: {integrity: sha512-9AGcX7VAkGbc5zTSa+bjQ757tkjr6C/pKS7OK8cX7QEiK6MHIIezBLcQ7gQqbDW2k5yaqba2aDtaBeyyZh1i6Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@14.2.16': + resolution: {integrity: sha512-Klgeagrdun4WWDaOizdbtIIm8khUDQJ/5cRzdpXHfkbY91LxBXeejL4kbZBrpR/nmgRrQvmz4l3OtttNVkz2Sg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@14.2.16': + resolution: {integrity: sha512-PwW8A1UC1Y0xIm83G3yFGPiOBftJK4zukTmk7DI1CebyMOoaVpd8aSy7K6GhobzhkjYvqS/QmzcfsWG2Dwizdg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-ia32-msvc@14.2.16': + resolution: {integrity: sha512-jhPl3nN0oKEshJBNDAo0etGMzv0j3q3VYorTSFqH1o3rwv1MQRdor27u1zhkgsHPNeY1jxcgyx1ZsCkDD1IHgg==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@next/swc-win32-x64-msvc@14.2.16': + resolution: {integrity: sha512-OA7NtfxgirCjfqt+02BqxC3MIgM/JaGjw9tOe4fyZgPsqfseNiMPnCRP44Pfs+Gpo9zPN+SXaFsgP6vk8d571A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@panva/hkdf@1.2.0': + resolution: {integrity: sha512-97ZQvZJ4gJhi24Io6zI+W7B67I82q1I8i3BSzQ4OyZj1z4OW87/ruF26lrMES58inTKLy2KgVIDcx8PU4AaANQ==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.5': + resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + + '@tailwindcss/forms@0.5.7': + resolution: {integrity: sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==} + peerDependencies: + tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1' + + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + + '@types/bcrypt@5.0.2': + resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==} + + '@types/caseless@0.12.5': + resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + + '@types/fabric@5.3.10': + resolution: {integrity: sha512-fsJIuVkU+B2AnmQh+Ml2X0ax3NmRIqLvEXmZ+squX60HaF89TvdIP6tI6Uk5srXaauswTwPOOfWE7k2QboUZCg==} + + '@types/fluent-ffmpeg@2.1.27': + resolution: {integrity: sha512-QiDWjihpUhriISNoBi2hJBRUUmoj/BMTYcfz+F+ZM9hHWBYABFAE6hjP/TbCZC0GWwlpa3FzvHH9RzFeRusZ7A==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/long@4.0.2': + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + + '@types/node-fetch@2.6.11': + resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} + + '@types/node@20.14.8': + resolution: {integrity: sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/prop-types@15.7.12': + resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + + '@types/react-dom@18.3.0': + resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} + + '@types/react-transition-group@4.4.11': + resolution: {integrity: sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==} + + '@types/react-window@1.8.8': + resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} + + '@types/react@18.3.3': + resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} + + '@types/request@2.48.12': + resolution: {integrity: sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==} + + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + acorn-globals@7.0.1: + resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.1: + resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} + engines: {node: '>= 14'} + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + + are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + + async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + + async@0.2.10: + resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + attr-accept@2.2.4: + resolution: {integrity: sha512-2pA6xFIbdTUDCAwjN8nQwI+842VwzbDUXO2IYlpPXQIORgKnavorcr4Ce3rwh+zsNg9zK7QPsdvDj3Lum4WX4w==} + engines: {node: '>=4'} + + autoprefixer@10.4.20: + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.23.3: + resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + browserslist@4.24.4: + resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001715: + resolution: {integrity: sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==} + + canvas@2.11.2: + resolution: {integrity: sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==} + engines: {node: '>=6'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + clean-css@5.3.3: + resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} + engines: {node: '>= 10.0'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + + cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + + cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + data-urls@3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} + + debug@4.3.5: + resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + + decompress-response@4.2.1: + resolution: {integrity: sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==} + engines: {node: '>=8'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + deprecated: Use your platform's native DOMException instead + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + electron-to-chromium@1.5.141: + resolution: {integrity: sha512-qS+qH9oqVYc1ooubTiB9l904WVyM6qNYxtOEEGReoZXw3xlqeYdFr5GclNzbkAufWgwWLEPoDi3d9MoRwwIjGw==} + + electron-to-chromium@1.5.19: + resolution: {integrity: sha512-kpLJJi3zxTR1U828P+LIUDZ5ohixyo68/IcYOHLqnbTPr/wdgn4i1ECvmALN9E16JPA6cvCG5UG79gVwVdEK5w==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + enhanced-resolve@5.17.1: + resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} + engines: {node: '>=10.13.0'} + + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fabric@6.4.3: + resolution: {integrity: sha512-z/bJna3kWOBv+wmvVK4XxUQgCXLGb//VaSr5xPFIP708obH7472uuVsWbXam+xq+y21bLBtr4OHO1HuJyYi4FQ==} + engines: {node: '>=16.20.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-uri@3.0.6: + resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + + fast-xml-parser@4.4.1: + resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} + hasBin: true + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + file-selector@0.6.0: + resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==} + engines: {node: '>= 12'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + fluent-ffmpeg@2.1.3: + resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==} + engines: {node: '>=18'} + + foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + + form-data@2.5.1: + resolution: {integrity: sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==} + engines: {node: '>= 0.12'} + + form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fs@0.0.1-security: + resolution: {integrity: sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + functional-red-black-tree@1.0.1: + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + + gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gcp-metadata@6.1.0: + resolution: {integrity: sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==} + engines: {node: '>=14'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@10.4.1: + resolution: {integrity: sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==} + engines: {node: '>=16 || 14 >=14.18'} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + google-auth-library@9.13.0: + resolution: {integrity: sha512-p9Y03Uzp/Igcs36zAaB0XTSwZ8Y0/tpYiz5KIde5By+H9DCVUSYtDWZu6aFXsWTqENMb8BD/pDT3hR8NVrPkfA==} + engines: {node: '>=14'} + + google-gax@4.4.1: + resolution: {integrity: sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg==} + engines: {node: '>=14'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + + html-entities@2.5.2: + resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==} + + html-loader@5.1.0: + resolution: {integrity: sha512-Jb3xwDbsm0W3qlXrCZwcYqYGnYz55hb6aoKQTlzyZPXsPpi6tHXzAfqalecglMQgNvtEfxrCQPaKT90Irt5XDA==} + engines: {node: '>= 18.12.0'} + peerDependencies: + webpack: ^5.0.0 + + html-minifier-terser@7.2.0: + resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==} + engines: {node: ^14.13.1 || >=16.0.0} + hasBin: true + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.5: + resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + install@0.13.0: + resolution: {integrity: sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==} + engines: {node: '>= 0.10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.2.3: + resolution: {integrity: sha512-htOzIMPbpLid/Gq9/zaz9SfExABxqRe1sSCdxntlO/aMD6u0issZQiY25n2GKQUtJ02j7z5sfptlAOMpWWOmvw==} + engines: {node: '>=14'} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jiti@1.21.0: + resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} + hasBin: true + + jose@5.4.1: + resolution: {integrity: sha512-U6QajmpV/nhL9SyfAewo000fkiRQ+Yd2H0lBxJJ9apjpOgkOcBQJWOrMo917lxLptdS/n/o/xPzMkXhF46K8hQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsdom@20.0.3: + resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + jwa@2.0.0: + resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} + + jws@4.0.0: + resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lilconfig@3.1.1: + resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + lru-cache@10.2.2: + resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} + engines: {node: 14 || >=16.14} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.7: + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + mimic-response@2.1.0: + resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==} + engines: {node: '>=8'} + + mini-svg-data-uri@1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + hasBin: true + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.4: + resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nan@2.22.0: + resolution: {integrity: sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==} + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + net@1.0.2: + resolution: {integrity: sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==} + + next-auth@5.0.0-beta.19: + resolution: {integrity: sha512-YHu1igcAxZPh8ZB7GIM93dqgY6gcAzq66FOhQFheAdOx1raxNcApt05nNyNCSB6NegSiyJ4XOPsaNow4pfDmsg==} + peerDependencies: + '@simplewebauthn/browser': ^9.0.1 + '@simplewebauthn/server': ^9.0.2 + next: ^14 || ^15.0.0-0 + nodemailer: ^6.6.5 + react: ^18.2.0 || ^19.0.0-0 + peerDependenciesMeta: + '@simplewebauthn/browser': + optional: true + '@simplewebauthn/server': + optional: true + nodemailer: + optional: true + + next@14.2.16: + resolution: {integrity: sha512-LcO7WnFu6lYSvCzZoo1dB+IO0xXz5uEv52HF1IUN0IqVTUIZGHuuR10I5efiLadGt+4oZqTcNZyVVEem/TM5nA==} + engines: {node: '>=18.17.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + sass: + optional: true + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + npm@10.9.2: + resolution: {integrity: sha512-iriPEPIkoMYUy3F6f3wwSZAU93E0Eg6cHwIR6jzzOXWSy+SD/rOODEs74cVONHKSx2obXtuUoyidVEhISrisgQ==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + bundledDependencies: + - '@isaacs/string-locale-compare' + - '@npmcli/arborist' + - '@npmcli/config' + - '@npmcli/fs' + - '@npmcli/map-workspaces' + - '@npmcli/package-json' + - '@npmcli/promise-spawn' + - '@npmcli/redact' + - '@npmcli/run-script' + - '@sigstore/tuf' + - abbrev + - archy + - cacache + - chalk + - ci-info + - cli-columns + - fastest-levenshtein + - fs-minipass + - glob + - graceful-fs + - hosted-git-info + - ini + - init-package-json + - is-cidr + - json-parse-even-better-errors + - libnpmaccess + - libnpmdiff + - libnpmexec + - libnpmfund + - libnpmhook + - libnpmorg + - libnpmpack + - libnpmpublish + - libnpmsearch + - libnpmteam + - libnpmversion + - make-fetch-happen + - minimatch + - minipass + - minipass-pipeline + - ms + - node-gyp + - nopt + - normalize-package-data + - npm-audit-report + - npm-install-checks + - npm-package-arg + - npm-pick-manifest + - npm-profile + - npm-registry-fetch + - npm-user-validate + - p-map + - pacote + - parse-conflict-json + - proc-log + - qrcode-terminal + - read + - semver + - spdx-expression-parse + - ssri + - supports-color + - tar + - text-table + - tiny-relative-date + - treeverse + - validate-npm-package-name + - which + - write-file-atomic + + npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + deprecated: This package is no longer supported. + + nwsapi@2.2.13: + resolution: {integrity: sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==} + + oauth4webapi@2.11.1: + resolution: {integrity: sha512-aNzOnL98bL6izG97zgnZs1PFEyO4WDVRhz2Pd066NPak44w5ESLRCYmJIyey8avSBPOMtBjhF3ZDDm7bIb7UOg==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.0.1: + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-nested@6.0.1: + resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.0: + resolution: {integrity: sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.4.38: + resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} + engines: {node: ^10 || ^12 || >=14} + + preact-render-to-string@5.2.3: + resolution: {integrity: sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==} + peerDependencies: + preact: '>=10' + + preact@10.11.3: + resolution: {integrity: sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==} + + pretty-format@3.8.0: + resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + proto3-json-serializer@2.0.2: + resolution: {integrity: sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==} + engines: {node: '>=14.0.0'} + + protobufjs@7.4.0: + resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==} + engines: {node: '>=12.0.0'} + + psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-dropzone@14.2.10: + resolution: {integrity: sha512-Y98LOCYxGO2jOFWREeKJlL7gbrHcOlTBp+9DCM1dh9XQ8+P/8ThhZT7kFb05C+bPcTXq/rixpU+5+LzwYrFLUw==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0' + + react-hook-form@7.52.2: + resolution: {integrity: sha512-pqfPEbERnxxiNMPd0bzmt1tuaPcVccywFDpyk2uV5xCIBphHV5T8SVnX9/o3kplPE1zzKt77+YIoq+EMwJp56A==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-mask-editor@0.0.2: + resolution: {integrity: sha512-YXSkdvzt7avOnPymDdQ7d3OO3TLFmkyYNRgBPt70mzL4+B13x9SOqzHIcVTILZlMJr4ntwhoBhRf3nNyinNnKA==} + peerDependencies: + react: ^18 + + react-sketch-canvas@7.0.0-next.4: + resolution: {integrity: sha512-Njm/Kn/t8bPZuSmkuLNy4KyyyayjejqRGtct+9VA1OPHEyUfwNDU+HOr7VUlWnHPJBN9yJyTUyMc3kgRQkDMyg==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16.8' + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + retry-request@7.0.2: + resolution: {integrity: sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==} + engines: {node: '>=14'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + + schema-utils@4.3.2: + resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==} + engines: {node: '>= 10.13.0'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@3.1.1: + resolution: {integrity: sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + stream-events@1.0.5: + resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + + stubs@3.0.0: + resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} + + styled-jsx@5.1.1: + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tailwindcss@3.4.4: + resolution: {integrity: sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==} + engines: {node: '>=14.0.0'} + hasBin: true + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + teeny-request@9.0.0: + resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==} + engines: {node: '>=14'} + + terser-webpack-plugin@5.3.14: + resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + + terser@5.34.1: + resolution: {integrity: sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==} + engines: {node: '>=10'} + hasBin: true + + terser@5.39.0: + resolution: {integrity: sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==} + engines: {node: '>=10'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tls@0.0.1: + resolution: {integrity: sha512-GzHpG+hwupY8VMR6rYsnAhTHqT/97zT45PG8WD5eTT1lq+dFE0nN+1PYpsoBcHJgSmTz5ceK2Cv88IkPmIPOtQ==} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tr46@3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + ts-loader@9.5.1: + resolution: {integrity: sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==} + engines: {node: '>=12.0.0'} + peerDependencies: + typescript: '*' + webpack: ^5.0.0 + + tslib@2.6.3: + resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + + typescript@5.6.2: + resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + update-browserslist-db@1.1.0: + resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + w3c-xmlserializer@4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + + watchpack@2.4.2: + resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} + engines: {node: '>=10.13.0'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + + webpack@5.95.0: + resolution: {integrity: sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + whatwg-url@11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yaml@2.4.3: + resolution: {integrity: sha512-sntgmxj8o7DE7g/Qi60cqpLBA3HG3STcDA0kO+WfB05jEKhZMbY7umNm2rBpQvsmZ16/lPXCJGW2672dgOUkrg==} + engines: {node: '>= 14'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@auth/core@0.32.0': + dependencies: + '@panva/hkdf': 1.2.0 + '@types/cookie': 0.6.0 + cookie: 0.6.0 + jose: 5.4.1 + oauth4webapi: 2.11.1 + preact: 10.11.3 + preact-render-to-string: 5.2.3(preact@10.11.3) + + '@babel/code-frame@7.24.7': + dependencies: + '@babel/highlight': 7.24.7 + picocolors: 1.0.1 + + '@babel/generator@7.25.0': + dependencies: + '@babel/types': 7.25.2 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 + + '@babel/helper-module-imports@7.24.7': + dependencies: + '@babel/traverse': 7.25.3 + '@babel/types': 7.25.2 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.24.8': {} + + '@babel/helper-validator-identifier@7.24.7': {} + + '@babel/highlight@7.24.7': + dependencies: + '@babel/helper-validator-identifier': 7.24.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.1 + + '@babel/parser@7.25.3': + dependencies: + '@babel/types': 7.25.2 + + '@babel/runtime@7.25.0': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/runtime@7.25.6': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/template@7.25.0': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/parser': 7.25.3 + '@babel/types': 7.25.2 + + '@babel/traverse@7.25.3': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.25.0 + '@babel/parser': 7.25.3 + '@babel/template': 7.25.0 + '@babel/types': 7.25.2 + debug: 4.3.5 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.25.2': + dependencies: + '@babel/helper-string-parser': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.6.3 + optional: true + + '@emotion/babel-plugin@11.12.0': + dependencies: + '@babel/helper-module-imports': 7.24.7 + '@babel/runtime': 7.25.0 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.1 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.13.1': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.0 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/is-prop-valid@1.3.0': + dependencies: + '@emotion/memoize': 0.9.0 + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.0 + '@emotion/babel-plugin': 11.12.0 + '@emotion/cache': 11.13.1 + '@emotion/serialize': 1.3.1 + '@emotion/use-insertion-effect-with-fallbacks': 1.1.0(react@18.3.1) + '@emotion/utils': 1.4.0 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.0': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.9.0 + '@emotion/utils': 1.4.0 + csstype: 3.1.3 + + '@emotion/serialize@1.3.1': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.0 + csstype: 3.1.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.0 + '@emotion/babel-plugin': 11.12.0 + '@emotion/is-prop-valid': 1.3.0 + '@emotion/react': 11.13.3(@types/react@18.3.3)(react@18.3.1) + '@emotion/serialize': 1.3.0 + '@emotion/use-insertion-effect-with-fallbacks': 1.1.0(react@18.3.1) + '@emotion/utils': 1.4.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + transitivePeerDependencies: + - supports-color + + '@emotion/unitless@0.10.0': {} + + '@emotion/unitless@0.9.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.1.0(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@emotion/utils@1.4.0': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@ffmpeg-installer/darwin-arm64@4.1.5': + optional: true + + '@ffmpeg-installer/darwin-x64@4.1.0': + optional: true + + '@ffmpeg-installer/ffmpeg@1.1.0': + optionalDependencies: + '@ffmpeg-installer/darwin-arm64': 4.1.5 + '@ffmpeg-installer/darwin-x64': 4.1.0 + '@ffmpeg-installer/linux-arm': 4.1.3 + '@ffmpeg-installer/linux-arm64': 4.1.4 + '@ffmpeg-installer/linux-ia32': 4.1.0 + '@ffmpeg-installer/linux-x64': 4.1.0 + '@ffmpeg-installer/win32-ia32': 4.1.0 + '@ffmpeg-installer/win32-x64': 4.1.0 + + '@ffmpeg-installer/linux-arm64@4.1.4': + optional: true + + '@ffmpeg-installer/linux-arm@4.1.3': + optional: true + + '@ffmpeg-installer/linux-ia32@4.1.0': + optional: true + + '@ffmpeg-installer/linux-x64@4.1.0': + optional: true + + '@ffmpeg-installer/win32-ia32@4.1.0': + optional: true + + '@ffmpeg-installer/win32-x64@4.1.0': + optional: true + + '@ffprobe-installer/darwin-arm64@5.0.1': + optional: true + + '@ffprobe-installer/darwin-x64@5.1.0': + optional: true + + '@ffprobe-installer/ffprobe@2.1.2': + optionalDependencies: + '@ffprobe-installer/darwin-arm64': 5.0.1 + '@ffprobe-installer/darwin-x64': 5.1.0 + '@ffprobe-installer/linux-arm': 5.2.0 + '@ffprobe-installer/linux-arm64': 5.2.0 + '@ffprobe-installer/linux-ia32': 5.2.0 + '@ffprobe-installer/linux-x64': 5.2.0 + '@ffprobe-installer/win32-ia32': 5.1.0 + '@ffprobe-installer/win32-x64': 5.1.0 + + '@ffprobe-installer/linux-arm64@5.2.0': + optional: true + + '@ffprobe-installer/linux-arm@5.2.0': + optional: true + + '@ffprobe-installer/linux-ia32@5.2.0': + optional: true + + '@ffprobe-installer/linux-x64@5.2.0': + optional: true + + '@ffprobe-installer/win32-ia32@5.1.0': + optional: true + + '@ffprobe-installer/win32-x64@5.1.0': + optional: true + + '@fontsource/roboto@5.1.0': {} + + '@google-cloud/firestore@7.10.0': + dependencies: + '@opentelemetry/api': 1.9.0 + fast-deep-equal: 3.1.3 + functional-red-black-tree: 1.0.1 + google-gax: 4.4.1 + protobufjs: 7.4.0 + transitivePeerDependencies: + - encoding + - supports-color + + '@google-cloud/iap@3.4.0': + dependencies: + google-gax: 4.4.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@google-cloud/paginator@5.0.2': + dependencies: + arrify: 2.0.1 + extend: 3.0.2 + + '@google-cloud/projectify@4.0.0': {} + + '@google-cloud/promisify@4.0.0': {} + + '@google-cloud/storage@7.12.1': + dependencies: + '@google-cloud/paginator': 5.0.2 + '@google-cloud/projectify': 4.0.0 + '@google-cloud/promisify': 4.0.0 + abort-controller: 3.0.0 + async-retry: 1.3.3 + duplexify: 4.1.3 + fast-xml-parser: 4.4.1 + gaxios: 6.7.1 + google-auth-library: 9.13.0 + html-entities: 2.5.2 + mime: 3.0.0 + p-limit: 3.1.0 + retry-request: 7.0.2 + teeny-request: 9.0.0 + uuid: 8.3.2 + transitivePeerDependencies: + - encoding + - supports-color + + '@google-cloud/vertexai@1.6.0': + dependencies: + google-auth-library: 9.13.0 + transitivePeerDependencies: + - encoding + - supports-color + + '@grpc/grpc-js@1.11.2': + dependencies: + '@grpc/proto-loader': 0.7.13 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.7.13': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.2.3 + protobufjs: 7.4.0 + yargs: 17.7.2 + + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.3.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/source-map@0.3.6': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/sourcemap-codec@1.4.15': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + + '@js-sdsl/ordered-map@4.4.2': {} + + '@mapbox/node-pre-gyp@1.0.11': + dependencies: + detect-libc: 2.0.3 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0 + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.6.3 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@mui/core-downloads-tracker@6.1.0': {} + + '@mui/icons-material@6.1.0(@mui/material@6.1.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.6 + '@mui/material': 6.1.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@mui/material-nextjs@6.1.0(@emotion/cache@11.13.1)(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(next@14.2.16(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.6 + '@emotion/react': 11.13.3(@types/react@18.3.3)(react@18.3.1) + next: 14.2.16(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@emotion/cache': 11.13.1 + '@types/react': 18.3.3 + + '@mui/material@6.1.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.6 + '@mui/core-downloads-tracker': 6.1.0 + '@mui/system': 6.1.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) + '@mui/types': 7.2.16(@types/react@18.3.3) + '@mui/utils': 6.1.0(@types/react@18.3.3)(react@18.3.1) + '@popperjs/core': 2.11.8 + '@types/react-transition-group': 4.4.11 + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 18.3.1 + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + optionalDependencies: + '@emotion/react': 11.13.3(@types/react@18.3.3)(react@18.3.1) + '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) + '@types/react': 18.3.3 + + '@mui/private-theming@6.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.6 + '@mui/utils': 6.1.0(@types/react@18.3.3)(react@18.3.1) + prop-types: 15.8.1 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@mui/styled-engine@6.1.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.6 + '@emotion/cache': 11.13.1 + '@emotion/sheet': 1.4.0 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.3.1 + optionalDependencies: + '@emotion/react': 11.13.3(@types/react@18.3.3)(react@18.3.1) + '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) + + '@mui/system@6.1.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.6 + '@mui/private-theming': 6.1.0(@types/react@18.3.3)(react@18.3.1) + '@mui/styled-engine': 6.1.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1) + '@mui/types': 7.2.16(@types/react@18.3.3) + '@mui/utils': 6.1.0(@types/react@18.3.3)(react@18.3.1) + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.3.1 + optionalDependencies: + '@emotion/react': 11.13.3(@types/react@18.3.3)(react@18.3.1) + '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) + '@types/react': 18.3.3 + + '@mui/types@7.2.16(@types/react@18.3.3)': + optionalDependencies: + '@types/react': 18.3.3 + + '@mui/utils@6.1.0(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.6 + '@mui/types': 7.2.16(@types/react@18.3.3) + '@types/prop-types': 15.7.12 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 18.3.1 + react-is: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + + '@next/env@14.2.16': {} + + '@next/swc-darwin-arm64@14.2.16': + optional: true + + '@next/swc-darwin-x64@14.2.16': + optional: true + + '@next/swc-linux-arm64-gnu@14.2.16': + optional: true + + '@next/swc-linux-arm64-musl@14.2.16': + optional: true + + '@next/swc-linux-x64-gnu@14.2.16': + optional: true + + '@next/swc-linux-x64-musl@14.2.16': + optional: true + + '@next/swc-win32-arm64-msvc@14.2.16': + optional: true + + '@next/swc-win32-ia32-msvc@14.2.16': + optional: true + + '@next/swc-win32-x64-msvc@14.2.16': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@opentelemetry/api@1.9.0': {} + + '@panva/hkdf@1.2.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@popperjs/core@2.11.8': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.5': + dependencies: + '@swc/counter': 0.1.3 + tslib: 2.6.3 + + '@tailwindcss/forms@0.5.7(tailwindcss@3.4.4)': + dependencies: + mini-svg-data-uri: 1.4.4 + tailwindcss: 3.4.4 + + '@tootallnate/once@2.0.0': {} + + '@types/bcrypt@5.0.2': + dependencies: + '@types/node': 20.14.8 + + '@types/caseless@0.12.5': {} + + '@types/cookie@0.6.0': {} + + '@types/estree@1.0.7': {} + + '@types/fabric@5.3.10': {} + + '@types/fluent-ffmpeg@2.1.27': + dependencies: + '@types/node': 20.14.8 + + '@types/json-schema@7.0.15': {} + + '@types/long@4.0.2': {} + + '@types/node-fetch@2.6.11': + dependencies: + '@types/node': 20.14.8 + form-data: 4.0.0 + + '@types/node@20.14.8': + dependencies: + undici-types: 5.26.5 + + '@types/parse-json@4.0.2': {} + + '@types/prop-types@15.7.12': {} + + '@types/react-dom@18.3.0': + dependencies: + '@types/react': 18.3.3 + + '@types/react-transition-group@4.4.11': + dependencies: + '@types/react': 18.3.3 + + '@types/react-window@1.8.8': + dependencies: + '@types/react': 18.3.3 + + '@types/react@18.3.3': + dependencies: + '@types/prop-types': 15.7.12 + csstype: 3.1.3 + + '@types/request@2.48.12': + dependencies: + '@types/caseless': 0.12.5 + '@types/node': 20.14.8 + '@types/tough-cookie': 4.0.5 + form-data: 2.5.1 + + '@types/tough-cookie@4.0.5': {} + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + abab@2.0.6: + optional: true + + abbrev@1.1.1: + optional: true + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + acorn-globals@7.0.1: + dependencies: + acorn: 8.12.1 + acorn-walk: 8.3.4 + optional: true + + acorn-import-attributes@1.9.5(acorn@8.14.1): + dependencies: + acorn: 8.14.1 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.12.1 + optional: true + + acorn@8.12.1: {} + + acorn@8.14.1: {} + + agent-base@6.0.2: + dependencies: + debug: 4.3.5 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.1: + dependencies: + debug: 4.3.5 + transitivePeerDependencies: + - supports-color + + ajv-formats@2.1.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv-keywords@3.5.2(ajv@6.12.6): + dependencies: + ajv: 6.12.6 + + ajv-keywords@5.1.0(ajv@8.17.1): + dependencies: + ajv: 8.17.1 + fast-deep-equal: 3.1.3 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.0.1: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + aproba@2.0.0: + optional: true + + are-we-there-yet@2.0.0: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + optional: true + + arg@5.0.2: {} + + arrify@2.0.1: {} + + async-retry@1.3.3: + dependencies: + retry: 0.13.1 + + async@0.2.10: {} + + asynckit@0.4.0: {} + + attr-accept@2.2.4: {} + + autoprefixer@10.4.20(postcss@8.4.38): + dependencies: + browserslist: 4.23.3 + caniuse-lite: 1.0.30001715 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.0.1 + postcss: 8.4.38 + postcss-value-parser: 4.2.0 + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.25.0 + cosmiconfig: 7.1.0 + resolve: 1.22.8 + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + bignumber.js@9.1.2: {} + + binary-extensions@2.3.0: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + optional: true + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.23.3: + dependencies: + caniuse-lite: 1.0.30001715 + electron-to-chromium: 1.5.19 + node-releases: 2.0.18 + update-browserslist-db: 1.1.0(browserslist@4.23.3) + + browserslist@4.24.4: + dependencies: + caniuse-lite: 1.0.30001715 + electron-to-chromium: 1.5.141 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.24.4) + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + callsites@3.1.0: {} + + camel-case@4.1.2: + dependencies: + pascal-case: 3.1.2 + tslib: 2.6.3 + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001715: {} + + canvas@2.11.2: + dependencies: + '@mapbox/node-pre-gyp': 1.0.11 + nan: 2.22.0 + simple-get: 3.1.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chownr@2.0.0: + optional: true + + chrome-trace-event@1.0.4: {} + + clean-css@5.3.3: + dependencies: + source-map: 0.6.1 + + client-only@0.0.1: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@2.1.1: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color-support@1.1.3: + optional: true + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@10.0.1: {} + + commander@2.20.3: {} + + commander@4.1.1: {} + + concat-map@0.0.1: + optional: true + + console-control-strings@1.1.0: + optional: true + + convert-source-map@1.9.0: {} + + cookie@0.6.0: {} + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + cssom@0.3.8: + optional: true + + cssom@0.5.0: + optional: true + + cssstyle@2.3.0: + dependencies: + cssom: 0.3.8 + optional: true + + csstype@3.1.3: {} + + data-urls@3.0.2: + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + optional: true + + debug@4.3.5: + dependencies: + ms: 2.1.2 + + decimal.js@10.4.3: + optional: true + + decompress-response@4.2.1: + dependencies: + mimic-response: 2.1.0 + optional: true + + delayed-stream@1.0.0: {} + + delegates@1.0.0: + optional: true + + detect-libc@2.0.3: {} + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.25.6 + csstype: 3.1.3 + + domexception@4.0.0: + dependencies: + webidl-conversions: 7.0.0 + optional: true + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.6.3 + + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.4 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + + eastasianwidth@0.2.0: {} + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + electron-to-chromium@1.5.141: {} + + electron-to-chromium@1.5.19: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.17.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + enhanced-resolve@5.18.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + entities@4.5.0: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-module-lexer@1.7.0: {} + + escalade@3.1.2: {} + + escalade@3.2.0: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + optional: true + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + esprima@4.0.1: + optional: true + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: + optional: true + + event-target-shim@5.0.1: {} + + events@3.3.0: {} + + extend@3.0.2: {} + + fabric@6.4.3: + optionalDependencies: + canvas: 2.11.2 + jsdom: 20.0.3(canvas@2.11.2) + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.7 + + fast-json-stable-stringify@2.1.0: {} + + fast-uri@3.0.6: {} + + fast-xml-parser@4.4.1: + dependencies: + strnum: 1.0.5 + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + file-selector@0.6.0: + dependencies: + tslib: 2.6.3 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-root@1.1.0: {} + + fluent-ffmpeg@2.1.3: + dependencies: + async: 0.2.10 + which: 1.3.1 + + foreground-child@3.1.1: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + + form-data@2.5.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + form-data@4.0.0: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + fraction.js@4.3.7: {} + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + optional: true + + fs.realpath@1.0.0: + optional: true + + fs@0.0.1-security: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + functional-red-black-tree@1.0.1: {} + + gauge@3.0.2: + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + optional: true + + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.5 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gcp-metadata@6.1.0: + dependencies: + gaxios: 6.7.1 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + get-caller-file@2.0.5: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: {} + + glob@10.4.1: + dependencies: + foreground-child: 3.1.1 + jackspeak: 3.2.3 + minimatch: 9.0.4 + minipass: 7.1.2 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + optional: true + + globals@11.12.0: {} + + google-auth-library@9.13.0: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1 + gcp-metadata: 6.1.0 + gtoken: 7.1.0 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + google-gax@4.4.1: + dependencies: + '@grpc/grpc-js': 1.11.2 + '@grpc/proto-loader': 0.7.13 + '@types/long': 4.0.2 + abort-controller: 3.0.0 + duplexify: 4.1.3 + google-auth-library: 9.13.0 + node-fetch: 2.7.0 + object-hash: 3.0.0 + proto3-json-serializer: 2.0.2 + protobufjs: 7.4.0 + retry-request: 7.0.2 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + graceful-fs@4.2.11: {} + + gtoken@7.1.0: + dependencies: + gaxios: 6.7.1 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-unicode@2.0.1: + optional: true + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + html-encoding-sniffer@3.0.0: + dependencies: + whatwg-encoding: 2.0.0 + optional: true + + html-entities@2.5.2: {} + + html-loader@5.1.0(webpack@5.95.0): + dependencies: + html-minifier-terser: 7.2.0 + parse5: 7.2.1 + webpack: 5.95.0 + + html-minifier-terser@7.2.0: + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.3 + commander: 10.0.1 + entities: 4.5.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.34.1 + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.5 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.3.5 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.5: + dependencies: + agent-base: 7.1.1 + debug: 4.3.5 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + optional: true + + inherits@2.0.4: {} + + install@0.13.0: {} + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.2: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.13.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-potential-custom-element-name@1.0.1: + optional: true + + is-stream@2.0.1: {} + + isexe@2.0.0: {} + + jackspeak@3.2.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jest-worker@27.5.1: + dependencies: + '@types/node': 20.14.8 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jiti@1.21.0: {} + + jose@5.4.1: {} + + js-tokens@4.0.0: {} + + jsdom@20.0.3(canvas@2.11.2): + dependencies: + abab: 2.0.6 + acorn: 8.12.1 + acorn-globals: 7.0.1 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.4.3 + domexception: 4.0.0 + escodegen: 2.1.0 + form-data: 4.0.0 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.13 + parse5: 7.2.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + ws: 8.18.0 + xml-name-validator: 4.0.0 + optionalDependencies: + canvas: 2.11.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + optional: true + + jsesc@2.5.2: {} + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.1.2 + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + jwa@2.0.0: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.0: + dependencies: + jwa: 2.0.0 + safe-buffer: 5.2.1 + + lilconfig@2.1.0: {} + + lilconfig@3.1.1: {} + + lines-and-columns@1.2.4: {} + + loader-runner@4.3.0: {} + + lodash.camelcase@4.3.0: {} + + long@5.2.3: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lower-case@2.0.2: + dependencies: + tslib: 2.6.3 + + lru-cache@10.2.2: {} + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + optional: true + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.7: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@3.0.0: {} + + mimic-response@2.1.0: + optional: true + + mini-svg-data-uri@1.4.4: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + optional: true + + minimatch@9.0.4: + dependencies: + brace-expansion: 2.0.1 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + optional: true + + minipass@5.0.0: + optional: true + + minipass@7.1.2: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + optional: true + + mkdirp@1.0.4: + optional: true + + ms@2.1.2: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nan@2.22.0: + optional: true + + nanoid@3.3.7: {} + + neo-async@2.6.2: {} + + net@1.0.2: {} + + next-auth@5.0.0-beta.19(next@14.2.16(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + dependencies: + '@auth/core': 0.32.0 + next: 14.2.16(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + + next@14.2.16(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@next/env': 14.2.16 + '@swc/helpers': 0.5.5 + busboy: 1.6.0 + caniuse-lite: 1.0.30001715 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.1(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 14.2.16 + '@next/swc-darwin-x64': 14.2.16 + '@next/swc-linux-arm64-gnu': 14.2.16 + '@next/swc-linux-arm64-musl': 14.2.16 + '@next/swc-linux-x64-gnu': 14.2.16 + '@next/swc-linux-x64-musl': 14.2.16 + '@next/swc-win32-arm64-msvc': 14.2.16 + '@next/swc-win32-ia32-msvc': 14.2.16 + '@next/swc-win32-x64-msvc': 14.2.16 + '@opentelemetry/api': 1.9.0 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.6.3 + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-releases@2.0.18: {} + + node-releases@2.0.19: {} + + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + optional: true + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + npm@10.9.2: {} + + npmlog@5.0.1: + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + optional: true + + nwsapi@2.2.13: + optional: true + + oauth4webapi@2.11.1: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + param-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.6.3 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.24.7 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse5@7.2.1: + dependencies: + entities: 4.5.0 + + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.6.3 + + path-is-absolute@1.0.1: + optional: true + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.2.2 + minipass: 7.1.2 + + path-type@4.0.0: {} + + picocolors@1.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + pify@2.3.0: {} + + pirates@4.0.6: {} + + postcss-import@15.1.0(postcss@8.4.38): + dependencies: + postcss: 8.4.38 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + + postcss-js@4.0.1(postcss@8.4.38): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.38 + + postcss-load-config@4.0.2(postcss@8.4.38): + dependencies: + lilconfig: 3.1.1 + yaml: 2.4.3 + optionalDependencies: + postcss: 8.4.38 + + postcss-nested@6.0.1(postcss@8.4.38): + dependencies: + postcss: 8.4.38 + postcss-selector-parser: 6.1.0 + + postcss-selector-parser@6.1.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.0 + + postcss@8.4.38: + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.0 + + preact-render-to-string@5.2.3(preact@10.11.3): + dependencies: + preact: 10.11.3 + pretty-format: 3.8.0 + + preact@10.11.3: {} + + pretty-format@3.8.0: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + proto3-json-serializer@2.0.2: + dependencies: + protobufjs: 7.4.0 + + protobufjs@7.4.0: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 20.14.8 + long: 5.2.3 + + psl@1.9.0: + optional: true + + punycode@2.3.1: {} + + querystringify@2.2.0: + optional: true + + queue-microtask@1.2.3: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-dropzone@14.2.10(react@18.3.1): + dependencies: + attr-accept: 2.2.4 + file-selector: 0.6.0 + prop-types: 15.8.1 + react: 18.3.1 + + react-hook-form@7.52.2(react@18.3.1): + dependencies: + react: 18.3.1 + + react-is@16.13.1: {} + + react-is@18.3.1: {} + + react-mask-editor@0.0.2(react@18.3.1): + dependencies: + react: 18.3.1 + + react-sketch-canvas@7.0.0-next.4(react@18.3.1): + dependencies: + react: 18.3.1 + + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.25.6 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + regenerator-runtime@0.14.1: {} + + relateurl@0.2.7: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + requires-port@1.0.0: + optional: true + + resolve-from@4.0.0: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.13.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + retry-request@7.0.2: + dependencies: + '@types/request': 2.48.12 + extend: 3.0.2 + teeny-request: 9.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + retry@0.13.1: {} + + reusify@1.0.4: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + optional: true + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: + optional: true + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + optional: true + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + schema-utils@3.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + + schema-utils@4.3.2: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + ajv-keywords: 5.1.0(ajv@8.17.1) + + semver@6.3.1: + optional: true + + semver@7.6.3: {} + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + set-blocking@2.0.0: + optional: true + + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.6.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@3.0.7: + optional: true + + signal-exit@4.1.0: {} + + simple-concat@1.0.1: + optional: true + + simple-get@3.1.1: + dependencies: + decompress-response: 4.2.1 + once: 1.4.0 + simple-concat: 1.0.1 + optional: true + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + + source-map-js@1.2.0: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + source-map@0.7.4: {} + + stream-events@1.0.5: + dependencies: + stubs: 3.0.0 + + stream-shift@1.0.3: {} + + streamsearch@1.1.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.0.1 + + strnum@1.0.5: {} + + stubs@3.0.0: {} + + styled-jsx@5.1.1(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + + stylis@4.2.0: {} + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + commander: 4.1.1 + glob: 10.4.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + symbol-tree@3.2.4: + optional: true + + tailwindcss@3.4.4: + 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.0 + lilconfig: 2.1.0 + micromatch: 4.0.7 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.1 + postcss: 8.4.38 + postcss-import: 15.1.0(postcss@8.4.38) + postcss-js: 4.0.1(postcss@8.4.38) + postcss-load-config: 4.0.2(postcss@8.4.38) + postcss-nested: 6.0.1(postcss@8.4.38) + postcss-selector-parser: 6.1.0 + resolve: 1.22.8 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + + tapable@2.2.1: {} + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + optional: true + + teeny-request@9.0.0: + dependencies: + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0 + stream-events: 1.0.5 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + terser-webpack-plugin@5.3.14(webpack@5.95.0): + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + jest-worker: 27.5.1 + schema-utils: 4.3.2 + serialize-javascript: 6.0.2 + terser: 5.39.0 + webpack: 5.95.0 + + terser@5.34.1: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.12.1 + commander: 2.20.3 + source-map-support: 0.5.21 + + terser@5.39.0: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.14.1 + commander: 2.20.3 + source-map-support: 0.5.21 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tls@0.0.1: {} + + to-fast-properties@2.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tough-cookie@4.1.4: + dependencies: + psl: 1.9.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + optional: true + + tr46@0.0.3: {} + + tr46@3.0.0: + dependencies: + punycode: 2.3.1 + optional: true + + ts-interface-checker@0.1.13: {} + + ts-loader@9.5.1(typescript@5.6.2)(webpack@5.95.0): + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.17.1 + micromatch: 4.0.7 + semver: 7.6.3 + source-map: 0.7.4 + typescript: 5.6.2 + webpack: 5.95.0 + + tslib@2.6.3: {} + + typescript@5.6.2: {} + + undici-types@5.26.5: {} + + universalify@0.2.0: + optional: true + + update-browserslist-db@1.1.0(browserslist@4.23.3): + dependencies: + browserslist: 4.23.3 + escalade: 3.1.2 + picocolors: 1.0.1 + + update-browserslist-db@1.1.3(browserslist@4.24.4): + dependencies: + browserslist: 4.24.4 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + optional: true + + util-deprecate@1.0.2: {} + + uuid@8.3.2: {} + + uuid@9.0.1: {} + + w3c-xmlserializer@4.0.0: + dependencies: + xml-name-validator: 4.0.0 + optional: true + + watchpack@2.4.2: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + webidl-conversions@3.0.1: {} + + webidl-conversions@7.0.0: + optional: true + + webpack-sources@3.2.3: {} + + webpack@5.95.0: + dependencies: + '@types/estree': 1.0.7 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.14.1 + acorn-import-attributes: 1.9.5(acorn@8.14.1) + browserslist: 4.24.4 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.1 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.14(webpack@5.95.0) + watchpack: 2.4.2 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + whatwg-encoding@2.0.0: + dependencies: + iconv-lite: 0.6.3 + optional: true + + whatwg-mimetype@3.0.0: + optional: true + + whatwg-url@11.0.0: + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + optional: true + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + optional: true + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + ws@8.18.0: + optional: true + + xml-name-validator@4.0.0: + optional: true + + xmlchars@2.2.0: + optional: true + + y18n@5.0.8: {} + + yallist@4.0.0: + optional: true + + yaml@1.10.2: {} + + yaml@2.4.3: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} diff --git a/src/postcss.config.js b/src/postcss.config.js new file mode 100644 index 00000000..a47ef4f9 --- /dev/null +++ b/src/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + autoprefixer: {}, + }, +}; diff --git a/src/public/ImgStudioLogo.svg b/src/public/ImgStudioLogo.svg new file mode 100644 index 00000000..43d9fa54 --- /dev/null +++ b/src/public/ImgStudioLogo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/ImgStudioLogoReversedMini.svg b/src/public/ImgStudioLogoReversedMini.svg new file mode 100644 index 00000000..5d571fb0 --- /dev/null +++ b/src/public/ImgStudioLogoReversedMini.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/cloudicon.svg b/src/public/cloudicon.svg new file mode 100644 index 00000000..e4904b11 --- /dev/null +++ b/src/public/cloudicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/favicon.ico b/src/public/favicon.ico new file mode 100644 index 00000000..7217989a Binary files /dev/null and b/src/public/favicon.ico differ diff --git a/src/third_party/@emotion/cache/LICENSE b/src/third_party/@emotion/cache/LICENSE new file mode 100644 index 00000000..56e808de --- /dev/null +++ b/src/third_party/@emotion/cache/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Emotion team and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/third_party/@emotion/cache/METADATA b/src/third_party/@emotion/cache/METADATA new file mode 100644 index 00000000..c60a1f22 --- /dev/null +++ b/src/third_party/@emotion/cache/METADATA @@ -0,0 +1,6 @@ +Name: @emotion/cache +Version: 11.13.1 +Homepage: https://github.com/emotion-js/emotion/tree/main/packages/cache +License: MIT + +This package was sourced from npm. diff --git a/src/third_party/@emotion/react/LICENSE b/src/third_party/@emotion/react/LICENSE new file mode 100644 index 00000000..56e808de --- /dev/null +++ b/src/third_party/@emotion/react/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Emotion team and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/third_party/@emotion/react/METADATA b/src/third_party/@emotion/react/METADATA new file mode 100644 index 00000000..925d9eee --- /dev/null +++ b/src/third_party/@emotion/react/METADATA @@ -0,0 +1,6 @@ +Name: @emotion/react +Version: 11.13.3 +Homepage: https://github.com/emotion-js/emotion/tree/main/packages/react +License: MIT + +This package was sourced from npm. diff --git a/src/third_party/@emotion/styled/LICENSE b/src/third_party/@emotion/styled/LICENSE new file mode 100644 index 00000000..56e808de --- /dev/null +++ b/src/third_party/@emotion/styled/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Emotion team and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/third_party/@emotion/styled/METADATA b/src/third_party/@emotion/styled/METADATA new file mode 100644 index 00000000..f23f420b --- /dev/null +++ b/src/third_party/@emotion/styled/METADATA @@ -0,0 +1,6 @@ +Name: @emotion/styled +Version: 11.13.0 +Homepage: https://github.com/emotion-js/emotion/tree/main/packages/styled +License: MIT + +This package was sourced from npm. diff --git a/src/third_party/@fontsource/roboto/LICENSE b/src/third_party/@fontsource/roboto/LICENSE new file mode 100644 index 00000000..c0027451 --- /dev/null +++ b/src/third_party/@fontsource/roboto/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2011 Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/src/third_party/@fontsource/roboto/METADATA b/src/third_party/@fontsource/roboto/METADATA new file mode 100644 index 00000000..c11bc402 --- /dev/null +++ b/src/third_party/@fontsource/roboto/METADATA @@ -0,0 +1,6 @@ +Name: @fontsource/roboto +Version: 5.1.0 +Homepage: https://fontsource.org/fonts/roboto +License: Apache-2.0 + +This package was sourced from npm. diff --git a/src/third_party/@google-cloud/firestore/LICENSE b/src/third_party/@google-cloud/firestore/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/src/third_party/@google-cloud/firestore/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/third_party/@google-cloud/firestore/METADATA b/src/third_party/@google-cloud/firestore/METADATA new file mode 100644 index 00000000..edf510d2 --- /dev/null +++ b/src/third_party/@google-cloud/firestore/METADATA @@ -0,0 +1,6 @@ +Name: @google-cloud/firestore +Version: 7.10.0 +Homepage: googleapis/nodejs-firestore +License: Apache-2.0 + +This package was sourced from npm. diff --git a/src/third_party/@google-cloud/iap/LICENSE b/src/third_party/@google-cloud/iap/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/src/third_party/@google-cloud/iap/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/third_party/@google-cloud/iap/METADATA b/src/third_party/@google-cloud/iap/METADATA new file mode 100644 index 00000000..06f29c50 --- /dev/null +++ b/src/third_party/@google-cloud/iap/METADATA @@ -0,0 +1,6 @@ +Name: @google-cloud/iap +Version: 3.4.0 +Homepage: https://github.com/googleapis/google-cloud-node/tree/main/packages/google-cloud-iap +License: Apache-2.0 + +This package was sourced from npm. diff --git a/src/third_party/@google-cloud/storage/LICENSE b/src/third_party/@google-cloud/storage/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/src/third_party/@google-cloud/storage/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/third_party/@google-cloud/storage/METADATA b/src/third_party/@google-cloud/storage/METADATA new file mode 100644 index 00000000..481da552 --- /dev/null +++ b/src/third_party/@google-cloud/storage/METADATA @@ -0,0 +1,6 @@ +Name: @google-cloud/storage +Version: 7.12.1 +Homepage: googleapis/nodejs-storage +License: Apache-2.0 + +This package was sourced from npm. diff --git a/src/third_party/@google-cloud/vertexai/LICENSE b/src/third_party/@google-cloud/vertexai/LICENSE new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/src/third_party/@google-cloud/vertexai/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/src/third_party/@google-cloud/vertexai/METADATA b/src/third_party/@google-cloud/vertexai/METADATA new file mode 100644 index 00000000..d1366823 --- /dev/null +++ b/src/third_party/@google-cloud/vertexai/METADATA @@ -0,0 +1,6 @@ +Name: @google-cloud/vertexai +Version: 1.7.0 +Homepage: https://github.com/googleapis/nodejs-vertexai +License: Apache-2.0 + +This package was sourced from npm. diff --git a/src/third_party/@mui/icons-material/LICENSE b/src/third_party/@mui/icons-material/LICENSE new file mode 100644 index 00000000..af1beaff --- /dev/null +++ b/src/third_party/@mui/icons-material/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Call-Em-All + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/third_party/@mui/icons-material/METADATA b/src/third_party/@mui/icons-material/METADATA new file mode 100644 index 00000000..2d06d85f --- /dev/null +++ b/src/third_party/@mui/icons-material/METADATA @@ -0,0 +1,6 @@ +Name: @mui/icons-material +Version: 6.1.0 +Homepage: https://mui.com/material-ui/material-icons/ +License: MIT + +This package was sourced from npm. diff --git a/src/third_party/@mui/material-nextjs/LICENSE b/src/third_party/@mui/material-nextjs/LICENSE new file mode 100644 index 00000000..af1beaff --- /dev/null +++ b/src/third_party/@mui/material-nextjs/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Call-Em-All + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/third_party/@mui/material-nextjs/METADATA b/src/third_party/@mui/material-nextjs/METADATA new file mode 100644 index 00000000..d53e1d36 --- /dev/null +++ b/src/third_party/@mui/material-nextjs/METADATA @@ -0,0 +1,6 @@ +Name: @mui/material-nextjs +Version: 6.1.0 +Homepage: https://mui.com/material-ui/guides/nextjs/ +License: MIT + +This package was sourced from npm. diff --git a/src/third_party/@mui/material/LICENSE b/src/third_party/@mui/material/LICENSE new file mode 100644 index 00000000..af1beaff --- /dev/null +++ b/src/third_party/@mui/material/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Call-Em-All + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/third_party/@mui/material/METADATA b/src/third_party/@mui/material/METADATA new file mode 100644 index 00000000..662ab10b --- /dev/null +++ b/src/third_party/@mui/material/METADATA @@ -0,0 +1,6 @@ +Name: @mui/material +Version: 6.1.0 +Homepage: https://mui.com/material-ui/ +License: MIT + +This package was sourced from npm. diff --git a/src/third_party/@tailwindcss/forms/LICENSE b/src/third_party/@tailwindcss/forms/LICENSE new file mode 100644 index 00000000..d6a82290 --- /dev/null +++ b/src/third_party/@tailwindcss/forms/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Tailwind Labs, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/third_party/@tailwindcss/forms/METADATA b/src/third_party/@tailwindcss/forms/METADATA new file mode 100644 index 00000000..7482416c --- /dev/null +++ b/src/third_party/@tailwindcss/forms/METADATA @@ -0,0 +1,6 @@ +Name: @tailwindcss/forms +Version: 0.5.9 +Homepage: https://github.com/tailwindlabs/tailwindcss-forms +License: MIT + +This package was sourced from npm. diff --git a/src/third_party/autoprefixer/LICENSE b/src/third_party/autoprefixer/LICENSE new file mode 100644 index 00000000..da057b45 --- /dev/null +++ b/src/third_party/autoprefixer/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright 2013 Andrey Sitnik + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/third_party/autoprefixer/METADATA b/src/third_party/autoprefixer/METADATA new file mode 100644 index 00000000..faa787cf --- /dev/null +++ b/src/third_party/autoprefixer/METADATA @@ -0,0 +1,6 @@ +Name: autoprefixer +Version: 10.4.20 +Homepage: postcss/autoprefixer +License: MIT + +This package was sourced from npm. diff --git a/src/third_party/fabric/LICENSE b/src/third_party/fabric/LICENSE new file mode 100644 index 00000000..accc2c68 --- /dev/null +++ b/src/third_party/fabric/LICENSE @@ -0,0 +1,16 @@ +Copyright (c) 2008-2015 Printio (Juriy Zaytsev, Maxim Chernyak) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/third_party/fabric/METADATA b/src/third_party/fabric/METADATA new file mode 100644 index 00000000..5ff6fefa --- /dev/null +++ b/src/third_party/fabric/METADATA @@ -0,0 +1,6 @@ +Name: fabric +Version: 6.4.3 +Homepage: http://fabricjs.com/ +License: MIT + +This package was sourced from npm. diff --git a/src/third_party/fs/LICENSE b/src/third_party/fs/LICENSE new file mode 100644 index 00000000..6d8852ac --- /dev/null +++ b/src/third_party/fs/LICENSE @@ -0,0 +1 @@ +ISC - see package.json \ No newline at end of file diff --git a/src/third_party/fs/METADATA b/src/third_party/fs/METADATA new file mode 100644 index 00000000..def23ef5 --- /dev/null +++ b/src/third_party/fs/METADATA @@ -0,0 +1,6 @@ +Name: fs +Version: 0.0.1-security +Homepage: https://github.com/npm/security-holder#readme +License: ISC + +This package was sourced from npm. diff --git a/src/third_party/google-auth-library/LICENSE b/src/third_party/google-auth-library/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/src/third_party/google-auth-library/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/third_party/google-auth-library/METADATA b/src/third_party/google-auth-library/METADATA new file mode 100644 index 00000000..d8bc2b2d --- /dev/null +++ b/src/third_party/google-auth-library/METADATA @@ -0,0 +1,6 @@ +Name: google-auth-library +Version: 9.14.1 +Homepage: googleapis/google-auth-library-nodejs.git +License: Apache-2.0 + +This package was sourced from npm. diff --git a/src/third_party/install/LICENSE b/src/third_party/install/LICENSE new file mode 100644 index 00000000..ad7986c4 --- /dev/null +++ b/src/third_party/install/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Benjamin Newman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/src/third_party/install/METADATA b/src/third_party/install/METADATA new file mode 100644 index 00000000..ec9807b1 --- /dev/null +++ b/src/third_party/install/METADATA @@ -0,0 +1,6 @@ +Name: install +Version: 0.13.0 +Homepage: http://github.com/benjamn/install +License: MIT + +This package was sourced from npm. diff --git a/src/third_party/next-auth/LICENSE b/src/third_party/next-auth/LICENSE new file mode 100644 index 00000000..360d3576 --- /dev/null +++ b/src/third_party/next-auth/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2022-2024, Balázs Orbán + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/src/third_party/next-auth/METADATA b/src/third_party/next-auth/METADATA new file mode 100644 index 00000000..ed7b40e8 --- /dev/null +++ b/src/third_party/next-auth/METADATA @@ -0,0 +1,6 @@ +Name: next-auth +Version: 5.0.0-beta.19 +Homepage: https://nextjs.authjs.dev +License: ISC + +This package was sourced from npm. diff --git a/src/third_party/npm/LICENSE b/src/third_party/npm/LICENSE new file mode 100644 index 00000000..0b6c2287 --- /dev/null +++ b/src/third_party/npm/LICENSE @@ -0,0 +1,235 @@ +The npm application +Copyright (c) npm, Inc. and Contributors +Licensed on the terms of The Artistic License 2.0 + +Node package dependencies of the npm application +Copyright (c) their respective copyright owners +Licensed on their respective license terms + +The npm public registry at https://registry.npmjs.org +and the npm website at https://www.npmjs.com +Operated by npm, Inc. +Use governed by terms published on https://www.npmjs.com + +"Node.js" +Trademark Joyent, Inc., https://joyent.com +Neither npm nor npm, Inc. are affiliated with Joyent, Inc. + +The Node.js application +Project of Node Foundation, https://nodejs.org + +The npm Logo +Copyright (c) Mathias Pettersson and Brian Hammond + +"Gubblebum Blocky" typeface +Copyright (c) Tjarda Koster, https://jelloween.deviantart.com +Used with permission + + +-------- + + +The Artistic License 2.0 + +Copyright (c) 2000-2006, The Perl Foundation. + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +Preamble + +This license establishes the terms under which a given free software +Package may be copied, modified, distributed, and/or redistributed. +The intent is that the Copyright Holder maintains some artistic +control over the development of that Package while still keeping the +Package available as open source and free software. + +You are always permitted to make arrangements wholly outside of this +license directly with the Copyright Holder of a given Package. If the +terms of this license do not permit the full use that you propose to +make of the Package, you should contact the Copyright Holder and seek +a different licensing arrangement. + +Definitions + + "Copyright Holder" means the individual(s) or organization(s) + named in the copyright notice for the entire Package. + + "Contributor" means any party that has contributed code or other + material to the Package, in accordance with the Copyright Holder's + procedures. + + "You" and "your" means any person who would like to copy, + distribute, or modify the Package. + + "Package" means the collection of files distributed by the + Copyright Holder, and derivatives of that collection and/or of + those files. A given Package may consist of either the Standard + Version, or a Modified Version. + + "Distribute" means providing a copy of the Package or making it + accessible to anyone else, or in the case of a company or + organization, to others outside of your company or organization. + + "Distributor Fee" means any fee that you charge for Distributing + this Package or providing support for this Package to another + party. It does not mean licensing fees. + + "Standard Version" refers to the Package if it has not been + modified, or has been modified only in ways explicitly requested + by the Copyright Holder. + + "Modified Version" means the Package, if it has been changed, and + such changes were not explicitly requested by the Copyright + Holder. + + "Original License" means this Artistic License as Distributed with + the Standard Version of the Package, in its current version or as + it may be modified by The Perl Foundation in the future. + + "Source" form means the source code, documentation source, and + configuration files for the Package. + + "Compiled" form means the compiled bytecode, object code, binary, + or any other form resulting from mechanical transformation or + translation of the Source form. + + +Permission for Use and Modification Without Distribution + +(1) You are permitted to use the Standard Version and create and use +Modified Versions for any purpose without restriction, provided that +you do not Distribute the Modified Version. + + +Permissions for Redistribution of the Standard Version + +(2) You may Distribute verbatim copies of the Source form of the +Standard Version of this Package in any medium without restriction, +either gratis or for a Distributor Fee, provided that you duplicate +all of the original copyright notices and associated disclaimers. At +your discretion, such verbatim copies may or may not include a +Compiled form of the Package. + +(3) You may apply any bug fixes, portability changes, and other +modifications made available from the Copyright Holder. The resulting +Package will still be considered the Standard Version, and as such +will be subject to the Original License. + + +Distribution of Modified Versions of the Package as Source + +(4) You may Distribute your Modified Version as Source (either gratis +or for a Distributor Fee, and with or without a Compiled form of the +Modified Version) provided that you clearly document how it differs +from the Standard Version, including, but not limited to, documenting +any non-standard features, executables, or modules, and provided that +you do at least ONE of the following: + + (a) make the Modified Version available to the Copyright Holder + of the Standard Version, under the Original License, so that the + Copyright Holder may include your modifications in the Standard + Version. + + (b) ensure that installation of your Modified Version does not + prevent the user installing or running the Standard Version. In + addition, the Modified Version must bear a name that is different + from the name of the Standard Version. + + (c) allow anyone who receives a copy of the Modified Version to + make the Source form of the Modified Version available to others + under + + (i) the Original License or + + (ii) a license that permits the licensee to freely copy, + modify and redistribute the Modified Version using the same + licensing terms that apply to the copy that the licensee + received, and requires that the Source form of the Modified + Version, and of any works derived from it, be made freely + available in that license fees are prohibited but Distributor + Fees are allowed. + + +Distribution of Compiled Forms of the Standard Version +or Modified Versions without the Source + +(5) You may Distribute Compiled forms of the Standard Version without +the Source, provided that you include complete instructions on how to +get the Source of the Standard Version. Such instructions must be +valid at the time of your distribution. If these instructions, at any +time while you are carrying out such distribution, become invalid, you +must provide new instructions on demand or cease further distribution. +If you provide valid instructions or cease distribution within thirty +days after you become aware that the instructions are invalid, then +you do not forfeit any of your rights under this license. + +(6) You may Distribute a Modified Version in Compiled form without +the Source, provided that you comply with Section 4 with respect to +the Source of the Modified Version. + + +Aggregating or Linking the Package + +(7) You may aggregate the Package (either the Standard Version or +Modified Version) with other packages and Distribute the resulting +aggregation provided that you do not charge a licensing fee for the +Package. Distributor Fees are permitted, and licensing fees for other +components in the aggregation are permitted. The terms of this license +apply to the use and Distribution of the Standard or Modified Versions +as included in the aggregation. + +(8) You are permitted to link Modified and Standard Versions with +other works, to embed the Package in a larger work of your own, or to +build stand-alone binary or bytecode versions of applications that +include the Package, and Distribute the result without restriction, +provided the result does not expose a direct interface to the Package. + + +Items That are Not Considered Part of a Modified Version + +(9) Works (including, but not limited to, modules and scripts) that +merely extend or make use of the Package, do not, by themselves, cause +the Package to be a Modified Version. In addition, such works are not +considered parts of the Package itself, and are not subject to the +terms of this license. + + +General Provisions + +(10) Any use, modification, and distribution of the Standard or +Modified Versions is governed by this Artistic License. By using, +modifying or distributing the Package, you accept this license. Do not +use, modify, or distribute the Package, if you do not accept this +license. + +(11) If your Modified Version has been derived from a Modified +Version made by someone other than you, you are nevertheless required +to ensure that your Modified Version complies with the requirements of +this license. + +(12) This license does not grant you the right to use any trademark, +service mark, tradename, or logo of the Copyright Holder. + +(13) This license includes the non-exclusive, worldwide, +free-of-charge patent license to make, have made, use, offer to sell, +sell, import and otherwise transfer the Package with respect to any +patent claims licensable by the Copyright Holder that are necessarily +infringed by the Package. If you institute patent litigation +(including a cross-claim or counterclaim) against any party alleging +that the Package constitutes direct or contributory patent +infringement, then this Artistic License to you shall terminate on the +date that such litigation is filed. + +(14) Disclaimer of Warranty: +THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS +IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR +NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL +LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +-------- diff --git a/src/third_party/npm/METADATA b/src/third_party/npm/METADATA new file mode 100644 index 00000000..f3cecfb7 --- /dev/null +++ b/src/third_party/npm/METADATA @@ -0,0 +1,6 @@ +Name: npm +Version: 10.9.0 +Homepage: https://docs.npmjs.com/ +License: Artistic-2.0 + +This package was sourced from npm. diff --git a/src/third_party/react-dom/LICENSE b/src/third_party/react-dom/LICENSE new file mode 100644 index 00000000..b96dcb04 --- /dev/null +++ b/src/third_party/react-dom/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/third_party/react-dom/METADATA b/src/third_party/react-dom/METADATA new file mode 100644 index 00000000..f08b1dd5 --- /dev/null +++ b/src/third_party/react-dom/METADATA @@ -0,0 +1,6 @@ +Name: react-dom +Version: 18.3.1 +Homepage: https://reactjs.org/ +License: MIT + +This package was sourced from npm. diff --git a/src/third_party/react-dropzone/LICENSE b/src/third_party/react-dropzone/LICENSE new file mode 100644 index 00000000..3f894344 --- /dev/null +++ b/src/third_party/react-dropzone/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2018 Param Aggarwal + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/src/third_party/react-dropzone/METADATA b/src/third_party/react-dropzone/METADATA new file mode 100644 index 00000000..d03f288e --- /dev/null +++ b/src/third_party/react-dropzone/METADATA @@ -0,0 +1,6 @@ +Name: react-dropzone +Version: 14.2.9 +Homepage: https://github.com/react-dropzone/react-dropzone +License: MIT + +This package was sourced from npm. diff --git a/src/third_party/react-hook-form/LICENSE b/src/third_party/react-hook-form/LICENSE new file mode 100644 index 00000000..139faf48 --- /dev/null +++ b/src/third_party/react-hook-form/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-present Beier(Bill) Luo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/third_party/react-hook-form/METADATA b/src/third_party/react-hook-form/METADATA new file mode 100644 index 00000000..b405e36b --- /dev/null +++ b/src/third_party/react-hook-form/METADATA @@ -0,0 +1,6 @@ +Name: react-hook-form +Version: 7.53.0 +Homepage: https://www.react-hook-form.com +License: MIT + +This package was sourced from npm. diff --git a/src/third_party/react-mask-editor/LICENSE b/src/third_party/react-mask-editor/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/src/third_party/react-mask-editor/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/third_party/react-mask-editor/METADATA b/src/third_party/react-mask-editor/METADATA new file mode 100644 index 00000000..d3f6ed95 --- /dev/null +++ b/src/third_party/react-mask-editor/METADATA @@ -0,0 +1,6 @@ +Name: react-mask-editor +Version: 0.0.2 +Homepage: https://docs.voliere.dev/react-mask-editor +License: License Not Found + +This package was sourced from npm. diff --git a/src/third_party/react-sketch-canvas/LICENSE b/src/third_party/react-sketch-canvas/LICENSE new file mode 100644 index 00000000..c4bb2919 --- /dev/null +++ b/src/third_party/react-sketch-canvas/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Vinoth Pandian + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/third_party/react-sketch-canvas/METADATA b/src/third_party/react-sketch-canvas/METADATA new file mode 100644 index 00000000..56f3587c --- /dev/null +++ b/src/third_party/react-sketch-canvas/METADATA @@ -0,0 +1,6 @@ +Name: react-sketch-canvas +Version: 7.0.0-next.4 +Homepage: https://vinoth.info/react-sketch-canvas +License: MIT + +This package was sourced from npm. diff --git a/src/third_party/react/LICENSE b/src/third_party/react/LICENSE new file mode 100644 index 00000000..b96dcb04 --- /dev/null +++ b/src/third_party/react/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/third_party/react/METADATA b/src/third_party/react/METADATA new file mode 100644 index 00000000..831ba8ff --- /dev/null +++ b/src/third_party/react/METADATA @@ -0,0 +1,6 @@ +Name: react +Version: 18.3.1 +Homepage: https://reactjs.org/ +License: MIT + +This package was sourced from npm. diff --git a/src/third_party/sharp/LICENSE b/src/third_party/sharp/LICENSE new file mode 100644 index 00000000..37ec93a1 --- /dev/null +++ b/src/third_party/sharp/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/third_party/sharp/METADATA b/src/third_party/sharp/METADATA new file mode 100644 index 00000000..5fadaf30 --- /dev/null +++ b/src/third_party/sharp/METADATA @@ -0,0 +1,6 @@ +Name: sharp +Version: 0.33.5 +Homepage: https://sharp.pixelplumbing.com +License: Apache-2.0 + +This package was sourced from npm. diff --git a/src/third_party/tailwindcss/LICENSE b/src/third_party/tailwindcss/LICENSE new file mode 100644 index 00000000..d6a82290 --- /dev/null +++ b/src/third_party/tailwindcss/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Tailwind Labs, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/third_party/tailwindcss/METADATA b/src/third_party/tailwindcss/METADATA new file mode 100644 index 00000000..893168f9 --- /dev/null +++ b/src/third_party/tailwindcss/METADATA @@ -0,0 +1,6 @@ +Name: tailwindcss +Version: 3.4.4 +Homepage: https://tailwindcss.com +License: MIT + +This package was sourced from npm. diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 00000000..54253639 --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,39 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "outDir": "./dist", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + } + }, + "include": [ + "next-env.d.ts", + "types/**/*.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "app/context", + "app/api/verify-iap.js", + "generate-third-party.js" + ], + "exclude": ["node_modules"] +} diff --git a/src/webpack.config.js b/src/webpack.config.js new file mode 100644 index 00000000..fa761a7d --- /dev/null +++ b/src/webpack.config.js @@ -0,0 +1,15 @@ +module.exports = { + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + ], + }, + + resolve: { + extensions: ['.tsx', '.ts', '.js'], + }, +} diff --git a/terraform/cloud_run.tf b/terraform/cloud_run.tf new file mode 100644 index 00000000..4791c044 --- /dev/null +++ b/terraform/cloud_run.tf @@ -0,0 +1,28 @@ +# Img Studio Cloud Run service +resource "google_cloud_run_v2_service" "img_studio_service" { + name = local.app_name + project = var.project_id + location = var.region + + + template { + service_account = google_service_account.app_sa.email + containers { + image = local.app_container + } + annotations = { + "autoscaling.knative.dev/minScale" = "1" + "autoscaling.knative.dev/maxScale" = "2" + } + labels = local.resource_labels + } + + traffic { + percent = 100 + type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST" + } + + depends_on = [ + google_project_iam_member.app_sa_roles + ] +} \ No newline at end of file diff --git a/terraform/firestore.tf b/terraform/firestore.tf new file mode 100644 index 00000000..3e54fe59 --- /dev/null +++ b/terraform/firestore.tf @@ -0,0 +1,56 @@ +resource "google_firestore_database" "database" { + project = var.project_id + name = "(default)" + location_id = var.region # Tokyo is single region DB + type = "FIRESTORE_NATIVE" + + delete_protection_state = "DELETE_PROTECTION_DISABLED" + deletion_policy = "DELETE" +} + +resource "google_firestore_index" "db-index" { + project = var.project_id + database = google_firestore_database.database.name + collection = "metadata" + query_scope = "COLLECTION" + + fields { + field_path = "combinedFilters" + array_config = "CONTAINS" + } + + fields { + field_path = "timestamp" + order = "DESCENDING" + } + + fields { + field_path = "__name__" + order = "DESCENDING" + } +} + +resource "google_firebaserules_ruleset" "security_rules" { + project = var.project_id + + source { + files { + name = "firestore.rules" + content = <