diff --git a/.gitattributes b/.gitattributes
index 911d3917bb6961ec7f20fa9a41e33b180242628a..ca462f55d0d8216f9c93c9cd20003bd373ab2ac8 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -33,5 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.zst filter=lfs diff=lfs merge=lfs -text
*tfevents* filter=lfs diff=lfs merge=lfs -text
+backend/histopathology_trained_model.keras filter=lfs diff=lfs merge=lfs -text
+frontend/public/cyto/*.jpg filter=lfs diff=lfs merge=lfs -text
+frontend/public/cyto/*.png filter=lfs diff=lfs merge=lfs -text
backend/outputs/** filter=lfs diff=lfs merge=lfs -text
frontend/public/cyto/** filter=lfs diff=lfs merge=lfs -text
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..689c8d17bc67ffc5cea2ad069b0026953f336d46
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,43 @@
+# -----------------------------
+# 1️⃣ Build Frontend
+# -----------------------------
+FROM node:18-bullseye AS frontend-builder
+WORKDIR /app/frontend
+COPY frontend/package*.json ./
+RUN npm install
+COPY frontend/ .
+RUN npm run build
+
+# -----------------------------
+# 2️⃣ Build Backend
+# -----------------------------
+FROM python:3.10-slim-bullseye
+
+WORKDIR /app
+
+# Install essential system libraries for OpenCV, YOLO, and Ultralytics
+RUN apt-get update && apt-get install -y \
+ libgl1 \
+ libglib2.0-0 \
+ libgomp1 \
+ && apt-get clean && rm -rf /var/lib/apt/lists/*
+
+# Copy backend source code
+COPY backend/ .
+
+# Copy built frontend into the right folder for FastAPI
+# ✅ this must match your app.mount() path in app.py
+COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
+
+# Install Python dependencies
+RUN pip install --upgrade pip
+RUN pip install -r requirements.txt || pip install -r backend/requirements.txt || true
+
+# Install runtime dependencies explicitly
+RUN pip install --no-cache-dir fastapi uvicorn python-multipart ultralytics opencv-python-headless pillow numpy scikit-learn tensorflow keras
+
+# Hugging Face Spaces expect port 7860
+EXPOSE 7860
+
+# Run FastAPI app
+CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
diff --git a/README.md b/README.md
index 08b2f384487fcc667954f7881f647ba22fb29b78..6ecfe89d1fc794a679ac9bdf12c650b8beb84fe6 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,10 @@
---
-title: Pathora
-emoji: 🐨
-colorFrom: red
+title: Proj Demo
+emoji: 🚀
+colorFrom: purple
colorTo: green
sdk: docker
pinned: false
-short_description: Manalife's AI Pathology Assistant
---
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
diff --git a/backend/MWTclass2.pth b/backend/MWTclass2.pth
new file mode 100644
index 0000000000000000000000000000000000000000..e59bf9f7f0a497bafefc230e2803dde6b551c76b
--- /dev/null
+++ b/backend/MWTclass2.pth
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:09fae21e537056bc61f9ab4bb08249b591697bb087546cf639c599c70b8c6a2c
+size 79777797
diff --git a/backend/__pycache__/app0.cpython-312.pyc b/backend/__pycache__/app0.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..3f33ed0902bfc66b71817241d9e37a9ef70e6bbe
Binary files /dev/null and b/backend/__pycache__/app0.cpython-312.pyc differ
diff --git a/backend/__pycache__/app2.cpython-312.pyc b/backend/__pycache__/app2.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..5b61f5b95ca36a8cbafa691a87d33de3cfc1b969
Binary files /dev/null and b/backend/__pycache__/app2.cpython-312.pyc differ
diff --git a/backend/__pycache__/app3.cpython-312.pyc b/backend/__pycache__/app3.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b74c06fe2e2a8244423d93f88d1cc657ab34f77c
Binary files /dev/null and b/backend/__pycache__/app3.cpython-312.pyc differ
diff --git a/backend/__pycache__/app4.cpython-312.pyc b/backend/__pycache__/app4.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b1da0aa89c7c02b8c80b0cd9090655a6072f1f3c
Binary files /dev/null and b/backend/__pycache__/app4.cpython-312.pyc differ
diff --git a/backend/__pycache__/augmentations.cpython-312.pyc b/backend/__pycache__/augmentations.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..690d4d039d5d46a0e7c49a644450993a15ef027e
Binary files /dev/null and b/backend/__pycache__/augmentations.cpython-312.pyc differ
diff --git a/backend/__pycache__/model.cpython-312.pyc b/backend/__pycache__/model.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2aec20e5902e31ec5e20606dd821c1329d5db331
Binary files /dev/null and b/backend/__pycache__/model.cpython-312.pyc differ
diff --git a/backend/__pycache__/model_histo.cpython-312.pyc b/backend/__pycache__/model_histo.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..50b65b0c25c18cb329c45f8d43def0a6856bda09
Binary files /dev/null and b/backend/__pycache__/model_histo.cpython-312.pyc differ
diff --git a/backend/app.py b/backend/app.py
new file mode 100644
index 0000000000000000000000000000000000000000..adc0e469cf043b040ee82d5c8dfbd0a443803123
--- /dev/null
+++ b/backend/app.py
@@ -0,0 +1,241 @@
+from fastapi import FastAPI, File, UploadFile, Form
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import JSONResponse
+from fastapi.staticfiles import StaticFiles
+from ultralytics import YOLO
+from io import BytesIO
+from PIL import Image
+import uvicorn
+import json, os, uuid, numpy as np, torch, cv2, joblib, io, tensorflow as tf
+import torch.nn as nn
+import torchvision.transforms as transforms
+import torchvision.models as models
+from sklearn.preprocessing import MinMaxScaler
+from model import MWT as create_model
+from augmentations import Augmentations
+from model_histo import BreastCancerClassifier # TensorFlow model
+
+from huggingface_hub import login
+import os
+
+hf_token = os.getenv("HF_TOKEN")
+if hf_token:
+ login(token=hf_token)
+
+
+# =====================================================
+# App setup
+# =====================================================
+app = FastAPI(title="Unified Cervical & Breast Cancer Analysis API")
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+OUTPUT_DIR = "outputs"
+os.makedirs(OUTPUT_DIR, exist_ok=True)
+app.mount("/outputs", StaticFiles(directory=OUTPUT_DIR), name="outputs")
+
+device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+
+# =====================================================
+# Model 1: YOLO (Colposcopy Detection)
+# =====================================================
+print("🔹 Loading YOLO model...")
+yolo_model = YOLO("best2.pt")
+
+# =====================================================
+# Model 2: MWT Classifier
+# =====================================================
+print("🔹 Loading MWT model...")
+mwt_model = create_model(num_classes=2).to(device)
+mwt_model.load_state_dict(torch.load("MWTclass2.pth", map_location=device))
+mwt_model.eval()
+mwt_class_names = ['neg', 'pos']
+
+# =====================================================
+# Model 3: CIN Classifier
+# =====================================================
+print("🔹 Loading CIN model...")
+clf = joblib.load("logistic_regression_model.pkl")
+yolo_colposcopy = YOLO("yolo_colposcopy.pt")
+
+def build_resnet(model_name="resnet50"):
+ if model_name == "resnet50":
+ model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT)
+ elif model_name == "resnet101":
+ model = models.resnet101(weights=models.ResNet101_Weights.DEFAULT)
+ elif model_name == "resnet152":
+ model = models.resnet152(weights=models.ResNet152_Weights.DEFAULT)
+ model.eval().to(device)
+ return (
+ nn.Sequential(model.conv1, model.bn1, model.relu, model.maxpool),
+ model.layer1, model.layer2, model.layer3, model.layer4,
+ )
+
+gap = nn.AdaptiveAvgPool2d((1, 1))
+gmp = nn.AdaptiveMaxPool2d((1, 1))
+resnet50_blocks = build_resnet("resnet50")
+resnet101_blocks = build_resnet("resnet101")
+resnet152_blocks = build_resnet("resnet152")
+
+transform = transforms.Compose([
+ transforms.ToPILImage(),
+ transforms.Resize((224, 224)),
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.485, 0.456, 0.406],
+ std=[0.229, 0.224, 0.225]),
+])
+
+# =====================================================
+# Model 4: Histopathology Classifier (TensorFlow)
+# =====================================================
+print("🔹 Loading Breast Cancer Histopathology model...")
+classifier = BreastCancerClassifier(fine_tune=False)
+if not classifier.authenticate_huggingface():
+ raise RuntimeError("HuggingFace authentication failed.")
+if not classifier.load_path_foundation():
+ raise RuntimeError("Failed to load Path Foundation model.")
+model_path = "histopathology_trained_model.keras"
+classifier.model = tf.keras.models.load_model(model_path)
+print(f"✅ Loaded model from {model_path}")
+
+# =====================================================
+# Helper functions
+# =====================================================
+def preprocess_for_mwt(image_np):
+ img = cv2.resize(image_np, (224, 224))
+ img = Augmentations.Normalization((0, 1))(img)
+ img = np.array(img, np.float32)
+ img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
+ img = img.transpose(2, 0, 1)
+ img = np.expand_dims(img, axis=0)
+ return torch.Tensor(img)
+
+def extract_cbf_features(blocks, img_t):
+ block1, block2, block3, block4, block5 = blocks
+ with torch.no_grad():
+ f1 = block1(img_t)
+ f2 = block2(f1)
+ f3 = block3(f2)
+ f4 = block4(f3)
+ f5 = block5(f4)
+ p1 = gmp(f1).view(-1)
+ p2 = gmp(f2).view(-1)
+ p3 = gap(f3).view(-1)
+ p4 = gap(f4).view(-1)
+ p5 = gap(f5).view(-1)
+ cbf_feature = torch.cat([p1, p2, p3, p4, p5], dim=0)
+ return cbf_feature.cpu().numpy()
+
+def predict_histopathology(image: Image.Image):
+ if image.mode != "RGB":
+ image = image.convert("RGB")
+ image = image.resize((224, 224))
+ img_array = np.expand_dims(np.array(image).astype("float32") / 255.0, axis=0)
+ embeddings = classifier.extract_embeddings(img_array)
+ prediction_proba = classifier.model.predict(embeddings, verbose=0)[0]
+ predicted_class = int(np.argmax(prediction_proba))
+ class_names = ["Benign", "Malignant"]
+ return {
+ "model_used": "Breast Cancer Histopathology Classifier",
+ "prediction": class_names[predicted_class],
+ "confidence": float(np.max(prediction_proba)),
+ "probabilities": {
+ "Benign": float(prediction_proba[0]),
+ "Malignant": float(prediction_proba[1])
+ }
+ }
+
+# =====================================================
+# Main endpoint
+# =====================================================
+@app.post("/predict/")
+async def predict(model_name: str = Form(...), file: UploadFile = File(...)):
+ contents = await file.read()
+ image = Image.open(BytesIO(contents)).convert("RGB")
+ image_np = np.array(image)
+
+ if model_name == "yolo":
+ results = yolo_model(image)
+ detections_json = results[0].to_json()
+ detections = json.loads(detections_json)
+ output_filename = f"detected_{uuid.uuid4().hex[:8]}.jpg"
+ output_path = os.path.join(OUTPUT_DIR, output_filename)
+ results[0].save(filename=output_path)
+ return {
+ "model_used": "YOLO Detection",
+ "detections": detections,
+ "annotated_image_url": f"/outputs/{output_filename}"
+ }
+
+ elif model_name == "mwt":
+ tensor = preprocess_for_mwt(image_np)
+ with torch.no_grad():
+ output = mwt_model(tensor.to(device)).cpu()
+ probs = torch.softmax(output, dim=1)[0]
+ confidences = {mwt_class_names[i]: float(probs[i]) for i in range(2)}
+ predicted_label = mwt_class_names[torch.argmax(probs)]
+ return {"model_used": "MWT Classifier", "prediction": predicted_label, "confidence": confidences}
+
+ elif model_name == "cin":
+ nparr = np.frombuffer(contents, np.uint8)
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
+ results = yolo_colposcopy.predict(source=img, conf=0.7, save=False, verbose=False)
+ if len(results[0].boxes) == 0:
+ return {"error": "No cervix detected"}
+ x1, y1, x2, y2 = map(int, results[0].boxes.xyxy[0].cpu().numpy())
+ crop = img[y1:y2, x1:x2]
+ crop = cv2.resize(crop, (224, 224))
+ img_t = transform(crop).unsqueeze(0).to(device)
+ f50 = extract_cbf_features(resnet50_blocks, img_t)
+ f101 = extract_cbf_features(resnet101_blocks, img_t)
+ f152 = extract_cbf_features(resnet152_blocks, img_t)
+ features = np.concatenate([f50, f101, f152]).reshape(1, -1)
+ X_scaled = MinMaxScaler().fit_transform(features)
+ pred = clf.predict(X_scaled)[0]
+ proba = clf.predict_proba(X_scaled)[0]
+ classes = ["CIN1", "CIN2", "CIN3"]
+ return {
+ "model_used": "CIN Classifier",
+ "prediction": classes[pred],
+ "probabilities": dict(zip(classes, map(float, proba)))
+ }
+
+ elif model_name == "histopathology":
+ result = predict_histopathology(image)
+ return result
+
+ else:
+ return JSONResponse(content={"error": "Invalid model name"}, status_code=400)
+
+
+@app.get("/models")
+def get_models():
+ return {"available_models": ["yolo", "mwt", "cin", "histopathology"]}
+
+
+@app.get("/health")
+def health():
+ return {"message": "Unified Cervical & Breast Cancer API is running!"}
+
+# After other app.mount()s
+app.mount("/outputs", StaticFiles(directory=OUTPUT_DIR), name="outputs")
+app.mount("/assets", StaticFiles(directory="frontend/dist/assets"), name="assets")
+from fastapi.staticfiles import StaticFiles
+
+app.mount("/", StaticFiles(directory="frontend/dist", html=True), name="static")
+
+
+@app.get("/")
+async def serve_frontend():
+ index_path = os.path.join("frontend", "dist", "index.html")
+ return FileResponse(index_path)
+
+if __name__ == "__main__":
+ uvicorn.run(app, host="0.0.0.0", port=7860)
+
\ No newline at end of file
diff --git a/backend/augmentations.py b/backend/augmentations.py
new file mode 100644
index 0000000000000000000000000000000000000000..4817a3d137f00878bf0e18c5bd96888be63c8571
--- /dev/null
+++ b/backend/augmentations.py
@@ -0,0 +1,328 @@
+# -*- coding: utf-8 -*-
+"""
+This file is a part of project "Aided-Diagnosis-System-for-Cervical-Cancer-Screening".
+See https://github.com/ShenghuaCheng/Aided-Diagnosis-System-for-Cervical-Cancer-Screening for more information.
+File name: augmentations.py
+Description: augmentation functions.
+"""
+
+import functools
+import random
+import cv2
+import numpy as np
+from skimage.exposure import adjust_gamma
+from loguru import logger
+
+__all__ = [
+ "Augmentations",
+ "StylisticTrans",
+ "SpatialTrans",
+]
+
+
+class Augmentations:
+ """
+ All parameters in each augmentations have been fixed to a suitable range.
+ img = [size, size, ch]
+ ch = 3: only img
+ ch = 4: img with mask at 4th dim
+ """
+
+ @staticmethod
+ def Compose(*funcs):
+ funcs = list(funcs)
+ func_names = [f.__name__ for f in funcs]
+ # ensure the norm opt is the last opt
+ if 'norm' in func_names:
+ idx = func_names.index('norm')
+ funcs = funcs[:idx] + funcs[idx:] + [funcs[idx]]
+
+ def compose(img: np.ndarray):
+ return functools.reduce(lambda f, g: lambda x: g(f(x)), funcs)(img)
+
+ return compose
+
+ """
+ # ===========================================================================================================
+ # random stylistic augmentations
+ """
+
+ @staticmethod
+ def RandomGamma(p: float = 0.5):
+ def random_gamma(img: np.ndarray):
+ if random.random() < p:
+ gamma = 0.6 + random.random() * 0.6
+ img[..., :3] = StylisticTrans.gamma_adjust(img[..., :3], gamma)
+ return img
+
+ return random_gamma
+
+ @staticmethod
+ def RandomSharp(p: float = 0.5):
+ def random_sharp(img: np.ndarray):
+ if random.random() < p:
+ sigma = 8.3 + random.random() * 0.4
+ img[..., :3] = StylisticTrans.sharp(img[..., :3], sigma)
+ return img
+
+ return random_sharp
+
+ @staticmethod
+ def RandomGaussainBlur(p: float = 0.5):
+ def random_gaussian_blur(img: np.ndarray):
+ if random.random() < p:
+ sigma = 0.1 + random.random() * 1
+ img[..., :3] = StylisticTrans.gaussian_blur(img[..., :3], sigma)
+ return img
+
+ return random_gaussian_blur
+
+ @staticmethod
+ def RandomHSVDisturb(p: float = 0.5):
+ def random_hsv_disturb(img: np.ndarray):
+ if random.random() < p:
+ k = np.random.random(3) * [0.1, 0.8, 0.45] + [0.95, 0.7, 0.75]
+ b = np.random.random(3) * [6, 20, 18] + [-3, -10, -10]
+ img[..., :3] = StylisticTrans.hsv_disturb(img[..., :3], k.tolist(), b.tolist())
+ return img
+
+ return random_hsv_disturb
+
+ @staticmethod
+ def RandomRGBSwitch(p: float = 0.5):
+ def random_rgb_switch(img: np.ndarray):
+ if random.random() < p:
+ bgr_seq = list(range(3))
+ random.shuffle(bgr_seq)
+ img[..., :3] = StylisticTrans.bgr_switch(img[..., :3], bgr_seq)
+ return img
+
+ return random_rgb_switch
+
+ """
+ # ===========================================================================================================
+ # random spatial augmentations, funcs can be implement to tiles and their masks.
+ """
+
+ @staticmethod
+ def RandomRotate90(p: float = 0.5):
+ def random_rotate90(img: np.ndarray):
+ if random.random() < p:
+ angle = 90 * random.randint(1, 3)
+ img = SpatialTrans.rotate(img, angle)
+ return img
+
+ return random_rotate90
+
+ @staticmethod
+ def RandomHorizontalFlip(p: float = 0.5):
+ def random_horizontal_flip(img: np.ndarray):
+ if random.random() < p:
+ img = SpatialTrans.flip(img, 0)
+ return img
+
+ return random_horizontal_flip
+
+ @staticmethod
+ def RandomVerticalFlip(p: float = 0.5):
+ def random_vertical_flip(img: np.ndarray):
+ if random.random() < p:
+ img = SpatialTrans.flip(img, 1)
+ return img
+
+ return random_vertical_flip
+
+ @staticmethod
+ def RandomScale(p: float = 0.5):
+ def random_scale(img: np.ndarray):
+ if random.random() < p:
+ ratio = 0.8 + random.random() * 0.4
+ img = SpatialTrans.scale(img, ratio, True)
+ return img
+
+ return random_scale
+
+ @staticmethod
+ def RandomCrop(p: float = 1., size: tuple = (512, 512)):
+ def random_crop(img: np.ndarray):
+ if random.random() < p:
+ # for a large FOV, control the translate range
+ new_shape = list(img.shape[:2][::-1])
+ if img.shape[0] > size[1] * 1.5:
+ new_shape[1] = int(size[1] * 1.5)
+ if img.shape[1] > size[0] * 1.5:
+ new_shape[0] = int(size[0] * 1.5)
+ img = SpatialTrans.center_crop(img.copy(), tuple(new_shape))
+ # do translate
+ xy = np.random.random(2) * (np.array(img.shape[:2]) - list(size))
+ bbox = tuple(xy.astype(np.int).tolist() + list(size))
+ img = SpatialTrans.crop(img, bbox)
+ else:
+ img = SpatialTrans.center_crop(img, size)
+ return img
+
+ return random_crop
+
+ @staticmethod
+ def Normalization(rng: list = [-1, 1]):
+ def norm(img: np.ndarray):
+ img = StylisticTrans.normalization(img, rng)
+ return img
+
+ return norm
+
+ @staticmethod
+ def CenterCrop(size: tuple = (512, 512)):
+ def center_crop(img: np.ndarray):
+ img = SpatialTrans.center_crop(img, size)
+ return img
+
+ return center_crop
+
+
+class StylisticTrans:
+ # TODO Some implementations of augmentation need a efficient way
+ """
+ set of augmentations applied to the content of image
+ """
+
+ @staticmethod
+ def gamma_adjust(img: np.ndarray, gamma: float):
+ """ adjust gamma
+ :param img: a ndarray, better a BGR
+ :param gamma: gamma, recommended value 0.6, range [0.6, 1.2]
+ :return: a ndarray
+ """
+ return adjust_gamma(img.copy(), gamma)
+
+ @staticmethod
+ def sharp(img: np.ndarray, sigma: float):
+ """sharp image
+ :param img: a ndarray, better a BGR
+ :param sigma: sharp degree, recommended range [8.3, 8.7]
+ :return: a ndarray
+ """
+ kernel = np.array([[-1, -1, -1], [-1, sigma, -1], [-1, -1, -1]], np.float32) / (sigma - 8) # 锐化
+ return cv2.filter2D(img.copy(), -1, kernel=kernel)
+
+ @staticmethod
+ def gaussian_blur(img: np.ndarray, sigma: float):
+ """blurring image
+ :param img: a ndarray, better a BGR
+ :param sigma: blurring degree, recommended range [0.1, 1.1]
+ :return: a ndarray
+ """
+ return cv2.GaussianBlur(img.copy(), (int(6 * np.ceil(sigma) + 1), int(6 * np.ceil(sigma) + 1)), sigma)
+
+ @staticmethod
+ def hsv_disturb(img: np.ndarray, k: list, b: list):
+ """ disturb the hsv value
+ :param img: a BGR ndarray
+ :param k: low_b = [0.95, 0.7, 0.75] ,upper_b = [1.05, 1.5, 1.2]
+ :param b: low_b = [-3, -10, -10] ,upper_b = [3, 10, 8]
+ :return: a BGR ndarray
+ """
+ img = cv2.cvtColor(img.copy(), cv2.COLOR_BGR2HSV)
+ img = img.astype(np.float)
+ for ch in range(3):
+ img[..., ch] = k[ch] * img[..., ch] + b[ch]
+ img = np.uint8(np.clip(img, np.array([0, 1, 1]), np.array([180, 255, 255])))
+ return cv2.cvtColor(img, cv2.COLOR_HSV2BGR)
+
+ @staticmethod
+ def bgr_switch(img: np.ndarray, bgr_seq: list):
+ """ switch bgr
+ :param img: a ndarray, better a BGR
+ :param bgr_seq: new ch seq
+ :return: a ndarray
+ """
+ return img.copy()[..., bgr_seq]
+
+ @staticmethod
+ def normalization(img: np.ndarray, rng: list):
+ """normalize image according to min and max
+ :param img: a ndarray
+ :param rng: normalize image value to range[min, max]
+ :return: a ndarray
+ """
+ lb, ub = rng
+ delta = ub - lb
+ return (img.copy().astype(np.float64) / 255.) * delta + lb#yjx
+
+
+class SpatialTrans:
+ """
+ set of augmentations applied to the spatial space of image
+ """
+
+ @staticmethod
+ def rotate(img: np.ndarray, angle: int):
+ """ rotate image
+ # todo Square image and central rotate only, a universal version is needed
+ :param img: a ndarray
+ :param angle: rotate angle
+ :return: a ndarray has same size as input, padding zero or cut out region out of picture
+ """
+ assert img.shape[0] == img.shape[1], "Square image needed."
+ center = (img.shape[0]/2, img.shape[1]/2)
+ mat = cv2.getRotationMatrix2D(center, angle, scale=1)
+ # mat = cv2.getRotationMatrix2D(tuple(np.array(img.shape[:2]) // 2), angle, scale=1)
+ return cv2.warpAffine(img.copy(), mat, img.shape[:2])
+
+ @staticmethod
+ def flip(img: np.ndarray, flip_axis: int):
+ """flip image horizontal or vertical
+ :param img: a ndarray
+ :param flip_axis: 0 for horizontal, 1 for vertical
+ :return: a flipped image
+ """
+ return cv2.flip(img.copy(), flip_axis)
+
+ @staticmethod
+ def scale(img: np.ndarray, ratio: float, fix_size: bool = False):
+ """scale image
+ :param img: a ndarray
+ :param ratio: scale ratio
+ :param fix_size: return the center area of scaled image, size of area is same as the image before scaling
+ :return: a scaled image
+ """
+ shape = img.shape[:2][::-1]
+ img = cv2.resize(img.copy(), None, fx=ratio, fy=ratio)
+ if fix_size:
+ img = SpatialTrans.center_crop(img, shape)
+ return img
+
+ @staticmethod
+ def crop(img: np.ndarray, bbox: tuple):
+ """crop image according to given bbox
+ :param img: a ndarray
+ :param bbox: bbox of cropping area (x, y, w, h)
+ :return: cropped image,padding with zeros
+ """
+ ch = [] if len(img.shape) == 2 else [img.shape[-1]]
+ template = np.zeros(list(bbox[-2:])[::-1] + ch)
+
+ if (bbox[1] >= img.shape[0] or bbox[1] >= img.shape[1]) or (bbox[0] + bbox[2] <= 0 or bbox[1] + bbox[3] <= 0):
+ logger.warning("Crop area contains nothing, return a zeros array {}".format(template.shape))
+ return template
+
+ foreground = img[
+ np.maximum(bbox[1], 0): np.minimum(bbox[1] + bbox[3], img.shape[0]),
+ np.maximum(bbox[0], 0): np.minimum(bbox[0] + bbox[2], img.shape[1]), :]
+
+ template[
+ np.maximum(-bbox[1], 0): np.minimum(-bbox[1] + img.shape[0], bbox[3]),
+ np.maximum(-bbox[0], 0): np.minimum(-bbox[0] + img.shape[1], bbox[2]), :] = foreground
+ return template.astype(np.uint8)
+
+ @staticmethod
+ def center_crop(img: np.ndarray, shape: tuple):
+ """return the center area in shape
+ :param img: a ndarray
+ :param shape: center crop shape (w, h)
+ :return:
+ """
+ center = np.array(img.shape[:2]) // 2
+ init = center[::-1] - np.array(shape) // 2
+ bbox = tuple(init.tolist() + list(shape))
+ return SpatialTrans.crop(img, bbox)
\ No newline at end of file
diff --git a/backend/best2.pt b/backend/best2.pt
new file mode 100644
index 0000000000000000000000000000000000000000..d0ee8c7ed115ae7bacc419852ab373a2ca0d37d3
--- /dev/null
+++ b/backend/best2.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1dc58852bdf5287554846eacd4d27daf757dbbcb111cec21e7df2bc463401e5e
+size 6236202
diff --git a/backend/histopathology_trained_model.keras b/backend/histopathology_trained_model.keras
new file mode 100644
index 0000000000000000000000000000000000000000..66e97ff1f570192dbaa26b3ca6c96557021c31aa
--- /dev/null
+++ b/backend/histopathology_trained_model.keras
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:62e427f5d52567b488b157fa1aa8e3ef8236434b1b3752ca49d8a46c144d90c1
+size 8069694
diff --git a/backend/logistic_regression_model.pkl b/backend/logistic_regression_model.pkl
new file mode 100644
index 0000000000000000000000000000000000000000..7139f309e0eace8ae416a908bc6e1217fca45c4e
--- /dev/null
+++ b/backend/logistic_regression_model.pkl
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:09c3442ef1cec5614ed17bbd9e1a4a1f8e38c588fdea13b2171b6662ace86c7b
+size 94559
diff --git a/backend/model.py b/backend/model.py
new file mode 100644
index 0000000000000000000000000000000000000000..531e05b6360065eb3e058b811376d6a551417dbb
--- /dev/null
+++ b/backend/model.py
@@ -0,0 +1,521 @@
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+import torch.utils.checkpoint as checkpoint
+import numpy as np
+from typing import Optional
+from thop import profile
+
+class IRB(nn.Module):
+ def __init__(self, in_features, hidden_features=None, out_features=None, ksize=3, act_layer=nn.Hardswish, drop=0.):
+ super().__init__()
+ out_features = out_features or in_features
+ hidden_features = hidden_features or in_features
+ self.fc1 = nn.Conv2d(in_features, hidden_features, 1, 1, 0)
+ self.act = act_layer()
+ self.conv = nn.Conv2d(hidden_features, hidden_features, kernel_size=ksize, padding=ksize // 2, stride=1,
+ groups=hidden_features)
+ self.fc2 = nn.Conv2d(hidden_features, out_features, 1, 1, 0)
+ self.drop = nn.Dropout(drop)
+
+ def forward(self, x, H, W):
+ B, N, C = x.shape
+ x = x.permute(0, 2, 1).reshape(B, C, H, W)
+ x = self.fc1(x)
+ x = self.act(x)
+ x = self.conv(x)
+ x = self.act(x)
+ x = self.fc2(x)
+ return x.reshape(B, C, -1).permute(0, 2, 1)
+
+
+def drop_path_f(x, drop_prob: float = 0., training: bool = False):
+
+ if drop_prob == 0. or not training:
+ return x
+ keep_prob = 1 - drop_prob
+ shape = (x.shape[0],) + (1,) * (x.ndim - 1) # work with diff dim tensors, not just 2D ConvNets
+ random_tensor = keep_prob + torch.rand(shape, dtype=x.dtype, device=x.device)
+ random_tensor.floor_() # binarize
+ output = x.div(keep_prob) * random_tensor
+ return output
+
+
+class DropPath(nn.Module):
+
+ def __init__(self, drop_prob=None):
+ super(DropPath, self).__init__()
+ self.drop_prob = drop_prob
+
+ def forward(self, x):
+ return drop_path_f(x, self.drop_prob, self.training)
+
+
+def window_partition(x, window_size: int):
+ B, H, W, C = x.shape
+ x = x.view(B, H // window_size, window_size, W // window_size, window_size, C)
+ windows = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size, window_size, C)
+ return windows
+
+
+def window_reverse(windows, window_size: int, H: int, W: int):
+
+ B = int(windows.shape[0] / (H * W / window_size / window_size))
+ x = windows.view(B, H // window_size, W // window_size, window_size, window_size, -1)
+ x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, H, W, -1)
+ return x
+
+
+class PatchEmbed2(nn.Module):
+ def __init__(self, dim:int, patch_size=2, in_c=3, norm_layer=None):
+ super().__init__()
+ patch_size = (patch_size, patch_size)
+ self.patch_size = patch_size
+ self.in_chans = in_c
+ self.embed_dim = dim
+ self.proj = nn.Conv2d(dim, 2*dim, kernel_size=patch_size, stride=patch_size)
+ self.norm = norm_layer(2*dim) if norm_layer else nn.Identity()
+
+ def forward(self, x, H, W):
+ B, L, C = x.shape
+ assert L == H * W, "input feature has wrong size"
+
+ x = x.view(B, H, W, C)
+ pad_input = (H % self.patch_size[0] != 0) or (W % self.patch_size[1] != 0)
+ if pad_input:
+ x = F.pad(x, (0, self.patch_size[1] - W % self.patch_size[1],
+ 0, self.patch_size[0] - H % self.patch_size[0],
+ 0, 0))
+ x = x.reshape(B, H, W, -1).permute(0, 3, 1, 2)
+ x = self.proj(x)
+ _, _, H, W = x.shape
+ x = x.flatten(2).transpose(1, 2)
+ x = self.norm(x)
+ return x
+
+
+class PatchEmbed(nn.Module):
+ def __init__(self, patch_size=4, in_c=3, embed_dim=96, norm_layer=None):
+ super().__init__()
+ patch_size = (patch_size, patch_size)
+ self.patch_size = patch_size
+ self.in_chans = in_c
+ self.embed_dim = embed_dim
+ self.proj = nn.Conv2d(in_c, embed_dim, kernel_size=patch_size, stride=patch_size)
+ self.norm = norm_layer(embed_dim) if norm_layer else nn.Identity()
+
+ def forward(self, x):
+ _, _, H, W = x.shape
+
+ # padding
+ # 如果输入图片的H,W不是patch_size的整数倍,需要进行padding
+ pad_input = (H % self.patch_size[0] != 0) or (W % self.patch_size[1] != 0)
+ if pad_input:
+ x = F.pad(x, (0, self.patch_size[1] - W % self.patch_size[1],
+ 0, self.patch_size[0] - H % self.patch_size[0],
+ 0, 0))
+
+ # 下采样patch_size倍
+ x = self.proj(x)
+ _, _, H, W = x.shape
+ x = x.flatten(2).transpose(1, 2)
+ x = self.norm(x)
+ return x, H, W
+
+
+class PatchMerging(nn.Module):
+
+ def __init__(self, dim, norm_layer=nn.LayerNorm):
+ super().__init__()
+ self.dim = dim
+ self.reduction = nn.Linear(4 * dim, 2 * dim, bias=False)
+ self.norm = norm_layer(4 * dim)
+
+ def forward(self, x, H, W):
+ B, L, C = x.shape
+ assert L == H * W, "input feature has wrong size"
+
+ x = x.view(B, H, W, C)
+
+ pad_input = (H % 2 == 1) or (W % 2 == 1)
+ if pad_input:
+ x = F.pad(x, (0, 0, 0, W % 2, 0, H % 2))
+
+ x0 = x[:, 0::2, 0::2, :] # [B, H/2, W/2, C]
+ x1 = x[:, 1::2, 0::2, :] # [B, H/2, W/2, C]
+ x2 = x[:, 0::2, 1::2, :] # [B, H/2, W/2, C]
+ x3 = x[:, 1::2, 1::2, :] # [B, H/2, W/2, C]
+ x = torch.cat([x0, x1, x2, x3], -1) # [B, H/2, W/2, 4*C]
+ x = x.view(B, -1, 4 * C) # [B, H/2*W/2, 4*C]
+
+ x = self.norm(x)
+ x = self.reduction(x) # [B, H/2*W/2, 2*C]
+
+ return x
+
+
+class Mlp(nn.Module):
+
+ def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=nn.GELU, drop=0.):
+ super().__init__()
+ out_features = out_features or in_features
+ hidden_features = hidden_features or in_features
+
+ self.fc1 = nn.Linear(in_features, hidden_features)
+ self.act = act_layer()
+ self.drop1 = nn.Dropout(drop)
+ self.fc2 = nn.Linear(hidden_features, out_features)
+ self.drop2 = nn.Dropout(drop)
+
+ def forward(self, x):
+ x = self.fc1(x)
+ x = self.act(x)
+ x = self.drop1(x)
+ x = self.fc2(x)
+ x = self.drop2(x)
+ return x
+
+
+class WindowAttention(nn.Module):
+
+ def __init__(self, dim, window_size, num_heads, qkv_bias=True, attn_drop=0., proj_drop=0.):
+
+ super().__init__()
+ self.dim = dim
+ self.window_size = window_size # [Mh, Mw]
+ self.num_heads = num_heads
+ head_dim = dim // num_heads
+ self.scale = head_dim ** -0.5
+
+ # define a parameter table of relative position bias
+ self.relative_position_bias_table = nn.Parameter(
+ torch.zeros((2 * window_size[0] - 1) * (2 * window_size[1] - 1), num_heads)) # [2*Mh-1 * 2*Mw-1, nH]
+
+ # get pair-wise relative position index for each token inside the window
+ coords_h = torch.arange(self.window_size[0])
+ coords_w = torch.arange(self.window_size[1])
+ coords = torch.stack(torch.meshgrid([coords_h, coords_w]))
+ coords_flatten = torch.flatten(coords, 1) # [2, Mh*Mw]
+ # [2, Mh*Mw, 1] - [2, 1, Mh*Mw]
+ relative_coords = coords_flatten[:, :, None] - coords_flatten[:, None, :] # [2, Mh*Mw, Mh*Mw]
+ relative_coords = relative_coords.permute(1, 2, 0).contiguous() # [Mh*Mw, Mh*Mw, 2]
+ relative_coords[:, :, 0] += self.window_size[0] - 1 # shift to start from 0
+ relative_coords[:, :, 1] += self.window_size[1] - 1
+ relative_coords[:, :, 0] *= 2 * self.window_size[1] - 1
+ relative_position_index = relative_coords.sum(-1) # [Mh*Mw, Mh*Mw]
+ self.register_buffer("relative_position_index", relative_position_index)
+
+ self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
+ self.attn_drop = nn.Dropout(attn_drop)
+ self.proj = nn.Linear(dim, dim)
+ self.proj_drop = nn.Dropout(proj_drop)
+
+ nn.init.trunc_normal_(self.relative_position_bias_table, std=.02)
+ self.softmax = nn.Softmax(dim=-1)
+
+ def forward(self, x, mask: Optional[torch.Tensor] = None):
+ B_, N, C = x.shape
+ qkv = self.qkv(x).reshape(B_, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4)
+ # [batch_size*num_windows, num_heads, Mh*Mw, embed_dim_per_head]
+ q, k, v = qkv.unbind(0) # make torchscript happy (cannot use tensor as tuple)
+
+ # transpose: -> [batch_size*num_windows, num_heads, embed_dim_per_head, Mh*Mw]
+ # @: multiply -> [batch_size*num_windows, num_heads, Mh*Mw, Mh*Mw]
+ q = q * self.scale
+ attn = (q @ k.transpose(-2, -1))
+
+ relative_position_bias = self.relative_position_bias_table[self.relative_position_index.view(-1)].view(
+ self.window_size[0] * self.window_size[1], self.window_size[0] * self.window_size[1], -1)
+ relative_position_bias = relative_position_bias.permute(2, 0, 1).contiguous() # [nH, Mh*Mw, Mh*Mw]
+ attn = attn + relative_position_bias.unsqueeze(0)
+
+ if mask is not None:
+ nW = mask.shape[0] # num_windows
+ attn = attn.view(B_ // nW, nW, self.num_heads, N, N) + mask.unsqueeze(1).unsqueeze(0)
+ attn = attn.view(-1, self.num_heads, N, N)
+ attn = self.softmax(attn)
+ else:
+ attn = self.softmax(attn)
+
+ attn = self.attn_drop(attn)
+
+ x = (attn @ v).transpose(1, 2).reshape(B_, N, C)
+ x = self.proj(x)
+ x = self.proj_drop(x)
+ return x
+
+
+class TransformerBlock(nn.Module):
+ def __init__(self, dim, num_heads, window_sizes=(7,4,2), branch_num=3,
+ mlp_ratio=4., qkv_bias=True, drop=0., attn_drop=0., drop_path=0.,
+ act_layer=nn.GELU, norm_layer=nn.LayerNorm):
+ super().__init__()
+ self.dim = dim
+ self.num_heads = num_heads
+ self.window_sizes = window_sizes
+ self.branch_num = branch_num
+ self.mlp_ratio = mlp_ratio
+
+ self.norm1 = norm_layer(dim)
+ self.attn = WindowAttention(
+ dim//branch_num, window_size=(self.window_sizes[0], self.window_sizes[0]), num_heads=num_heads//branch_num, qkv_bias=qkv_bias,
+ attn_drop=attn_drop, proj_drop=drop)
+ self.attn1 = WindowAttention(
+ dim//branch_num, window_size=(self.window_sizes[1], self.window_sizes[1]), num_heads=num_heads//branch_num, qkv_bias=qkv_bias,
+ attn_drop=attn_drop, proj_drop=drop)
+ self.attn2 = WindowAttention(
+ dim//branch_num, window_size=(self.window_sizes[2], self.window_sizes[2]), num_heads=num_heads//branch_num, qkv_bias=qkv_bias,
+ attn_drop=attn_drop, proj_drop=drop)
+
+ self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
+ self.norm2 = norm_layer(dim)
+ mlp_hidden_dim = int(dim * mlp_ratio)
+ self.mlp = IRB(in_features=dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop=drop)
+
+ def forward(self, x, attn_mask):
+ H, W = self.H, self.W
+ B, L, C = x.shape
+ assert L == H * W, "input feature has wrong size"
+
+ shortcut = x
+ x = self.norm1(x)
+ x = x.view(B, H, W, C)
+ x0 = x[:,:,:,:(C//self.branch_num)]
+ x1 = x[:,:,:,(C//self.branch_num):(2*C//self.branch_num)]
+ x2 = x[:,:,:,(2*C//self.branch_num):]
+ # ----------------------------------------------------------------------------------------------
+ pad_l = pad_t = 0
+ pad_r = (self.window_sizes[0] - W % self.window_sizes[0]) % self.window_sizes[0]
+ pad_b = (self.window_sizes[0] - H % self.window_sizes[0]) % self.window_sizes[0]
+ x0 = F.pad(x0, (0, 0, pad_l, pad_r, pad_t, pad_b))
+ _, Hp, Wp, _ = x0.shape
+ attn_mask = None
+
+ # partition windows
+ x_windows = window_partition(x0, self.window_sizes[0]) # [nW*B, Mh, Mw, C]
+ x_windows = x_windows.view(-1, self.window_sizes[0] * self.window_sizes[0], C//self.branch_num) # [nW*B, Mh*Mw, C]
+
+ # W-MSA/SW-MSA
+ attn_windows = self.attn(x_windows, mask=attn_mask) # [nW*B, Mh*Mw, C]
+
+ # merge windows
+ attn_windows = attn_windows.view(-1, self.window_sizes[0], self.window_sizes[0], C//self.branch_num) # [nW*B, Mh, Mw, C]
+ x0 = window_reverse(attn_windows, self.window_sizes[0], Hp, Wp) # [B, H', W', C]
+
+ if pad_r > 0 or pad_b > 0:
+ # 把前面pad的数据移除掉
+ x0 = x0[:, :H, :W, :].contiguous()
+
+ x0 = x0.view(B, H * W, C//self.branch_num)
+ # ----------------------------------------------------------------------------------------------
+ pad_l = pad_t = 0
+ pad_r = (self.window_sizes[1] - W % self.window_sizes[1]) % self.window_sizes[1]
+ pad_b = (self.window_sizes[1] - H % self.window_sizes[1]) % self.window_sizes[1]
+ x1 = F.pad(x1, (0, 0, pad_l, pad_r, pad_t, pad_b))
+ _, Hp, Wp, _ = x1.shape
+ attn_mask = None
+
+ # partition windows
+ x_windows = window_partition(x1, self.window_sizes[1]) # [nW*B, Mh, Mw, C]
+ x_windows = x_windows.view(-1, self.window_sizes[1] * self.window_sizes[1],
+ C // self.branch_num) # [nW*B, Mh*Mw, C]
+
+ # W-MSA/SW-MSA
+ attn_windows = self.attn1(x_windows, mask=attn_mask) # [nW*B, Mh*Mw, C]
+
+ # merge windows
+ attn_windows = attn_windows.view(-1, self.window_sizes[1], self.window_sizes[1],
+ C // self.branch_num) # [nW*B, Mh, Mw, C]
+ x1 = window_reverse(attn_windows, self.window_sizes[1], Hp, Wp) # [B, H', W', C]
+
+ if pad_r > 0 or pad_b > 0:
+ # 把前面pad的数据移除掉
+ x1 = x1[:, :H, :W, :].contiguous()
+ x1 = x1.view(B, H * W, C // self.branch_num)
+ # ----------------------------------------------------------------------------------------------
+ pad_l = pad_t = 0
+ pad_r = (self.window_sizes[2] - W % self.window_sizes[2]) % self.window_sizes[2]
+ pad_b = (self.window_sizes[2] - H % self.window_sizes[2]) % self.window_sizes[2]
+ x2 = F.pad(x2, (0, 0, pad_l, pad_r, pad_t, pad_b))
+ _, Hp, Wp, _ = x2.shape
+ attn_mask = None
+ x_windows = window_partition(x2, self.window_sizes[2]) # [nW*B, Mh, Mw, C]
+ x_windows = x_windows.view(-1, self.window_sizes[2] * self.window_sizes[2],
+ C // self.branch_num) # [nW*B, Mh*Mw, C]
+
+ attn_windows = self.attn2(x_windows, mask=attn_mask) # [nW*B, Mh*Mw, C]
+
+ attn_windows = attn_windows.view(-1, self.window_sizes[2], self.window_sizes[2],
+ C // self.branch_num) # [nW*B, Mh, Mw, C]
+ x2 = window_reverse(attn_windows, self.window_sizes[2], Hp, Wp) # [B, H', W', C]
+
+ if pad_r > 0 or pad_b > 0:
+ x2 = x2[:, :H, :W, :].contiguous()
+
+ x2 = x2.view(B, H * W, C // self.branch_num)
+
+ x = torch.cat([x0, x1, x2], -1)
+ # FFN
+ x = shortcut + self.drop_path(x)
+ x = x + self.drop_path(self.mlp(self.norm2(x), H, W))
+
+ return x
+
+
+class BasicLayer(nn.Module):
+ def __init__(self, dim, depth, num_heads, window_size,
+ mlp_ratio=4., qkv_bias=True, drop=0., attn_drop=0.,
+ drop_path=0., norm_layer=nn.LayerNorm, downsample=None, use_checkpoint=False):
+ super().__init__()
+ self.dim = dim
+ self.depth = depth
+ self.window_size = window_size
+ self.use_checkpoint = use_checkpoint
+ self.shift_size = window_size // 2
+
+ # build blocks
+ self.blocks = nn.ModuleList([
+ TransformerBlock(
+ dim=dim,
+ num_heads=num_heads,
+ window_sizes=(7,4,2),
+ mlp_ratio=mlp_ratio,
+ qkv_bias=qkv_bias,
+ drop=drop,
+ attn_drop=attn_drop,
+ drop_path=drop_path[i] if isinstance(drop_path, list) else drop_path,
+ norm_layer=norm_layer)
+ for i in range(depth)])
+
+ # patch merging layer
+ if downsample is not None:
+ self.downsample = downsample(dim=dim, norm_layer=norm_layer)
+ else:
+ self.downsample = None
+
+ def create_mask(self, x, H, W):
+ Hp = int(np.ceil(H / self.window_size)) * self.window_size
+ Wp = int(np.ceil(W / self.window_size)) * self.window_size
+ img_mask = torch.zeros((1, Hp, Wp, 1), device=x.device) # [1, Hp, Wp, 1]
+ h_slices = (slice(0, -self.window_size),
+ slice(-self.window_size, -self.shift_size),
+ slice(-self.shift_size, None))
+ w_slices = (slice(0, -self.window_size),
+ slice(-self.window_size, -self.shift_size),
+ slice(-self.shift_size, None))
+ cnt = 0
+ for h in h_slices:
+ for w in w_slices:
+ img_mask[:, h, w, :] = cnt
+ cnt += 1
+
+ mask_windows = window_partition(img_mask, self.window_size) # [nW, Mh, Mw, 1]
+ mask_windows = mask_windows.view(-1, self.window_size * self.window_size) # [nW, Mh*Mw]
+ attn_mask = mask_windows.unsqueeze(1) - mask_windows.unsqueeze(2) # [nW, 1, Mh*Mw] - [nW, Mh*Mw, 1]
+ attn_mask = attn_mask.masked_fill(attn_mask != 0, float(-100.0)).masked_fill(attn_mask == 0, float(0.0))
+ return attn_mask
+
+ def forward(self, x, H, W):
+ attn_mask = self.create_mask(x, H, W) # [nW, Mh*Mw, Mh*Mw]
+ for blk in self.blocks:
+ blk.H, blk.W = H, W
+ if not torch.jit.is_scripting() and self.use_checkpoint:
+ x = checkpoint.checkpoint(blk, x, attn_mask)
+ else:
+ x = blk(x, attn_mask)
+ if self.downsample is not None:
+ x = self.downsample(x, H, W)
+ H, W = (H + 1) // 2, (W + 1) // 2
+
+ return x, H, W
+
+
+class Transformer(nn.Module):
+ def __init__(self, patch_size=4, in_chans=3, num_classes=1000,
+ embed_dim=96, depths=(2, 2, 6, 2), num_heads=(3, 6, 12, 24),
+ window_size=7, mlp_ratio=4., qkv_bias=True,
+ drop_rate=0., attn_drop_rate=0., drop_path_rate=0.1,
+ norm_layer=nn.LayerNorm, patch_norm=True,
+ use_checkpoint=False, **kwargs):
+ super().__init__()
+
+ self.num_classes = num_classes
+ self.num_layers = len(depths)
+ self.embed_dim = embed_dim
+ self.patch_norm = patch_norm
+ # stage4输出特征矩阵的channels
+ self.num_features = int(embed_dim * 2 ** (self.num_layers - 1))
+ self.mlp_ratio = mlp_ratio
+
+ self.patch_embed = PatchEmbed(
+ patch_size=patch_size, in_c=in_chans, embed_dim=embed_dim,
+ norm_layer=norm_layer if self.patch_norm else None)
+ self.pos_drop = nn.Dropout(p=drop_rate)
+
+ # stochastic depth
+ dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))] # stochastic depth decay rule
+
+ self.layers = nn.ModuleList()
+ for i_layer in range(self.num_layers):
+ layers = BasicLayer(dim=int(embed_dim * 2 ** i_layer),
+ depth=depths[i_layer],
+ num_heads=num_heads[i_layer],
+ window_size=window_size,
+ mlp_ratio=self.mlp_ratio,
+ qkv_bias=qkv_bias,
+ drop=drop_rate,
+ attn_drop=attn_drop_rate,
+ drop_path=dpr[sum(depths[:i_layer]):sum(depths[:i_layer + 1])],
+ norm_layer=norm_layer,
+ downsample=PatchEmbed2 if (i_layer < self.num_layers - 1) else None,
+ use_checkpoint=use_checkpoint)
+ self.layers.append(layers)
+
+ self.norm = norm_layer(self.num_features)
+ self.avgpool = nn.AdaptiveAvgPool1d(1)
+ self.head = nn.Linear(self.num_features, num_classes) if num_classes > 0 else nn.Identity()
+
+ self.apply(self._init_weights)
+
+ def _init_weights(self, m):
+ if isinstance(m, nn.Linear):
+ nn.init.trunc_normal_(m.weight, std=.02)
+ if isinstance(m, nn.Linear) and m.bias is not None:
+ nn.init.constant_(m.bias, 0)
+ elif isinstance(m, nn.LayerNorm):
+ nn.init.constant_(m.bias, 0)
+ nn.init.constant_(m.weight, 1.0)
+
+ def forward(self, x):
+ # x: [B, L, C]
+ x, H, W = self.patch_embed(x)
+ x = self.pos_drop(x)
+
+ for layer in self.layers:
+ x, H, W = layer(x, H, W)
+
+ x = self.norm(x) # [B, L, C]
+ x = self.avgpool(x.transpose(1, 2)) # [B, C, 1]
+ x = torch.flatten(x, 1)
+ x = self.head(x)
+ return x
+
+
+def MWT(num_classes: int = 1000, **kwargs):
+ model = Transformer(in_chans=3,
+ patch_size=4,
+ # window_sizes=(7,4,2),
+ embed_dim=96,
+ depths=(2, 4, 4, 2),
+ num_heads=(3, 6, 12, 24),
+ num_classes=num_classes,
+ **kwargs)
+ return model
+
+if __name__ == '__main__':
+ model = MWT(num_classes=2)
+ input = torch.randn(1, 3, 224, 224)
+ flops, params = profile(model, inputs=(input,))
+ print(flops)
+ print(params)
+
diff --git a/backend/model_histo.py b/backend/model_histo.py
new file mode 100644
index 0000000000000000000000000000000000000000..ae0047e5354489f439bb9969bbd99ecd10536299
--- /dev/null
+++ b/backend/model_histo.py
@@ -0,0 +1,1495 @@
+"""
+Breast Cancer Histopathology Classification using Path Foundation Model
+
+This module implements a comprehensive deep learning pipeline for breast cancer classification
+from histopathology images using Google's Path Foundation model as a feature extractor. The
+system supports multiple datasets including BreakHis, PatchCamelyon (PCam), and BACH, employing
+transfer learning to achieve high classification accuracy.
+
+**Overview:**
+This system leverages Google's Path Foundation model, which is pre-trained on a large corpus
+of pathology images, to extract meaningful features from breast cancer histopathology images.
+The approach uses transfer learning where the foundation model serves as a frozen feature
+extractor, followed by a trainable classification head for binary classification (benign vs malignant).
+
+**Model Architecture:**
+- Foundation Model: Google's Path Foundation (pre-trained on pathology images)
+- Transfer Learning Approach: Feature extraction with frozen foundation model + trainable classifier head
+- Classification Head: Multi-layer dense network with regularisation and dropout
+- Optimisation: AdamW optimiser with learning rate scheduling and early stopping
+
+**Workflow:**
+1. Authentication & Model Loading: Authenticate with Hugging Face and load Path Foundation
+2. Data Loading: Load and preprocess histopathology datasets
+3. Feature Extraction: Extract embeddings using frozen foundation model
+4. Classifier Training: Train dense neural network on extracted features
+5. Evaluation: Comprehensive performance analysis with multiple metrics and visualisations
+
+**Supported Datasets:**
+- BreakHis: Breast cancer histopathology images at multiple magnifications
+- PatchCamelyon (PCam): Lymph node metastasis detection patches
+- BACH: ICIAR 2018 Breast Cancer Histology Challenge dataset
+- Combined: Ensemble of all three datasets for robust training
+
+**Key Features:**
+- Multiple dataset support with consistent pre-processing
+- Robust error handling and fallback mechanisms
+- Comprehensive evaluation metrics and visualisation
+- Memory-efficient batch processing
+- Data augmentation capabilities
+- Model persistence and deployment support
+
+Author: Research Team
+Date: 2024
+License: MIT
+"""
+
+# Import required libraries and configure environment
+import os
+import tensorflow as tf
+import numpy as np
+from PIL import Image
+from sklearn.metrics import classification_report, confusion_matrix, accuracy_score, precision_score, recall_score, f1_score
+from pathlib import Path
+import h5py
+from sklearn.model_selection import train_test_split
+from sklearn.utils.class_weight import compute_class_weight
+from tensorflow.keras import regularizers
+import matplotlib
+# Use a non-interactive backend to prevent blocking on plt.show()
+matplotlib.use('Agg')
+import matplotlib.pyplot as plt
+import seaborn as sns
+
+# Suppress TensorFlow logging for cleaner output
+os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
+
+# Configure TensorFlow logging for cleaner output
+try:
+ tf.get_logger().setLevel('ERROR')
+except AttributeError:
+ import logging
+ logging.getLogger('tensorflow').setLevel(logging.ERROR)
+
+# Configure Hugging Face Hub integration with fallback mechanisms
+# This section handles the loading of Google's Path Foundation model from Hugging Face Hub
+# with multiple fallback methods to ensure compatibility across different environments
+try:
+ from huggingface_hub import login, hf_hub_download, snapshot_download
+
+ # Try different methods for loading Keras models from HF Hub
+ # Method 1: Direct Keras loading (preferred)
+ try:
+ from huggingface_hub import from_pretrained_keras
+ KERAS_METHOD = "from_pretrained_keras"
+ except ImportError:
+ # Method 2: Transformers library fallback
+ try:
+ from transformers import TFAutoModel
+ KERAS_METHOD = "transformers"
+ except ImportError:
+ # Method 3: Manual download and TFSMLayer
+ KERAS_METHOD = "manual"
+
+ HF_AVAILABLE = True
+ print(f"Hugging Face Hub loaded successfully (method: {KERAS_METHOD})")
+except ImportError as e:
+ print(f"Hugging Face Hub unavailable: {e}")
+ print("Please install required packages: pip install huggingface_hub transformers")
+ HF_AVAILABLE = False
+ KERAS_METHOD = None
+
+class BreastCancerClassifier:
+ """
+ A comprehensive breast cancer classification system using Path Foundation model.
+
+ This class implements a transfer learning approach where Google's Path Foundation
+ model serves as a feature extractor, followed by a trainable classification head.
+ The system supports both feature extraction (frozen foundation model) and
+ fine-tuning approaches for maximum flexibility.
+
+ The classifier can work with multiple histopathology datasets and provides
+ comprehensive evaluation capabilities including confusion matrices, classification
+ reports, and performance metrics.
+
+ Attributes:
+ fine_tune (bool): Whether to fine-tune the foundation model or use it frozen
+ model (tf.keras.Model): The complete classification model
+ path_foundation: The loaded Path Foundation model from Hugging Face Hub
+ history: Training history from model.fit() containing loss and accuracy curves
+ embedding_dim (int): Dimensionality of extracted embeddings from foundation model
+ num_classes (int): Number of output classes (default: 2 for binary classification)
+
+ Example:
+ >>> classifier = BreastCancerClassifier(fine_tune=False)
+ >>> classifier.authenticate_huggingface()
+ >>> classifier.load_path_foundation()
+ >>> # Load data and train...
+ """
+
+ def __init__(self, fine_tune=False):
+ """
+ Initialise the breast cancer classifier.
+
+ Args:
+ fine_tune (bool): If True, allows fine-tuning of foundation model.
+ If False, uses foundation model as frozen feature extractor.
+
+ Note: Fine-tuning requires more computational resources and
+ may lead to overfitting on smaller datasets. Feature extraction
+ (fine_tune=False) is recommended for most use-cases.
+ """
+ self.fine_tune = fine_tune
+ self.model = None
+ self.path_foundation = None
+ self.history = None
+ self.embedding_dim = None
+ self.num_classes = 2 # Binary classification: benign vs malignant
+
+ def authenticate_huggingface(self, token=None):
+ """
+ Authenticate with Hugging Face Hub to access Path Foundation model.
+
+ This method handles authentication with Hugging Face Hub, which is required
+ to download and use Google's Path Foundation model. It supports multiple
+ token sources and provides fallback mechanisms.
+
+ Args:
+ token (str, optional): Hugging Face access token. If None, the method
+ will attempt to use environment variables:
+ - HF_TOKEN
+ - HUGGINGFACE_HUB_TOKEN
+
+ Returns:
+ bool: True if authentication successful, False otherwise
+
+ Note:
+ You can obtain a Hugging Face token by:
+ 1. Creating an account at https://huggingface.co
+ 2. Going to Settings > Access Tokens
+ 3. Creating a new token with read permissions
+
+ Example:
+ >>> classifier = BreastCancerClassifier()
+ >>> success = classifier.authenticate_huggingface("hf_xxxxxxxxxxxx")
+ >>> if success:
+ ... print("Authentication successful")
+ """
+ if not HF_AVAILABLE:
+ print("Cannot authenticate - Hugging Face Hub not available")
+ return False
+
+ try:
+ # Try multiple token sources: parameter, environment variables
+ final_token = token or os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN")
+
+ if final_token:
+ login(token=final_token, add_to_git_credential=False)
+ print("Hugging Face authentication successful")
+ return True
+ else:
+ print("No token provided, attempting to use cached login")
+ return True
+ except Exception as e:
+ print(f"Authentication failed: {e}")
+ return False
+
+ def load_path_foundation(self):
+ """
+ Load Google's Path Foundation model with multiple fallback mechanisms.
+
+ This method attempts to load the Path Foundation model using three different
+ approaches to ensure maximum compatibility across different environments:
+
+ 1. Direct Keras loading via huggingface_hub (preferred)
+ 2. Transformers library loading (fallback)
+ 3. Manual download and TFSMLayer loading (last resort)
+
+ The method also configures the model's training behavior based on the
+ fine_tune parameter set during initialization.
+
+ Returns:
+ bool: True if model loaded successfully, False otherwise
+
+ Raises:
+ Various exceptions may be raised during the loading process, but they
+ are caught and handled gracefully with informative error messages.
+
+ Note:
+ The Path Foundation model is a large pre-trained model (~1GB) that will
+ be downloaded on first use. Subsequent runs will use the cached version.
+
+ Example:
+ >>> classifier = BreastCancerClassifier(fine_tune=False)
+ >>> if classifier.load_path_foundation():
+ ... print("Model loaded successfully")
+ ... else:
+ ... print("Failed to load model")
+ """
+ if not HF_AVAILABLE:
+ print("Cannot load model - Hugging Face Hub unavailable")
+ return False
+
+ try:
+ print("Loading Path Foundation model...")
+ loaded = False
+
+ # Method 1: Direct Keras loading (preferred method)
+ if KERAS_METHOD == "from_pretrained_keras":
+ try:
+ self.path_foundation = from_pretrained_keras("google/path-foundation")
+ loaded = True
+ print("Successfully loaded via from_pretrained_keras")
+ except Exception as e:
+ print(f"Keras loading failed: {e}")
+
+ # Method 2: Transformers library fallback
+ if not loaded and KERAS_METHOD == "transformers":
+ try:
+ print("Attempting transformers fallback...")
+ self.path_foundation = TFAutoModel.from_pretrained("google/path-foundation")
+ loaded = True
+ print("Successfully loaded via transformers")
+ except Exception as e:
+ print(f"Transformers loading failed: {e}")
+
+ # Method 3: Manual download and TFSMLayer (last resort)
+ if not loaded:
+ try:
+ try:
+ import keras as _standalone_keras
+ except ImportError as _e:
+ print(f"Keras 3 not installed: {_e}")
+ return False
+
+ print("Attempting manual download and TFSMLayer loading...")
+ local_dir = snapshot_download(repo_id="google/path-foundation")
+ self.path_foundation = _standalone_keras.layers.TFSMLayer(
+ local_dir, call_endpoint="serving_default"
+ )
+ loaded = True
+ print("Successfully loaded via TFSMLayer")
+ except Exception as e:
+ print(f"TFSMLayer loading failed: {e}")
+ return False
+
+ # Configure training behavior based on fine_tune setting
+ if self.fine_tune:
+ self.path_foundation.trainable = True
+ try:
+ # Only fine-tune the last 3 layers for stability
+ for layer in self.path_foundation.layers[:-3]:
+ layer.trainable = False
+ print("Fine-tuning enabled: last 3 layers trainable")
+ except:
+ print("Fine-tuning enabled: full model trainable")
+ else:
+ self.path_foundation.trainable = False
+ print("Model frozen for feature extraction")
+
+ return True
+
+ except Exception as e:
+ print(f"Failed to load Path Foundation model: {e}")
+ return False
+
+ def preprocess_image_batch(self, images):
+ """
+ Pre-process a batch of images for Path Foundation model input.
+
+ This method handles multiple input formats and ensures all images are properly
+ formatted for the Path Foundation model. It performs the following operations:
+ - Resizes all images to 224x224 pixels (required by Path Foundation)
+ - Converts images to RGB format
+ - Normalises pixel values to [0, 1] range
+ - Handles both file paths and numpy arrays
+
+ Args:
+ images: List or array of images in various formats:
+ - File paths (strings) pointing to image files
+ - PIL Images
+ - NumPy arrays (various shapes and value ranges)
+
+ Returns:
+ np.ndarray: Preprocessed batch of shape (batch_size, 224, 224, 3)
+ with pixel values normalized to [0, 1] range
+
+ Note:
+ The method automatically handles different input formats and value ranges.
+ Images are resized using PIL's resize method with default interpolation.
+
+ Example:
+ >>> # Process file paths
+ >>> image_paths = ['image1.jpg', 'image2.png']
+ >>> processed = classifier.preprocess_image_batch(image_paths)
+ >>> print(processed.shape) # (2, 224, 224, 3)
+
+ >>> # Process numpy arrays
+ >>> image_arrays = [np.random.rand(100, 100, 3) for _ in range(5)]
+ >>> processed = classifier.preprocess_image_batch(image_arrays)
+ >>> print(processed.shape) # (5, 224, 224, 3)
+ """
+ processed = []
+
+ for img in images:
+ if isinstance(img, str):
+ # Handle file paths
+ img = Image.open(img).convert('RGB')
+ img = img.resize((224, 224))
+ img_array = np.array(img) / 255.0
+ else:
+ # Handle numpy arrays
+ if img.shape[:2] != (224, 224):
+ # Resize if necessary
+ if img.max() <= 1:
+ img_pil = Image.fromarray((img * 255).astype('uint8'))
+ else:
+ img_pil = Image.fromarray(img.astype('uint8'))
+ img_pil = img_pil.resize((224, 224))
+ img_array = np.array(img_pil) / 255.0
+ else:
+ img_array = img.astype('float32')
+ if img_array.max() > 1:
+ img_array = img_array / 255.0
+
+ processed.append(img_array)
+
+ return np.array(processed)
+
+ def extract_embeddings(self, images, batch_size=16):
+ """
+ Extract feature embeddings from images using Path Foundation model.
+
+ This method processes images in batches to extract high-level feature representations
+ using the pre-trained Path Foundation model. The embeddings capture semantic information
+ about the histopathology images that can be used for classification.
+
+ The method handles different model interface types and provides progress tracking
+ for large datasets. It automatically determines the embedding dimension on first use.
+
+ Args:
+ images: Array of preprocessed images or list of image paths
+ batch_size (int): Number of images to process per batch. Smaller batches
+ use less memory but may be slower. Default: 16
+
+ Returns:
+ np.ndarray: Extracted embeddings of shape (num_images, embedding_dim)
+ where embedding_dim is determined by the Path Foundation model
+
+ Raises:
+ ValueError: If no embeddings are successfully extracted
+ RuntimeError: If the Path Foundation model is not loaded
+
+ Note:
+ The embedding dimension is automatically determined from the first successful
+ batch and stored in self.embedding_dim for use in classifier construction.
+
+ Example:
+ >>> # Extract embeddings from a dataset
+ >>> embeddings = classifier.extract_embeddings(images, batch_size=32)
+ >>> print(f"Extracted {embeddings.shape[0]} embeddings of dimension {embeddings.shape[1]}")
+
+ >>> # Process with smaller batch size for memory-constrained environments
+ >>> embeddings = classifier.extract_embeddings(images, batch_size=8)
+ """
+ print(f"Extracting embeddings from {len(images)} images...")
+
+ embeddings = []
+ num_batches = (len(images) + batch_size - 1) // batch_size
+
+ for i in range(0, len(images), batch_size):
+ batch = images[i:i + batch_size]
+ processed_batch = self.preprocess_image_batch(batch)
+
+ try:
+ # Handle different model interface types
+ if hasattr(self.path_foundation, 'signatures') and "serving_default" in self.path_foundation.signatures:
+ # TensorFlow SavedModel format
+ infer = self.path_foundation.signatures["serving_default"]
+ batch_embeddings = infer(tf.constant(processed_batch))
+ elif hasattr(self.path_foundation, 'predict'):
+ # Standard Keras model
+ batch_embeddings = self.path_foundation.predict(processed_batch, verbose=0)
+ else:
+ # Direct callable
+ batch_embeddings = self.path_foundation(processed_batch)
+
+ # Handle different output formats
+ if isinstance(batch_embeddings, dict):
+ key = list(batch_embeddings.keys())[0]
+ if hasattr(batch_embeddings[key], 'numpy'):
+ batch_embeddings = batch_embeddings[key].numpy()
+ else:
+ batch_embeddings = batch_embeddings[key]
+ elif hasattr(batch_embeddings, 'numpy'):
+ batch_embeddings = batch_embeddings.numpy()
+
+ embeddings.append(batch_embeddings)
+
+ # Progress reporting
+ batch_num = i // batch_size + 1
+ if batch_num % 10 == 0:
+ print(f"Processed batch {batch_num}/{num_batches}")
+
+ except Exception as e:
+ print(f"Error processing batch {batch_num}: {e}")
+ continue
+
+ if not embeddings:
+ raise ValueError("No embeddings extracted successfully")
+
+ final_embeddings = np.vstack(embeddings)
+
+ # Set embedding dimension for classifier head
+ if self.embedding_dim is None:
+ self.embedding_dim = final_embeddings.shape[1]
+ print(f"Embedding dimension: {self.embedding_dim}")
+
+ print(f"Final embeddings shape: {final_embeddings.shape}")
+ return final_embeddings
+
+ def build_classifier(self):
+ """
+ Build the classification head architecture.
+
+ This method constructs the neural network architecture for breast cancer classification.
+ It creates different architectures based on the fine_tune setting:
+
+ 1. End-to-end model (fine_tune=True): Input -> Path Foundation -> Classifier -> Output
+ 2. Feature-based model (fine_tune=False): Embeddings -> Classifier -> Output
+
+ The architecture includes:
+ - Progressive dimensionality reduction (768 -> 384 -> 192 -> 2)
+ - L2 regularisation for weight decay and overfitting prevention
+ - Batch normalisation for training stability and faster convergence
+ - Dropout layers for regularization
+ - AdamW optimizer with appropriate learning rates
+
+ Returns:
+ None: The model is stored in self.model and compiled
+
+ Raises:
+ ValueError: If embedding dimension is not set (run extract_embeddings first)
+
+ Note:
+ The method automatically selects appropriate learning rates:
+ - Lower learning rate (1e-5) for fine-tuning to preserve pre-trained features
+ - Higher learning rate (0.001) for training from scratch on embeddings
+
+ Architecture Details:
+ - Input: Either raw images (224x224x3) or embeddings (embedding_dim,)
+ - Hidden layers: 768 -> 384 -> 192 neurons with ReLU activation
+ - Output: 2 neurons with softmax activation (benign/malignant)
+ - Regularisation: L2 weight decay (1e-4), Dropout (0.5, 0.3, 0.2)
+ - Normalisation: Batch normalisation after each dense layer
+
+ Example:
+ >>> classifier = BreastCancerClassifier(fine_tune=False)
+ >>> classifier.load_path_foundation()
+ >>> embeddings = classifier.extract_embeddings(images)
+ >>> classifier.build_classifier()
+ >>> print(f"Model has {classifier.model.count_params():,} parameters")
+ """
+ if self.embedding_dim is None:
+ raise ValueError("Embedding dimension not set - run extract_embeddings first")
+
+ if self.fine_tune:
+ # End-to-end fine-tuning architecture
+ inputs = tf.keras.Input(shape=(224, 224, 3))
+ x = self.path_foundation(inputs)
+
+ # Classification head with regularization
+ x = tf.keras.layers.Dense(768, activation='relu',
+ kernel_regularizer=regularizers.l2(1e-4))(x)
+ x = tf.keras.layers.BatchNormalization()(x)
+ x = tf.keras.layers.Dropout(0.5)(x)
+
+ x = tf.keras.layers.Dense(384, activation='relu',
+ kernel_regularizer=regularizers.l2(1e-4))(x)
+ x = tf.keras.layers.BatchNormalization()(x)
+ x = tf.keras.layers.Dropout(0.3)(x)
+
+ x = tf.keras.layers.Dense(192, activation='relu',
+ kernel_regularizer=regularizers.l2(1e-4))(x)
+ x = tf.keras.layers.Dropout(0.2)(x)
+
+ outputs = tf.keras.layers.Dense(self.num_classes, activation='softmax')(x)
+ self.model = tf.keras.Model(inputs, outputs)
+
+ # Lower learning rate for fine-tuning to preserve pre-trained features
+ optimizer = tf.keras.optimizers.AdamW(learning_rate=1e-5, weight_decay=1e-5)
+
+ else:
+ # Feature extraction architecture (recommended approach)
+ self.model = tf.keras.Sequential([
+ tf.keras.layers.Input(shape=(self.embedding_dim,)),
+
+ # First dense block
+ tf.keras.layers.Dense(768, activation='relu',
+ kernel_regularizer=regularizers.l2(1e-4)),
+ tf.keras.layers.BatchNormalization(),
+ tf.keras.layers.Dropout(0.5),
+
+ # Second dense block
+ tf.keras.layers.Dense(384, activation='relu',
+ kernel_regularizer=regularizers.l2(1e-4)),
+ tf.keras.layers.BatchNormalization(),
+ tf.keras.layers.Dropout(0.3),
+
+ # Third dense block
+ tf.keras.layers.Dense(192, activation='relu',
+ kernel_regularizer=regularizers.l2(1e-4)),
+ tf.keras.layers.Dropout(0.2),
+
+ # Output layer
+ tf.keras.layers.Dense(self.num_classes, activation='softmax')
+ ])
+
+ # Higher learning rate for training from scratch
+ optimizer = tf.keras.optimizers.AdamW(learning_rate=0.001, weight_decay=1e-5)
+
+ # Compile model with sparse categorical crossentropy for integer labels
+ self.model.compile(
+ optimizer=optimizer,
+ loss=tf.keras.losses.SparseCategoricalCrossentropy(),
+ metrics=['accuracy']
+ )
+
+ print(f"Model architecture built - Fine-tuning: {self.fine_tune}")
+ print(f"Total parameters: {self.model.count_params():,}")
+
+ def train_model(self, X_train, y_train, X_val, y_val, epochs=50):
+ """
+ Train the classification model with advanced techniques and callbacks.
+
+ This method implements a comprehensive training pipeline with:
+ - Class balancing to handle imbalanced datasets
+ - Early stopping to prevent overfitting
+ - Learning rate reduction on plateau
+ - Model checkpointing to save best weights
+ - Adaptive batch sizing based on training mode
+
+ Args:
+ X_train: Training features (embeddings or images)
+ y_train: Training labels (0 for benign, 1 for malignant)
+ X_val: Validation features
+ y_val: Validation labels
+ epochs (int): Maximum number of training epochs. Default: 50
+
+ Returns:
+ tf.keras.callbacks.History: Training history containing loss and accuracy curves
+
+ Note:
+ The method automatically handles class imbalance by computing balanced weights.
+ Training uses different batch sizes: 32 for fine-tuning, 64 for feature extraction.
+
+ Callbacks Used:
+ - EarlyStopping: Stops training if validation accuracy doesn't improve for 10 epochs
+ - ReduceLROnPlateau: Reduces learning rate by 50% if validation loss plateaus
+ - ModelCheckpoint: Saves the best model based on validation accuracy
+
+ Example:
+ >>> # Train the model
+ >>> history = classifier.train_model(X_train, y_train, X_val, y_val, epochs=30)
+ >>>
+ >>> # Access training metrics
+ >>> print(f"Final training accuracy: {history.history['accuracy'][-1]:.4f}")
+ >>> print(f"Final validation accuracy: {history.history['val_accuracy'][-1]:.4f}")
+ """
+ # Compute class weights to handle imbalanced datasets
+ try:
+ classes = np.unique(y_train)
+ weights = compute_class_weight(class_weight='balanced', classes=classes, y=y_train)
+ class_weight = {int(c): float(w) for c, w in zip(classes, weights)}
+ print(f"Class weights computed: {class_weight}")
+ except Exception:
+ class_weight = None
+ print("Using uniform class weights")
+
+ # Define training callbacks for robust training
+ callbacks = [
+ tf.keras.callbacks.EarlyStopping(
+ monitor='val_accuracy',
+ patience=10,
+ restore_best_weights=True,
+ verbose=1
+ ),
+ tf.keras.callbacks.ReduceLROnPlateau(
+ monitor='val_loss',
+ factor=0.5,
+ patience=5,
+ min_lr=1e-7,
+ verbose=1
+ ),
+ tf.keras.callbacks.ModelCheckpoint(
+ 'best_model.keras',
+ monitor='val_accuracy',
+ save_best_only=True,
+ verbose=0
+ )
+ ]
+
+ print("Starting model training...")
+ print(f"Training samples: {len(X_train)}, Validation samples: {len(X_val)}")
+
+ # Adaptive batch sizing based on training mode
+ batch_size = 32 if self.fine_tune else 64
+ print(f"Using batch size: {batch_size}")
+
+ # Train the model
+ self.history = self.model.fit(
+ X_train, y_train,
+ validation_data=(X_val, y_val),
+ epochs=epochs,
+ batch_size=batch_size,
+ callbacks=callbacks,
+ verbose=1,
+ class_weight=class_weight
+ )
+
+ print("Training completed successfully!")
+ return self.history
+
+ def evaluate_model(self, X_test, y_test):
+ """
+ Comprehensive model evaluation with multiple performance metrics and visualisations.
+
+ This method provides a thorough evaluation of the trained model including:
+ - Accuracy, Precision, Recall, and F1-score calculations
+ - Detailed classification report with per-class metrics
+ - Confusion matrix visualisation and analysis
+ - Model predictions and probabilities for further analysis
+
+ Args:
+ X_test: Test features (embeddings or images)
+ y_test: True test labels (0 for benign, 1 for malignant)
+
+ Returns:
+ dict: Dictionary containing comprehensive evaluation results:
+ - 'accuracy': Overall accuracy score
+ - 'precision': Weighted average precision
+ - 'recall': Weighted average recall
+ - 'f1': Weighted average F1-score
+ - 'predictions': Predicted class labels
+ - 'probabilities': Prediction probabilities for each class
+ - 'confusion_matrix': 2x2 confusion matrix
+
+ Note:
+ The method generates and saves a confusion matrix plot as 'confusion_matrix.png'
+ and displays it using matplotlib. The plot uses a blue color scheme for clarity.
+
+ Metrics Explanation:
+ - Accuracy: Overall correctness of predictions
+ - Precision: True positives / (True positives + False positives)
+ - Recall: True positives / (True positives + False negatives)
+ - F1-score: Harmonic mean of precision and recall
+
+ Example:
+ >>> # Evaluate the trained model
+ >>> results = classifier.evaluate_model(X_test, y_test)
+ >>>
+ >>> # Access specific metrics
+ >>> print(f"Test Accuracy: {results['accuracy']:.4f}")
+ >>> print(f"F1-Score: {results['f1']:.4f}")
+ >>>
+ >>> # Analyze predictions
+ >>> predictions = results['predictions']
+ >>> probabilities = results['probabilities']
+ """
+ print("Evaluating model performance...")
+
+ # Generate predictions and probabilities
+ y_pred_proba = self.model.predict(X_test)
+ y_pred = np.argmax(y_pred_proba, axis=1)
+
+ # Calculate comprehensive metrics
+ accuracy = accuracy_score(y_test, y_pred)
+ precision = precision_score(y_test, y_pred, average='weighted')
+ recall = recall_score(y_test, y_pred, average='weighted')
+ f1 = f1_score(y_test, y_pred, average='weighted')
+
+ # Display results
+ print(f"Accuracy: {accuracy:.4f} ({accuracy*100:.2f}%)")
+ print(f"Precision: {precision:.4f}")
+ print(f"Recall: {recall:.4f}")
+ print(f"F1-Score: {f1:.4f}")
+
+ # Detailed classification report
+ class_names = ['Benign', 'Malignant']
+ print("\nDetailed Classification Report:")
+ print(classification_report(y_test, y_pred, target_names=class_names))
+
+ # Generate and display confusion matrix
+ cm = confusion_matrix(y_test, y_pred)
+
+ # Create confusion matrix visualization
+ plt.figure(figsize=(8, 6))
+ sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
+ xticklabels=class_names, yticklabels=class_names)
+ plt.title('Confusion Matrix - Breast Cancer Classification')
+ plt.xlabel('Predicted Label')
+ plt.ylabel('True Label')
+ plt.tight_layout()
+ plt.savefig('confusion_matrix.png', dpi=300, bbox_inches='tight')
+ # Close the figure to free resources and avoid blocking
+ plt.close()
+
+ # Print confusion matrix in text format
+ print("\nConfusion Matrix:")
+ print(f" Predicted")
+ print(f" {class_names[0]:>8} {class_names[1]:>8}")
+ print(f"Actual {class_names[0]:>6} {cm[0,0]:>8} {cm[0,1]:>8}")
+ print(f" {class_names[1]:>6} {cm[1,0]:>8} {cm[1,1]:>8}")
+
+ return {
+ 'accuracy': accuracy,
+ 'precision': precision,
+ 'recall': recall,
+ 'f1': f1,
+ 'predictions': y_pred,
+ 'probabilities': y_pred_proba,
+ 'confusion_matrix': cm
+ }
+
+def load_breakhis_data(data_dir="datasets/breakhis/histology_slides/breast", max_samples_per_class=2000, magnification="40X"):
+ """
+ Load and preprocess the BreakHis breast cancer histopathology dataset.
+
+ The BreakHis dataset contains microscopic images of breast tumor tissue
+ collected from clinical studies. Images are organized by:
+ - Tumor type (benign/malignant)
+ - Specific histological type (adenosis, fibroadenoma, etc.)
+ - Patient ID
+ - Magnification level (40X, 100X, 200X, 400X)
+
+ This function loads images from the specified magnification level and
+ preprocesses them for use with the Path Foundation model.
+
+ Args:
+ data_dir (str): Path to BreakHis dataset root directory. Default structure:
+ datasets/breakhis/histology_slides/breast/
+ max_samples_per_class (int): Maximum images to load per class (benign/malignant).
+ Helps manage memory usage for large datasets.
+ magnification (str): Magnification level to use. Options: "40X", "100X", "200X", "400X".
+ Higher magnifications provide more detail but larger file sizes.
+
+ Returns:
+ tuple: (images, labels) as numpy arrays
+ - images: Array of shape (num_images, 224, 224, 3) with normalized pixel values
+ - labels: Array of shape (num_images,) with 0 for benign, 1 for malignant
+
+ Dataset Structure:
+ The function expects the following directory structure:
+ data_dir/
+ ├── benign/SOB/
+ │ ├── adenosis/
+ │ ├── fibroadenoma/
+ │ ├── phyllodes_tumor/
+ │ └── tubular_adenoma/
+ └── malignant/SOB/
+ ├── ductal_carcinoma/
+ ├── lobular_carcinoma/
+ ├── mucinous_carcinoma/
+ └── papillary_carcinoma/
+
+ Note:
+ Images are automatically resized to 224x224 pixels and normalized to [0,1] range.
+ The function handles various image formats (PNG, JPG, JPEG, TIF, TIFF).
+
+ Example:
+ >>> # Load BreakHis dataset with 40X magnification
+ >>> images, labels = load_breakhis_data(
+ ... data_dir="datasets/breakhis/histology_slides/breast",
+ ... max_samples_per_class=1000,
+ ... magnification="40X"
+ ... )
+ >>> print(f"Loaded {len(images)} images")
+ >>> print(f"Benign: {np.sum(labels == 0)}, Malignant: {np.sum(labels == 1)}")
+ """
+ print(f"Loading BreakHis dataset (magnification: {magnification})...")
+
+ benign_dir = os.path.join(data_dir, "benign", "SOB")
+ malignant_dir = os.path.join(data_dir, "malignant", "SOB")
+
+ images = []
+ labels = []
+
+ def load_images_from_category(base_dir, label, max_count):
+ """
+ Helper function to load images from a specific category (benign/malignant).
+
+ Traverses the directory structure: base_dir/tumor_type/patient_id/magnification/images
+ and loads images with progress reporting.
+ """
+ if not os.path.exists(base_dir):
+ print(f"Warning: Directory {base_dir} not found")
+ return 0
+
+ count = 0
+
+ # Traverse: base_dir/tumor_type/patient_id/magnification/images
+ for tumor_type in os.listdir(base_dir):
+ tumor_dir = os.path.join(base_dir, tumor_type)
+ if not os.path.isdir(tumor_dir):
+ continue
+
+ for patient_id in os.listdir(tumor_dir):
+ patient_dir = os.path.join(tumor_dir, patient_id)
+ if not os.path.isdir(patient_dir):
+ continue
+
+ mag_dir = os.path.join(patient_dir, magnification)
+ if not os.path.exists(mag_dir):
+ continue
+
+ for filename in os.listdir(mag_dir):
+ if count >= max_count:
+ return count
+
+ if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff')):
+ try:
+ img_path = os.path.join(mag_dir, filename)
+ img = Image.open(img_path).convert('RGB')
+ img = img.resize((224, 224))
+ img_array = np.array(img).astype('float32') / 255.0
+ images.append(img_array)
+ labels.append(label)
+ count += 1
+
+ if count % 100 == 0:
+ category = 'benign' if label == 0 else 'malignant'
+ print(f"Loaded {count} {category} images")
+
+ except Exception as e:
+ print(f"Error loading {filename}: {e}")
+ continue
+ return count
+
+ # Load both categories
+ benign_count = load_images_from_category(benign_dir, 0, max_samples_per_class)
+ malignant_count = load_images_from_category(malignant_dir, 1, max_samples_per_class)
+
+ print(f"BreakHis dataset loaded: {benign_count} benign, {malignant_count} malignant images")
+
+ return np.array(images), np.array(labels)
+
+def load_pcam_data(data_dir="datasets/pcam", label_dir="datasets/Labels", max_samples=3000, augment=True):
+ """
+ Load and preprocess the PatchCamelyon (PCam) dataset.
+
+ PCam contains 96x96 pixel patches extracted from histopathologic scans
+ of lymph node sections. Each patch is labeled with the presence of
+ metastatic tissue. This function includes data augmentation capabilities
+ to improve model generalization.
+
+ The dataset is stored in HDF5 format with separate files for images and labels,
+ and comes pre-split into training, validation, and test sets.
+
+ Args:
+ data_dir (str): Path to PCam image data directory containing:
+ - training_split.h5
+ - validation_split.h5
+ - test_split.h5
+ label_dir (str): Path to PCam label files directory containing:
+ - camelyonpatch_level_2_split_train_y.h5
+ - camelyonpatch_level_2_split_valid_y.h5
+ - camelyonpatch_level_2_split_test_y.h5
+ max_samples (int): Maximum total samples to load across all splits.
+ Distributed as: train=50%, val=25%, test=25%
+ augment (bool): Whether to apply data augmentation to training set.
+ Augmentation includes: horizontal flip, rotation, brightness adjustment
+
+ Returns:
+ dict: Dictionary with 'train', 'valid', 'test' keys containing (images, labels) tuples
+ - 'train': (train_images, train_labels) - Training data with optional augmentation
+ - 'valid': (val_images, val_labels) - Validation data
+ - 'test': (test_images, test_labels) - Test data
+
+ Dataset Details:
+ - Original patch size: 96x96 pixels
+ - Resized to: 224x224 pixels for Path Foundation compatibility
+ - Labels: 0 (normal tissue), 1 (metastatic tissue)
+ - Format: HDF5 files with 'x' key for images, 'y' key for labels
+
+ Data Augmentation (if enabled):
+ - Horizontal flip: 50% probability
+ - Rotation: Random 0°, 90°, 180°, or 270° rotation
+ - Brightness adjustment: 30% probability, factor between 0.9-1.1
+
+ Note:
+ The function automatically handles HDF5 file loading and memory management.
+ Images are resized from 96x96 to 224x224 pixels and normalized to [0,1] range.
+
+ Example:
+ >>> # Load PCam dataset with augmentation
+ >>> pcam_data = load_pcam_data(
+ ... data_dir="datasets/pcam",
+ ... label_dir="datasets/Labels",
+ ... max_samples=2000,
+ ... augment=True
+ ... )
+ >>>
+ >>> # Access training data
+ >>> train_images, train_labels = pcam_data['train']
+ >>> print(f"Training samples: {len(train_images)}")
+ >>> print(f"Image shape: {train_images[0].shape}")
+ """
+ print("Loading PatchCamelyon (PCam) dataset...")
+
+ # Define file paths
+ train_file = os.path.join(data_dir, "training_split.h5")
+ val_file = os.path.join(data_dir, "validation_split.h5")
+ test_file = os.path.join(data_dir, "test_split.h5")
+ train_label_file = os.path.join(label_dir, "camelyonpatch_level_2_split_train_y.h5")
+ val_label_file = os.path.join(label_dir, "camelyonpatch_level_2_split_valid_y.h5")
+ test_label_file = os.path.join(label_dir, "camelyonpatch_level_2_split_test_y.h5")
+
+ def preprocess(images):
+ """Resize and normalize images from 96x96 to 224x224 pixels."""
+ processed = []
+ for img in images:
+ im = Image.fromarray(img)
+ im = im.resize((224, 224)) # Resize to match Path Foundation input
+ arr = np.array(im).astype('float32') / 255.0
+ processed.append(arr)
+ return np.array(processed)
+
+ def safe_load(img_file, label_file, limit):
+ """Safely load data from HDF5 files with memory management."""
+ with h5py.File(img_file, 'r') as f_img, h5py.File(label_file, 'r') as f_lbl:
+ x = f_img['x'][:limit]
+ y = f_lbl['y'][:limit]
+ y = y.reshape(-1) # Ensure 1D label array
+ return x, y
+
+ # Load data splits with sample limits
+ train_images, train_labels = safe_load(train_file, train_label_file, max_samples//2)
+ val_images, val_labels = safe_load(val_file, val_label_file, max_samples//4)
+ test_images, test_labels = safe_load(test_file, test_label_file, max_samples//4)
+
+ # Preprocess all splits
+ train_images = preprocess(train_images)
+ val_images = preprocess(val_images)
+ test_images = preprocess(test_images)
+
+ # Apply data augmentation to training set
+ if augment:
+ print("Applying data augmentation to training set...")
+ for i in range(len(train_images)):
+ # Random horizontal flip
+ if np.random.rand() > 0.5:
+ train_images[i] = np.fliplr(train_images[i])
+
+ # Random rotation (0, 90, 180, 270 degrees)
+ k = np.random.randint(0, 4)
+ if k:
+ train_images[i] = np.rot90(train_images[i], k)
+
+ # Random brightness adjustment
+ if np.random.rand() > 0.7:
+ im = Image.fromarray((train_images[i] * 255).astype('uint8'))
+ brightness_factor = 0.9 + 0.2 * np.random.rand()
+ im = Image.fromarray(
+ np.clip(np.array(im, dtype=np.float32) * brightness_factor, 0, 255).astype('uint8')
+ )
+ train_images[i] = np.array(im).astype('float32') / 255.0
+
+ print(f"PCam dataset loaded - Train: {len(train_images)}, Val: {len(val_images)}, Test: {len(test_images)}")
+
+ return {
+ 'train': (train_images, train_labels),
+ 'valid': (val_images, val_labels),
+ 'test': (test_images, test_labels)
+ }
+
+def load_bach_data(data_dir="datasets/BACH/ICIAR2018_BACH_Challenge/Photos", max_samples=400, augment=True):
+ """
+ Load and preprocess the BACH (ICIAR 2018) breast cancer histology dataset.
+
+ BACH contains microscopy images classified into four categories:
+ - Normal tissue
+ - Benign lesions
+ - In situ carcinoma
+ - Invasive carcinoma
+
+ For binary classification, this function maps:
+ - Normal + Benign → Benign (label 0)
+ - In situ + Invasive → Malignant (label 1)
+
+ Args:
+ data_dir (str): Path to BACH dataset directory containing class subdirectories:
+ - Normal/
+ - Benign/
+ - InSitu/
+ - Invasive/
+ max_samples (int): Maximum total samples to load across all classes.
+ Distributed evenly across the 4 classes.
+ augment (bool): Whether to apply data augmentation (currently not implemented
+ for BACH dataset but parameter kept for consistency)
+
+ Returns:
+ dict: Dictionary with 'train', 'valid', 'test' keys containing (images, labels) tuples
+ - 'train': (train_images, train_labels) - Training data
+ - 'valid': (val_images, val_labels) - Validation data
+ - 'test': (test_images, test_labels) - Test data
+
+ Dataset Details:
+ - Original categories: 4 classes (Normal, Benign, InSitu, Invasive)
+ - Binary mapping: Normal(0), Benign(1) → Benign(0); InSitu(2), Invasive(3) → Malignant(1)
+ - Image format: TIF, TIFF, PNG, JPG, JPEG
+ - Resized to: 224x224 pixels for Path Foundation compatibility
+ - Normalized to: [0, 1] range
+
+ Data Splitting:
+ - Test set: 20% of total data
+ - Training set: 60% of total data (75% of remaining after test split)
+ - Validation set: 20% of total data (25% of remaining after test split)
+ - Stratified splitting to maintain class distribution
+
+ Note:
+ The function automatically handles the 4-class to binary classification mapping.
+ Images are resized to 224x224 pixels and normalized to [0,1] range.
+ The augment parameter is kept for API consistency but augmentation is not
+ currently implemented for the BACH dataset.
+
+ Example:
+ >>> # Load BACH dataset
+ >>> bach_data = load_bach_data(
+ ... data_dir="datasets/BACH/ICIAR2018_BACH_Challenge/Photos",
+ ... max_samples=400,
+ ... augment=True
+ ... )
+ >>>
+ >>> # Access training data
+ >>> train_images, train_labels = bach_data['train']
+ >>> print(f"Training samples: {len(train_images)}")
+ >>> print(f"Class distribution: Benign={np.sum(train_labels==0)}, Malignant={np.sum(train_labels==1)}")
+ """
+ print("Loading BACH (ICIAR 2018) dataset...")
+
+ # Original BACH categories mapped to binary classification
+ class_dirs = {
+ 'Normal': 0, # Normal tissue → Benign
+ 'Benign': 1, # Benign lesions → Benign
+ 'InSitu': 2, # In situ carcinoma → Malignant
+ 'Invasive': 3, # Invasive carcinoma → Malignant
+ }
+
+ images = []
+ labels = []
+ per_class_limit = None if not max_samples else max_samples // 4
+ counters = {0: 0, 1: 0, 2: 0, 3: 0}
+
+ # Load images from each category
+ for cls_name, cls_label in class_dirs.items():
+ cls_path = os.path.join(data_dir, cls_name)
+ if not os.path.isdir(cls_path):
+ print(f"Warning: Directory {cls_path} not found")
+ continue
+
+ for fname in os.listdir(cls_path):
+ if per_class_limit and counters[cls_label] >= per_class_limit:
+ break
+ if not fname.lower().endswith((".tif", ".tiff", ".png", ".jpg", ".jpeg")):
+ continue
+
+ fpath = os.path.join(cls_path, fname)
+ try:
+ im = Image.open(fpath).convert('RGB')
+ im = im.resize((224, 224))
+ arr = np.array(im).astype('float32') / 255.0
+ images.append(arr)
+ labels.append(cls_label)
+ counters[cls_label] += 1
+ except Exception as e:
+ print(f"Error loading {fname}: {e}")
+ continue
+
+ images = np.array(images)
+ labels = np.array(labels)
+
+ # Convert 4-class to binary classification
+ if labels.size > 0:
+ # Map: Normal(0), Benign(1) → Benign(0); InSitu(2), Invasive(3) → Malignant(1)
+ labels = np.where(np.isin(labels, [0, 1]), 0, 1)
+
+ print(f"BACH dataset loaded: {len(images)} images")
+ print(f"Class distribution - Benign: {np.sum(labels == 0)}, Malignant: {np.sum(labels == 1)}")
+
+ # Split into train/validation/test sets
+ X_temp, X_test, y_temp, y_test = train_test_split(
+ images, labels, test_size=0.2,
+ stratify=labels if len(set(labels)) > 1 else None,
+ random_state=42
+ )
+ X_train, X_val, y_train, y_val = train_test_split(
+ X_temp, y_temp, test_size=0.25,
+ stratify=y_temp if len(set(y_temp)) > 1 else None,
+ random_state=42
+ )
+
+ return {
+ 'train': (X_train, y_train),
+ 'valid': (X_val, y_val),
+ 'test': (X_test, y_test)
+ }
+
+def load_combined_data(dataset_choice="breakhis", max_samples=5000):
+ """
+ Unified data loading function supporting multiple datasets and combinations.
+
+ This function serves as the main entry point for data loading, supporting:
+ - Individual datasets (BreakHis, PCam, BACH)
+ - Combined dataset training for improved generalization
+ - Consistent data splitting and preprocessing across all datasets
+
+ The combined dataset approach leverages multiple histopathology datasets to
+ create a more robust and generalizable model by training on diverse data sources.
+
+ Args:
+ dataset_choice (str): Dataset to load. Options:
+ - "breakhis": BreakHis breast cancer histopathology dataset
+ - "pcam": PatchCamelyon lymph node metastasis dataset
+ - "bach": BACH ICIAR 2018 breast cancer histology dataset
+ - "combined": Ensemble of all three datasets for robust training
+ max_samples (int): Maximum total samples to load. For individual datasets,
+ this is the total limit. For combined datasets, this is
+ distributed across the constituent datasets.
+
+ Returns:
+ dict: Dictionary with 'train', 'valid', 'test' keys containing (images, labels) tuples
+ - 'train': (train_images, train_labels) - Training data
+ - 'valid': (val_images, val_labels) - Validation data
+ - 'test': (test_images, test_labels) - Test data
+
+ Dataset Combinations:
+ When dataset_choice="combined", the function:
+ 1. Loads BreakHis, PCam, and BACH datasets
+ 2. Combines their training data
+ 3. Shuffles the combined dataset
+ 4. Splits into train/validation/test sets
+ 5. Maintains class balance through stratified splitting
+
+ Sample Distribution (for combined datasets):
+ - BreakHis: max_samples // 6 (per-class limit)
+ - PCam: max_samples // 3 (total limit)
+ - BACH: max_samples // 3 (total limit)
+
+ Data Splitting:
+ - Test set: 20% of total data
+ - Training set: 60% of total data (75% of remaining after test split)
+ - Validation set: 20% of total data (25% of remaining after test split)
+ - Stratified splitting to maintain class distribution
+
+ Note:
+ All datasets are automatically preprocessed to 224x224 pixels and normalized
+ to [0,1] range for compatibility with the Path Foundation model.
+
+ Example:
+ >>> # Load individual dataset
+ >>> data = load_combined_data("breakhis", max_samples=2000)
+ >>>
+ >>> # Load combined dataset for robust training
+ >>> combined_data = load_combined_data("combined", max_samples=6000)
+ >>>
+ >>> # Access training data
+ >>> train_images, train_labels = combined_data['train']
+ >>> print(f"Combined training samples: {len(train_images)}")
+ """
+
+ if dataset_choice.lower() == "breakhis":
+ print("Loading BreakHis dataset only...")
+ images, labels = load_breakhis_data(max_samples_per_class=max_samples//2)
+
+ # Split into train/validation/test
+ X_temp, X_test, y_temp, y_test = train_test_split(
+ images, labels, test_size=0.2, stratify=labels, random_state=42
+ )
+
+ X_train, X_val, y_train, y_val = train_test_split(
+ X_temp, y_temp, test_size=0.25, stratify=y_temp, random_state=42
+ )
+
+ return {
+ 'train': (X_train, y_train),
+ 'valid': (X_val, y_val),
+ 'test': (X_test, y_test)
+ }
+
+ elif dataset_choice.lower() == "pcam":
+ return load_pcam_data(max_samples=max_samples)
+
+ elif dataset_choice.lower() == "bach":
+ return load_bach_data(max_samples=max_samples)
+
+ elif dataset_choice.lower() == "combined":
+ print("Loading combined datasets for enhanced generalization...")
+
+ # Distribute samples across datasets
+ if max_samples is None:
+ per_bh = None
+ per_pc = None
+ per_ba = None
+ else:
+ per_dataset = max(1, max_samples // 3)
+ per_bh = per_dataset // 2 # BreakHis uses per-class limit
+ per_pc = per_dataset
+ per_ba = per_dataset
+
+ # Load individual datasets
+ print("Loading BreakHis component...")
+ bh_images, bh_labels = load_breakhis_data(
+ max_samples_per_class=per_bh if per_bh else 10**9
+ )
+
+ print("Loading PCam component...")
+ pcam = load_pcam_data(max_samples=per_pc, augment=True)
+ pc_train_images, pc_train_labels = pcam["train"]
+
+ print("Loading BACH component...")
+ bach = load_bach_data(max_samples=per_ba, augment=True)
+ b_train_images, b_train_labels = bach["train"]
+
+ # Combine all datasets
+ images = np.concatenate([bh_images, pc_train_images, b_train_images], axis=0)
+ labels = np.concatenate([bh_labels, pc_train_labels, b_train_labels], axis=0)
+
+ print(f"Combined dataset: {len(images)} total images")
+ print(f"Final distribution - Benign: {np.sum(labels == 0)}, Malignant: {np.sum(labels == 1)}")
+
+ # Shuffle combined data
+ idx = np.arange(len(images))
+ np.random.shuffle(idx)
+ images, labels = images[idx], labels[idx]
+
+ # Split combined data
+ X_temp, X_test, y_temp, y_test = train_test_split(
+ images, labels, test_size=0.2,
+ stratify=labels if len(set(labels)) > 1 else None,
+ random_state=42
+ )
+ X_train, X_val, y_train, y_val = train_test_split(
+ X_temp, y_temp, test_size=0.25,
+ stratify=y_temp if len(set(y_temp)) > 1 else None,
+ random_state=42
+ )
+
+ return {
+ 'train': (X_train, y_train),
+ 'valid': (X_val, y_val),
+ 'test': (X_test, y_test)
+ }
+
+ else:
+ raise ValueError(f"Unknown dataset choice: {dataset_choice}. "
+ f"Choose from: 'breakhis', 'pcam', 'bach', 'combined'")
+
+def main():
+ """
+ Execute the complete breast cancer classification pipeline.
+
+ This function coordinates all components of the machine learning workflow:
+ 1. Environment validation and setup
+ 2. Model authentication and loading
+ 3. Dataset loading and preprocessing
+ 4. Feature extraction using Path Foundation
+ 5. Classifier training with advanced techniques
+ 6. Comprehensive model evaluation
+ 7. Model persistence for future use
+
+ The pipeline implements a robust transfer learning approach using Google's
+ Path Foundation model as a feature extractor, followed by a trainable
+ classification head for binary breast cancer classification.
+
+ Returns:
+ tuple: (classifier_instance, evaluation_results) or (None, None) if failed
+ - classifier_instance: Trained BreastCancerClassifier object
+ - evaluation_results: Dictionary containing performance metrics and predictions
+
+ Configuration:
+ The function uses global variables for configuration (can be modified):
+ - DATASET_CHOICE: Dataset to use ("breakhis", "pcam", "bach", "combined")
+ - MAX_SAMPLES: Maximum samples to load (adjust based on available memory)
+ - EPOCHS: Number of training epochs (default: 50)
+ - HF_TOKEN: Hugging Face authentication token (optional)
+
+ Pipeline Steps:
+ 1. Prerequisites Check: Validates required packages and dependencies
+ 2. Authentication: Authenticates with Hugging Face Hub
+ 3. Model Loading: Downloads and loads Path Foundation model
+ 4. Data Loading: Loads and preprocesses histopathology dataset
+ 5. Feature Extraction: Extracts embeddings using frozen foundation model
+ 6. Classifier Building: Constructs trainable classification head
+ 7. Training: Trains classifier with callbacks and monitoring
+ 8. Evaluation: Comprehensive performance assessment
+ 9. Model Saving: Persists trained model for future use
+
+ Error Handling:
+ The function includes comprehensive error handling with detailed error messages
+ and stack traces to aid in debugging and troubleshooting.
+
+ Example:
+ >>> # Run the complete pipeline
+ >>> classifier, results = main()
+ >>>
+ >>> if results:
+ ... print(f"Pipeline successful! Accuracy: {results['accuracy']:.4f}")
+ ... # Use the trained classifier for inference
+ ... else:
+ ... print("Pipeline failed - check error messages")
+
+ Note:
+ This function is designed to be run as a standalone script or imported
+ and called from other modules. It provides a complete end-to-end
+ machine learning pipeline for breast cancer classification.
+ """
+ print("="*60)
+ print("BREAST CANCER CLASSIFICATION WITH PATH FOUNDATION")
+ print("="*60)
+
+ # Validate prerequisites
+ if not HF_AVAILABLE:
+ print("ERROR: Prerequisites not met")
+ print("Required installations: pip install tensorflow huggingface_hub transformers")
+ return None, None
+
+ # Configuration parameters
+ EPOCHS = 50
+ HF_TOKEN = None # Set your Hugging Face token here if needed
+
+ # Global configuration (can be modified in notebook)
+ if 'DATASET_CHOICE' not in globals():
+ DATASET_CHOICE = 'combined' # Options: 'breakhis', 'pcam', 'bach', 'combined'
+ if 'MAX_SAMPLES' not in globals():
+ MAX_SAMPLES = 4000
+
+ print(f"Configuration:")
+ print(f" - Epochs: {EPOCHS}")
+ print(f" - Dataset: {DATASET_CHOICE}")
+ print(f" - Max samples: {MAX_SAMPLES}")
+ print(f" - Method: Feature extraction (frozen foundation model)")
+
+ try:
+ # Initialize classifier in feature extraction mode
+ classifier = BreastCancerClassifier(fine_tune=False)
+
+ print("\n" + "="*40)
+ print("STEP 1: HUGGING FACE AUTHENTICATION")
+ print("="*40)
+ if not classifier.authenticate_huggingface(HF_TOKEN):
+ raise Exception("Authentication failed - check your HF token")
+
+ print("\n" + "="*40)
+ print("STEP 2: LOADING PATH FOUNDATION MODEL")
+ print("="*40)
+ if not classifier.load_path_foundation():
+ raise Exception("Model loading failed - check network connection")
+
+ print("\n" + "="*40)
+ print(f"STEP 3: LOADING {DATASET_CHOICE.upper()} DATASET")
+ print("="*40)
+ data = load_combined_data(DATASET_CHOICE, MAX_SAMPLES)
+
+ X_train, y_train = data['train']
+ X_val, y_val = data['valid']
+ X_test, y_test = data['test']
+
+ print(f"Dataset splits:")
+ print(f" - Training: {len(X_train)} samples")
+ print(f" - Validation: {len(X_val)} samples")
+ print(f" - Test: {len(X_test)} samples")
+
+ print("\n" + "="*40)
+ print("STEP 4: EXTRACTING FEATURE EMBEDDINGS")
+ print("="*40)
+ print("Extracting training embeddings...")
+ X_train = classifier.extract_embeddings(X_train)
+ print("Extracting validation embeddings...")
+ X_val = classifier.extract_embeddings(X_val)
+ print("Extracting test embeddings...")
+ X_test = classifier.extract_embeddings(X_test)
+
+ print("\n" + "="*40)
+ print("STEP 5: BUILDING CLASSIFICATION HEAD")
+ print("="*40)
+ classifier.num_classes = 2
+ classifier.build_classifier()
+
+ print("\n" + "="*40)
+ print("STEP 6: TRAINING CLASSIFIER")
+ print("="*40)
+ classifier.train_model(X_train, y_train, X_val, y_val, EPOCHS)
+
+ print("\n" + "="*40)
+ print("STEP 7: MODEL EVALUATION")
+ print("="*40)
+ results = classifier.evaluate_model(X_test, y_test)
+
+ # Save trained model
+ model_name = f"{DATASET_CHOICE}_breast_cancer_classifier.keras"
+ classifier.model.save(model_name)
+ print(f"\nModel saved as: {model_name}")
+
+ print("\n" + "="*60)
+ print("PIPELINE COMPLETED SUCCESSFULLY")
+ print("="*60)
+ print(f"Final Performance Metrics:")
+ print(f" - Accuracy: {results['accuracy']:.4f} ({results['accuracy']*100:.2f}%)")
+ print(f" - F1-Score: {results['f1']:.4f}")
+ print(f" - Precision: {results['precision']:.4f}")
+ print(f" - Recall: {results['recall']:.4f}")
+
+ return classifier, results
+
+ except Exception as e:
+ print(f"\nERROR: Pipeline failed - {e}")
+ import traceback
+ traceback.print_exc()
+ return None, None
+
+# Script execution section
+if __name__ == "__main__":
+ """
+ Main execution block for running the breast cancer classification pipeline.
+
+ This section is executed when the script is run directly (not imported).
+ It provides a simple interface to run the complete machine learning pipeline
+ and displays the final results.
+
+ Usage:
+ python model2.py
+
+ The script will:
+ 1. Initialize and run the complete pipeline
+ 2. Display progress and intermediate results
+ 3. Show final performance metrics
+ 4. Save the trained model for future use
+ """
+ print("Starting Breast Cancer Classification Pipeline...")
+ print("This may take several minutes depending on your hardware and dataset size.")
+ print("="*60)
+
+ # Execute the complete pipeline
+ classifier, results = main()
+
+ # Display final results
+ if results:
+ print("\n" + "="*60)
+ print("🎉 PIPELINE EXECUTION SUCCESSFUL! 🎉")
+ print("="*60)
+ print(f"Final Accuracy: {results['accuracy']:.4f} ({results['accuracy']*100:.2f}%)")
+ print(f"F1-Score: {results['f1']:.4f}")
+ print(f"Precision: {results['precision']:.4f}")
+ print(f"Recall: {results['recall']:.4f}")
+ print("\nThe trained model has been saved and is ready for inference!")
+ print("You can now use the classifier for breast cancer classification tasks.")
+ else:
+ print("\n" + "="*60)
+ print("❌ PIPELINE EXECUTION FAILED ❌")
+ print("="*60)
+ print("Please check the error messages above for troubleshooting.")
+ print("Common issues:")
+ print("- Missing dependencies (install with: pip install tensorflow huggingface_hub transformers)")
+ print("- Network connectivity issues (for downloading Path Foundation model)")
+ print("- Insufficient memory (reduce MAX_SAMPLES parameter)")
+ print("- Invalid dataset paths (check dataset directory structure)")
\ No newline at end of file
diff --git a/backend/requirements.txt b/backend/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..f72b26bda849f417f1647f5a2fcba47bb81325f3
--- /dev/null
+++ b/backend/requirements.txt
@@ -0,0 +1,14 @@
+fastapi
+uvicorn
+pillow
+ultralytics
+tensorflow
+numpy
+huggingface_hub
+joblib
+scikit-learn
+scikit-image
+loguru
+thop
+seaborn
+python-multipart
diff --git a/backend/yolo_colposcopy.pt b/backend/yolo_colposcopy.pt
new file mode 100644
index 0000000000000000000000000000000000000000..194bfe4b33b7b7eeef2ef363044029d3bdf756a0
--- /dev/null
+++ b/backend/yolo_colposcopy.pt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f31fd29763c04b071b449db7bfa09743527f30a62913f25d5a6097366c4bf3b4
+size 6235498
diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs
new file mode 100644
index 0000000000000000000000000000000000000000..d6c953795300e4256c76542d6bb0fe06f08b5ad6
--- /dev/null
+++ b/frontend/.eslintrc.cjs
@@ -0,0 +1,18 @@
+module.exports = {
+ root: true,
+ env: { browser: true, es2020: true },
+ extends: [
+ 'eslint:recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:react-hooks/recommended',
+ ],
+ ignorePatterns: ['dist', '.eslintrc.cjs'],
+ parser: '@typescript-eslint/parser',
+ plugins: ['react-refresh'],
+ rules: {
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ },
+}
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..a547bf36d8d11a4f89c59c144f24795749086dd1
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..082515fd2b6554c3911a2553138c448e3ce13a08
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,8 @@
+# Magic Patterns - Vite Template
+
+This code was generated by [Magic Patterns](https://magicpatterns.com) for this design: [Source Design](https://www.magicpatterns.com/c/jitk86q9nv1at6tcwj7cxr)
+
+## Getting Started
+
+1. Run `npm install`
+2. Run `npm run dev`
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..bb3c064708c64d08b050c9ae4a8fe83b774d3a2c
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Manalife AI Pathology Assistant
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..8cf3cb8a696df97da7964fb16b6d7feb55896381
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,4776 @@
+{
+ "name": "magic-patterns-vite-template",
+ "version": "0.0.1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "magic-patterns-vite-template",
+ "version": "0.0.1",
+ "dependencies": {
+ "axios": "^1.12.2",
+ "lucide-react": "0.522.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.26.2"
+ },
+ "devDependencies": {
+ "@types/node": "^20.11.18",
+ "@types/react": "^18.3.1",
+ "@types/react-dom": "^18.3.1",
+ "@typescript-eslint/eslint-plugin": "^5.54.0",
+ "@typescript-eslint/parser": "^5.54.0",
+ "@vitejs/plugin-react": "^4.2.1",
+ "autoprefixer": "latest",
+ "eslint": "^8.50.0",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.1",
+ "postcss": "latest",
+ "tailwindcss": "3.4.17",
+ "typescript": "^5.5.4",
+ "vite": "^5.2.0"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz",
+ "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
+ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.3",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.4",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.4",
+ "@babel/types": "^7.28.4",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
+ "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.3",
+ "@babel/types": "^7.28.2",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
+ "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.4"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
+ "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.3",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.4",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
+ "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
+ "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
+ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+ "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
+ "deprecated": "Use @eslint/config-array instead",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^2.0.3",
+ "debug": "^4.3.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
+ "deprecated": "Use @eslint/object-schema instead",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.23.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
+ "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
+ "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
+ "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
+ "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
+ "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
+ "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
+ "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
+ "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
+ "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
+ "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
+ "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
+ "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
+ "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
+ "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
+ "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
+ "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
+ "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
+ "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
+ "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
+ "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
+ "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
+ "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
+ "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.23",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz",
+ "integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.26",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz",
+ "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@types/semver": {
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
+ "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
+ "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.4.0",
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/type-utils": "5.62.0",
+ "@typescript-eslint/utils": "5.62.0",
+ "debug": "^4.3.4",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "natural-compare-lite": "^1.4.0",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^5.0.0",
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
+ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
+ "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/visitor-keys": "5.62.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
+ "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "@typescript-eslint/utils": "5.62.0",
+ "debug": "^4.3.4",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
+ "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
+ "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/visitor-keys": "5.62.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
+ "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@types/json-schema": "^7.0.9",
+ "@types/semver": "^7.3.12",
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "eslint-scope": "^5.1.1",
+ "semver": "^7.3.7"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
+ "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.21",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
+ "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.24.4",
+ "caniuse-lite": "^1.0.30001702",
+ "fraction.js": "^4.3.7",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/axios": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
+ "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.8.19",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.19.tgz",
+ "integrity": "sha512-zoKGUdu6vb2jd3YOq0nnhEDQVbPcHhco3UImJrv5dSkvxTc2pl2WjOPsjZXDwPDSl5eghIMuY3R6J9NDKF3KcQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.26.3",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
+ "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.8.9",
+ "caniuse-lite": "^1.0.30001746",
+ "electron-to-chromium": "^1.5.227",
+ "node-releases": "^2.0.21",
+ "update-browserslist-db": "^1.1.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001751",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
+ "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.238",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.238.tgz",
+ "integrity": "sha512-khBdc+w/Gv+cS8e/Pbnaw/FXcBUeKrRVik9IxfXtgREOWyJhR4tj43n3amkVogJ/yeQUqzkrZcFhtIxIdqmmcQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
+ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
+ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.57.1",
+ "@humanwhocodes/config-array": "^0.13.0",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
+ "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz",
+ "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esquery/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esrecurse/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
+ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "patreon",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "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.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.522.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.522.0.tgz",
+ "integrity": "sha512-jnJbw974yZ7rQHHEFKJOlWAefG3ATSCZHANZxIdx8Rk/16siuwjgA4fBULpXEAWx/RlTs3FzmKW/udWUuO0aRw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/natural-compare-lite": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
+ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.26",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz",
+ "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
+ "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.0.0",
+ "yaml": "^2.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "postcss": ">=8.0.9",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.30.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
+ "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
+ "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.0",
+ "react-router": "6.30.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
+ "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.52.5",
+ "@rollup/rollup-android-arm64": "4.52.5",
+ "@rollup/rollup-darwin-arm64": "4.52.5",
+ "@rollup/rollup-darwin-x64": "4.52.5",
+ "@rollup/rollup-freebsd-arm64": "4.52.5",
+ "@rollup/rollup-freebsd-x64": "4.52.5",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.52.5",
+ "@rollup/rollup-linux-arm-musleabihf": "4.52.5",
+ "@rollup/rollup-linux-arm64-gnu": "4.52.5",
+ "@rollup/rollup-linux-arm64-musl": "4.52.5",
+ "@rollup/rollup-linux-loong64-gnu": "4.52.5",
+ "@rollup/rollup-linux-ppc64-gnu": "4.52.5",
+ "@rollup/rollup-linux-riscv64-gnu": "4.52.5",
+ "@rollup/rollup-linux-riscv64-musl": "4.52.5",
+ "@rollup/rollup-linux-s390x-gnu": "4.52.5",
+ "@rollup/rollup-linux-x64-gnu": "4.52.5",
+ "@rollup/rollup-linux-x64-musl": "4.52.5",
+ "@rollup/rollup-openharmony-arm64": "4.52.5",
+ "@rollup/rollup-win32-arm64-msvc": "4.52.5",
+ "@rollup/rollup-win32-ia32-msvc": "4.52.5",
+ "@rollup/rollup-win32-x64-gnu": "4.52.5",
+ "@rollup/rollup-win32-x64-msvc": "4.52.5",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/string-width/node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.0",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
+ "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "glob": "^10.3.10",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/sucrase/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/sucrase/node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/sucrase/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.17",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
+ "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.6",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/tsutils": {
+ "version": "3.21.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+ "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^1.8.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+ "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yaml": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
+ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..d6cae821ed6f7b142d7d48d87b818d421d504724
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "magic-patterns-vite-template",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "npx vite",
+ "build": "npx vite build",
+ "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
+ "preview": "npx vite preview"
+ },
+ "dependencies": {
+ "axios": "^1.12.2",
+ "lucide-react": "0.522.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.26.2"
+ },
+ "devDependencies": {
+ "@types/node": "^20.11.18",
+ "@types/react": "^18.3.1",
+ "@types/react-dom": "^18.3.1",
+ "@typescript-eslint/eslint-plugin": "^5.54.0",
+ "@typescript-eslint/parser": "^5.54.0",
+ "@vitejs/plugin-react": "^4.2.1",
+ "autoprefixer": "latest",
+ "eslint": "^8.50.0",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.1",
+ "postcss": "latest",
+ "tailwindcss": "3.4.17",
+ "typescript": "^5.5.4",
+ "vite": "^5.2.0"
+ }
+}
diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..2e7af2b7f1a6f391da1631d93968a9d487ba977d
--- /dev/null
+++ b/frontend/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/frontend/public/banner.jpeg b/frontend/public/banner.jpeg
new file mode 100644
index 0000000000000000000000000000000000000000..8bc949d0a57ef30e5b7fc8a15abc3f4aedfcbc96
Binary files /dev/null and b/frontend/public/banner.jpeg differ
diff --git a/frontend/public/black_logo.png b/frontend/public/black_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..dd13ea113369c6b83b34a244b594c02dc1c26d02
Binary files /dev/null and b/frontend/public/black_logo.png differ
diff --git a/frontend/public/colpo/colp1.jpg b/frontend/public/colpo/colp1.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..03fe9c522a6192401eef3adb7ee737ffd4d61326
Binary files /dev/null and b/frontend/public/colpo/colp1.jpg differ
diff --git a/frontend/public/colpo/colp2.jpg b/frontend/public/colpo/colp2.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..fab09d0512eab9d2b7b391f223497e0bba19437c
Binary files /dev/null and b/frontend/public/colpo/colp2.jpg differ
diff --git a/frontend/public/colpo/colp3.jpg b/frontend/public/colpo/colp3.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..1e650f8ca6ef4208372f9c9473df1d90afd05761
Binary files /dev/null and b/frontend/public/colpo/colp3.jpg differ
diff --git a/frontend/public/cyto/cyt1.jpg b/frontend/public/cyto/cyt1.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..966f3c5446c5070a8b0cdcd0262e24d6d50c2100
--- /dev/null
+++ b/frontend/public/cyto/cyt1.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5272c04ec47e122e332f3197d8248298eb40f496b51cfe4c37f3f43ec5a9ea2c
+size 716187
diff --git a/frontend/public/cyto/cyt2.png b/frontend/public/cyto/cyt2.png
new file mode 100644
index 0000000000000000000000000000000000000000..836817f7875594687022213c04acbd4131ba78d8
--- /dev/null
+++ b/frontend/public/cyto/cyt2.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:339cb5b78762ac985f76a36be2165f4e2c8c473d1028957cb81d7f3a2050276d
+size 470329
diff --git a/frontend/public/cyto/cyt3.png b/frontend/public/cyto/cyt3.png
new file mode 100644
index 0000000000000000000000000000000000000000..5a32ef1ccd916e262aaa453724fa0bff32b31091
--- /dev/null
+++ b/frontend/public/cyto/cyt3.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7533ab6eea48e4a4671c14c87334f8c387e39bf09e7995bbc960db6b04c2cba7
+size 5858373
diff --git a/frontend/public/histo/hist1.png b/frontend/public/histo/hist1.png
new file mode 100644
index 0000000000000000000000000000000000000000..5a560817061efa9cdc71efcec91a5c0e75dce3bc
Binary files /dev/null and b/frontend/public/histo/hist1.png differ
diff --git a/frontend/public/histo/hist2.png b/frontend/public/histo/hist2.png
new file mode 100644
index 0000000000000000000000000000000000000000..21037234e2bf7d358e60bd18767ecd8001dbe501
Binary files /dev/null and b/frontend/public/histo/hist2.png differ
diff --git a/frontend/public/histo/hist3.jpg b/frontend/public/histo/hist3.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..3b9294a2736de36dfdb375d794575d2caa14cf81
Binary files /dev/null and b/frontend/public/histo/hist3.jpg differ
diff --git a/frontend/public/manalife_LOGO.jpg b/frontend/public/manalife_LOGO.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..559711628e4de66b9c149c92e50d105760733429
Binary files /dev/null and b/frontend/public/manalife_LOGO.jpg differ
diff --git a/frontend/public/white_logo.png b/frontend/public/white_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..56216800f859347c4af5cf3d1fbddc5614dc1caf
Binary files /dev/null and b/frontend/public/white_logo.png differ
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9bf4686dc867d933da9a74018df872d88f8379dc
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,125 @@
+import { useState, useEffect } from "react";
+import axios from "axios";
+import { Header } from "./components/Header";
+import { Sidebar } from "./components/Sidebar";
+import { UploadSection } from "./components/UploadSection";
+import { ResultsPanel } from "./components/ResultsPanel";
+import { Footer } from "./components/Footer";
+import { ProgressBar } from "./components/progressbar";
+
+export function App() {
+// ----------------------------
+// State Management
+// ----------------------------
+const [selectedTest, setSelectedTest] = useState("cytology");
+const [uploadedImage, setUploadedImage] = useState(null);
+const [selectedModel, setSelectedModel] = useState("");
+const [apiResult, setApiResult] = useState(null);
+const [showResults, setShowResults] = useState(false);
+const [currentStep, setCurrentStep] = useState(0);
+const [loading, setLoading] = useState(false);
+
+// ----------------------------
+// Progress bar logic
+// ----------------------------
+useEffect(() => {
+if (showResults) setCurrentStep(2);
+else if (uploadedImage) setCurrentStep(1);
+else setCurrentStep(0);
+}, [uploadedImage, showResults]);
+
+// ----------------------------
+// Reset logic — new test
+// ----------------------------
+useEffect(() => {
+setCurrentStep(0);
+setShowResults(false);
+setUploadedImage(null);
+setSelectedModel("");
+setApiResult(null);
+}, [selectedTest]);
+
+// ----------------------------
+// Analyze handler (Backend call)
+// ----------------------------
+const handleAnalyze = async () => {
+if (!uploadedImage || !selectedModel) {
+alert("Please select a model and upload an image first!");
+return;
+}
+
+
+setLoading(true);
+setShowResults(false);
+setApiResult(null);
+
+try {
+ // Convert Base64 → File
+ const blob = await fetch(uploadedImage).then((r) => r.blob());
+ const file = new File([blob], "input.jpg", { type: blob.type });
+
+ const formData = new FormData();
+ formData.append("file", file);
+ formData.append("analysis_type", selectedTest);
+ formData.append("model_name", selectedModel);
+
+ // POST to backend
+const baseURL =
+ import.meta.env.MODE === "development"
+ ? "http://127.0.0.1:8000"
+ : window.location.origin;
+
+ const res = await axios.post(`${baseURL}/predict/`, formData, {
+ headers: { "Content-Type": "multipart/form-data" },
+ });
+
+ setApiResult(res.data);
+ setShowResults(true);
+ } catch (err) {
+ console.error("❌ Error during inference:", err);
+ alert("Error analyzing the image. Check backend logs.");
+ } finally {
+ setLoading(false);
+ }
+};
+// ----------------------------
+// Layout
+// ----------------------------
+return (
+
+
+
+
+
+
+
+ {/* Upload & Model Selection */}
+
+
+ {/* Results Panel */}
+ {showResults && (
+
+ )}
+
+
+
+
+
+
+
+
+);
+}
\ No newline at end of file
diff --git a/frontend/src/AppRouter.tsx b/frontend/src/AppRouter.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9789d9e51f3b8c2b096bed506bfc73791ef63068
--- /dev/null
+++ b/frontend/src/AppRouter.tsx
@@ -0,0 +1,10 @@
+import React from "react";
+import { BrowserRouter, Routes, Route } from "react-router-dom";
+import { App } from "./App";
+export function AppRouter() {
+ return
+
+ } />
+
+ ;
+}
\ No newline at end of file
diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..66f5377d30f20b2b02f88e3cdf4226c582bad4e2
--- /dev/null
+++ b/frontend/src/components/Footer.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+
+export function Footer() {
+ return (
+
+ {/* Overlay for readability */}
+
+
+ {/* Main footer content */}
+
+
+
+
+
© 2025 Manalife. All rights reserved.
+
+ Advancing innovation in women's health and digital pathology.
+
+
+
+
+ {/* Logo at bottom-right corner */}
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ed4a444ff432a835dc657e966d73ef0ce31434b5
--- /dev/null
+++ b/frontend/src/components/Header.tsx
@@ -0,0 +1,40 @@
+export function Header() {
+ return (
+
+ {/* Banner */}
+
+
+ {/* Logo + Title */}
+
+
+
+ Manalife AI Pathology Assistant
+
+
+
+
+
+ {/* Disclaimer */}
+
+
Public Disclaimer
+
+ Manalife AI models are research prototypes developed to advance
+ innovation in women's health and digital pathology. They are not
+ certified medical devices and are not intended for direct diagnosis or
+ treatment. Clinical validation and regulatory approval are required
+ before any medical use.
+
+
+
+ );
+}
diff --git a/frontend/src/components/ResultsPanel.tsx b/frontend/src/components/ResultsPanel.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..99dc46d6c74bf9c1cf6171632fca615f21be3b42
--- /dev/null
+++ b/frontend/src/components/ResultsPanel.tsx
@@ -0,0 +1,143 @@
+import { DownloadIcon, InfoIcon, Loader2Icon } from "lucide-react";
+
+interface ResultsPanelProps {
+uploadedImage: string | null;
+result?: any;
+loading?: boolean;
+}
+
+export function ResultsPanel({ uploadedImage, result, loading }: ResultsPanelProps) {
+if (loading) {
+return (
+);
+}
+
+if (!result) {
+return (
+No analysis result available yet.
+);
+}
+
+const {
+prediction,
+confidence,
+probabilities,
+detections,
+summary,
+annotated_image_url,
+model_name,
+analysis_type,
+} = result;
+
+const handleDownload = () => {
+if (annotated_image_url) {
+const link = document.createElement("a");
+link.href = annotated_image_url;
+link.download = "analysis_result.jpg";
+link.click();
+}
+};
+
+return (
+{/* Header */}
+{model_name ? model_name.toUpperCase() : "Analysis Result"}
+{analysis_type || "Test Type"}
+{annotated_image_url && (
+Download Image
+)}
+
+
+ {/* Image */}
+
+
+
+
+ {/* Results Summary */}
+
+ {prediction && (
+
+ {prediction}
+
+ )}
+
+ {confidence && (
+
+
+
+ Confidence: {(confidence * 100).toFixed(2)}%
+
+
+
+
+
0.7
+ ? "bg-green-500"
+ : confidence > 0.4
+ ? "bg-yellow-500"
+ : "bg-red-500"
+ }`}
+ style={{ width: `${confidence * 100}%` }}
+ />
+
+
+ )}
+
+ {summary && (
+
+ {summary}
+
+ )}
+
+
+ {/* Detections / Probabilities */}
+ {detections && detections.length > 0 && (
+
+
+ Detected Regions:
+
+
+ {detections.map((det: any, i: number) => (
+
+ {det.name || "object"} – {(det.confidence * 100).toFixed(1)}%
+
+ ))}
+
+
+ )}
+
+ {probabilities && (
+
+
+ Class Probabilities:
+
+
+ {JSON.stringify(probabilities, null, 2)}
+
+
+ )}
+
+ {/* Report Button */}
+
+
+ Generate Report
+
+
+
+
+);
+}
\ No newline at end of file
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..719d2f03e732ca4ad86fa6215d5ee6417f693453
--- /dev/null
+++ b/frontend/src/components/Sidebar.tsx
@@ -0,0 +1,54 @@
+import React, { useState } from 'react';
+import { ChevronDownIcon, FileTextIcon, HelpCircleIcon } from 'lucide-react';
+interface SidebarProps {
+ selectedTest: string;
+ onTestChange: (test: string) => void;
+}
+export function Sidebar({
+ selectedTest,
+ onTestChange
+}: SidebarProps) {
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
+ const testTypes = [{
+ value: 'cytology',
+ label: 'Cytology Analysis'
+ }, {
+ value: 'colposcopy',
+ label: 'Colposcopy Analysis'
+ }, {
+ value: 'histopathology',
+ label: 'Histopathology Analysis'
+ }];
+ return
;
+}
\ No newline at end of file
diff --git a/frontend/src/components/UploadSection.tsx b/frontend/src/components/UploadSection.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..23b22e097ec69c811531cc1bc96fe2396c6b2b8a
--- /dev/null
+++ b/frontend/src/components/UploadSection.tsx
@@ -0,0 +1,194 @@
+import React, { useRef } from 'react';
+import { UploadIcon } from 'lucide-react';
+
+interface UploadSectionProps {
+ selectedTest: string;
+ uploadedImage: string | null;
+ setUploadedImage: (image: string | null) => void;
+ selectedModel: string;
+ setSelectedModel: (model: string) => void;
+ onAnalyze: () => void;
+}
+
+
+
+export function UploadSection({
+ selectedTest,
+ uploadedImage,
+ setUploadedImage,
+ selectedModel,
+ setSelectedModel,
+ onAnalyze,
+}: UploadSectionProps) {
+ const fileInputRef = useRef
(null);
+
+ const modelOptions = {
+ cytology: [
+ { value: 'mwt', label: 'MWT' },
+ { value: 'yolo', label: 'YOLOv8' },
+ ],
+ colposcopy: [
+ { value: 'cin', label: 'Logistic-Colpo' },
+
+ ],
+ histopathology: [
+ { value: 'histopathology', label: 'Path Foundation Model' },
+
+ ],
+ };
+
+
+ const sampleImages = {
+ cytology: [
+ "/cyto/cyt1.jpg",
+ "/cyto/cyt2.png",
+ "/cyto/cyt3.png",
+ ],
+ colposcopy: [
+ "/colpo/colp1.jpg",
+ "/colpo/colp2.jpg",
+ "/colpo/colp3.jpg",
+ ],
+ histopathology: [
+ "/histo/hist1.png",
+ "/histo/hist2.png",
+ "/histo/hist3.jpg",
+ ],
+ };
+
+ const currentModels =
+ modelOptions[selectedTest as keyof typeof modelOptions] || [];
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ setUploadedImage(event.target?.result as string);
+ };
+ reader.readAsDataURL(file);
+ }
+ };
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault();
+ const file = e.dataTransfer.files[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ setUploadedImage(event.target?.result as string);
+ };
+ reader.readAsDataURL(file);
+ }
+ };
+
+ // Handle click on sample image
+ const handleSampleClick = (imgUrl: string) => {
+ setUploadedImage(imgUrl);
+ };
+
+ return (
+
+
+ Upload an image of a tissue sample
+
+
+ {/* Upload Area */}
+
e.preventDefault()}
+ onClick={() => fileInputRef.current?.click()}
+ className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center cursor-pointer hover:border-blue-400 transition-colors"
+ >
+
+
+
+
+
+ {uploadedImage ? (
+ <>
+
+ Image uploaded successfully!
+
+
+ Click to upload a different image
+
+
+
+
+ >
+ ) : (
+
Drag and drop or click to upload
+ )}
+
+
+
+ {/* Model Selection */}
+
+
+ Select Analysis Model:
+
+ setSelectedModel(e.target.value)}
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+ >
+ Choose a model...
+ {currentModels.map((model) => (
+
+ {model.label}
+
+ ))}
+
+
+
+ {/* Analyze Button */}
+
+ Analyze
+
+
+ {/* Separator */}
+
+
+ {/* Sample Images Section */}
+
+
+ Samples Images
+
+
+ {(sampleImages[selectedTest as keyof typeof sampleImages] || []).map(
+ (img, index) => (
+
handleSampleClick(img)}
+ >
+
+
+ )
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/components/progressbar.tsx b/frontend/src/components/progressbar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..28e763212ce04c7d1d81c3c43d73a4fd6de66aa2
--- /dev/null
+++ b/frontend/src/components/progressbar.tsx
@@ -0,0 +1,61 @@
+import { Fragment } from "react";
+import { CheckIcon, FileTextIcon } from "lucide-react";
+
+interface ProgressBarProps {
+ currentStep: number;
+}
+
+export function ProgressBar({ currentStep }: ProgressBarProps) {
+ const steps = [
+ { label: "Upload", index: 0 },
+ { label: "Analyze", index: 1 },
+ { label: "Report", index: 2 },
+ ];
+
+ return (
+
+
+ {steps.map((step, index) => (
+
+
+ {/* Step circle */}
+
step.index
+ ? "bg-gradient-to-r from-blue-800 to-teal-600"
+ : currentStep === step.index
+ ? "bg-gradient-to-r from-blue-600 to-teal-500"
+ : "bg-gray-300 text-gray-600"
+ }`}
+ >
+ {currentStep > step.index ? (
+
+ ) : step.index === 2 ? (
+
+ ) : (
+ {index + 1}
+ )}
+
+
+ {/* Label */}
+
+ {step.label}
+
+
+
+ {/* Connecting line */}
+ {index < steps.length - 1 && (
+ step.index
+ ? "bg-gradient-to-r from-blue-800 to-teal-600"
+ : "bg-gray-300"
+ }`}
+ />
+ )}
+
+ ))}
+
+
+ );
+}
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000000000000000000000000000000000000..ab6b3df45bac8a47fc64ba78435f724b50afe045
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,5 @@
+/* PLEASE NOTE: THESE TAILWIND IMPORTS SHOULD NEVER BE DELETED */
+@import 'tailwindcss/base';
+@import 'tailwindcss/components';
+@import 'tailwindcss/utilities';
+/* DO NOT DELETE THESE TAILWIND IMPORTS, OTHERWISE THE STYLING WILL NOT RENDER AT ALL */
\ No newline at end of file
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..cd789be8c7443e0ff402f649b00a2f22b7a2310a
--- /dev/null
+++ b/frontend/src/index.tsx
@@ -0,0 +1,5 @@
+import './index.css';
+import React from "react";
+import { render } from "react-dom";
+import { App } from "./App";
+render(
, document.getElementById("root"));
\ No newline at end of file
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..7fa250e36d024b9417d150e452f4a7d68e06e061
--- /dev/null
+++ b/frontend/tailwind.config.js
@@ -0,0 +1,4 @@
+export default {content: [
+ './index.html',
+ './src/**/*.{js,ts,jsx,tsx}'
+],}
\ No newline at end of file
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..a7fc6fbf23de2a53e36754bc4a2c306d0291d7b2
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json
new file mode 100644
index 0000000000000000000000000000000000000000..97ede7ee6f2d37bd2d76e60c0b6a447bee718b05
--- /dev/null
+++ b/frontend/tsconfig.node.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "strict": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..5a33944a9b41b59a9cf06ee4bb5586c77510f06b
--- /dev/null
+++ b/frontend/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+})