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 */} +
+ Manalife Logo +
+
+ ); +} 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 Logo +

+ 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 (

Analyzing image...

+); +} + +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 && ( +)}
+ + + {/* Image */} +
+ Analysis Result +
+ + {/* 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 */} + +
+ + +); +} \ 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 +

+
+ Uploaded sample +
+ + ) : ( +

Drag and drop or click to upload

+ )} +
+
+ + {/* Model Selection */} +
+ + +
+ + {/* Analyze Button */} + + + {/* Separator */} +
+ + {/* Sample Images Section */} +
+

+ Samples Images +

+
+ {(sampleImages[selectedTest as keyof typeof sampleImages] || []).map( + (img, index) => ( +
handleSampleClick(img)} + > + {`Sample +
+ ) + )} +
+
+
+ ); +} 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()], +})