diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000000000000000000000000000000000..d5a46ac3bb27dc979fbfbcc01b12c553a9d54332 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,38 @@ +[run] +source = . +omit = + */tests/* + */venv/* + */.venv/* + */env/* + */.env/* + */site-packages/* + */__pycache__/* + +[report] +# Lines to exclude from coverage consideration +exclude_lines = + # Don't complain about debug-only code + pragma: no cover + def __repr__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__. + pass +precision = 2 +show_missing = True + +[html] +directory = htmlcov + +[paths] +source = + . + */backend/* + */app/* \ No newline at end of file diff --git a/.coveragerc.new b/.coveragerc.new new file mode 100644 index 0000000000000000000000000000000000000000..c624114db74317ff5127d25f81440ac813888b19 --- /dev/null +++ b/.coveragerc.new @@ -0,0 +1,23 @@ +[run] +source = . +omit = + */tests/* + */venv/* + */.venv/* + */env/* + */.env/* + */site-packages/* + +[report] +exclude_lines = + pragma: no cover + def __repr__ + if self\.debug + raise AssertionError + raise NotImplementedError + if 0: + if __name__ == ['"].*?['"] + @(abc\\.)?abstractmethod + +[html] +directory = htmlcov diff --git a/.env b/.env new file mode 100644 index 0000000000000000000000000000000000000000..b3fc3d543aa48ab7c7601f0eb1b7f24dedf76802 --- /dev/null +++ b/.env @@ -0,0 +1,13 @@ +# 資料庫設定 +DATABASE_URL=sqlite:///./data/health_assistant.db + +# Redis 設定 +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_DB=0 + +# API 設定 +API_HOST=localhost +API_PORT=8000 + +USDA_API_KEY="0bZCszzPSbc5r6RXfm0aHKtWGX2V0SX1hLiMmXwi" diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..be195d5756cded81c857953750dcfb3cad60ef00 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +COMPUTER_VISION_ENDPOINT=your_azure_endpoint +COMPUTER_VISION_KEY=your_azure_key diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..f00682feb9e8a7245b96eba8b99b8e4765ea3d1b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.py linguist-language=Python +app/main.py linguist-detectable=true \ No newline at end of file diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..64f6a525727854151573c85c8148e5208c708d70 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,49 @@ +name: Python CI + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.9', '3.10', '3.11'] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-cov + + - name: Run tests with coverage + run: | + python -m pytest tests/ --cov=backend --cov-report=xml + env: + CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + fail_ci_if_error: false + + - name: Upload coverage report + uses: actions/upload-artifact@v3 + with: + name: coverage-report + path: htmlcov/ + if-no-files-found: error + retention-days: 5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..04a3266886b16b05e1975a3105a421c3a8021577 --- /dev/null +++ b/.gitignore @@ -0,0 +1,91 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual Environment +venv/ +env/ +ENV/ +.env +.venv + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Testing +.coverage +htmlcov/ +.pytest_cache/ + +# Logs +*.log + +# Environment variables +.env + +# Local development +*.db +*.sqlite3 + +# Frontend +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build +dist/ +build/ + +# macOS +.DS_Store + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini + +# Project specific +backend/.env +frontend/.env + +# Coverage reports +coverage/ +.coverage.* + +# Jupyter Notebook +.ipynb_checkpoints + +# VS Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# MyPy +.mypy_cache/ +.dmypy.json +dmypy.json diff --git a/Dockerfile b/Dockerfile index c9f26a7b54404f69a38b57631c98edbb6f8763b5..412bf00f58df4bf16e2c0816d3f8254d7f31f1aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,4 +8,4 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"] \ No newline at end of file +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"] \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000000000000000000000000000000000000..52b3fbbcfca2eb4cdec108e5bd035126f217b293 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,8 @@ +include README.md +include requirements.txt +include .coveragerc +include pytest.ini +recursive-include backend/app/templates * +recursive-include tests * +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..956e77d9c3a9555ec244e9448ed478d29d157c9a --- /dev/null +++ b/README.md @@ -0,0 +1,159 @@ +--- +title: Health Assistant AI +emoji: 🏥 +colorFrom: blue +colorTo: green +sdk: gradio +sdk_version: 4.44.0 +app_file: app.py +pinned: false +--- + +# Health Assistant AI + +一個整合飲食追蹤、運動記錄和AI圖像辨識的健康生活助手應用。 + +## 主要功能 + +- 🍽️ 飲食記錄(支援AI圖像辨識) +- 💧 飲水追蹤 +- 🏃‍♂️ 運動記錄 +- 📊 營養分析儀表板 +- 🤖 AI驅動的個人化建議 + +## 技術堆疊 + +### 前端 +- React +- TailwindCSS +- Chart.js + +### 後端 +- Python FastAPI +- SQLAlchemy +- PostgreSQL +- TensorFlow/PyTorch +- Pydantic +- HuggingFace Transformers +- Anthropic Claude API + +## 安裝說明 + +1. 克隆專案 +```bash +git clone https://github.com/yourusername/health_assistant.git +cd health_assistant +``` + +2. 設置 Python 虛擬環境並安裝依賴: +```bash +python -m venv venv +source venv/bin/activate # Linux/Mac +# 或 +.\venv\Scripts\activate # Windows + +pip install -e . # 以開發模式安裝 +``` + +3. 安裝前端依賴: +```bash +cd frontend +npm install +``` + +## 開發說明 + +### 後端開發 +```bash +# 啟動後端開發服務器 +uvicorn backend.main:app --reload +``` + +### 前端開發 +```bash +cd frontend +npm run dev +``` + +## 測試 + +### 運行測試 + +運行所有測試: +```bash +pytest +``` + +運行特定測試文件: +```bash +pytest tests/test_api/test_main.py # 運行 API 測試 +pytest tests/test_services/ # 運行服務層測試 +pytest -k "test_function_name" # 運行特定測試函數 +``` + +### 測試覆蓋率報告 + +生成測試覆蓋率報告: +```bash +pytest --cov=backend --cov-report=html +``` + +這將在 `htmlcov` 目錄下生成 HTML 格式的覆蓋率報告。 + +### 代碼風格檢查 + +使用 black 和 isort 進行代碼格式化: +```bash +black . +isort . +``` + +### 類型檢查 + +運行 mypy 進行靜態類型檢查: +```bash +mypy . +``` + +## 持續整合 (CI) + +項目使用 GitHub Actions 進行持續整合。每次推送代碼或創建 Pull Request 時,會自動運行以下檢查: + +- 在 Python 3.9, 3.10, 3.11 上運行測試 +- 生成測試覆蓋率報告 +- 上傳覆蓋率到 Codecov + +### 本地運行 CI 檢查 + +在提交代碼前,可以本地運行 CI 檢查: +```bash +# 運行測試和覆蓋率 +pytest --cov=backend + +# 檢查代碼風格 +black --check . +isort --check-only . + +# 運行類型檢查 +mypy . +``` + +## 測試覆蓋率要求 + +- 所有新代碼應該有對應的測試 +- 目標是達到至少 80% 的代碼覆蓋率 +- 關鍵業務邏輯應該有完整的測試覆蓋 +- 測試應該包含成功和失敗案例 +- 使用 `# pragma: no cover` 時需提供正當理由 + +## 貢獻指南 + +1. Fork 項目 +2. 創建特性分支 (`git checkout -b feature/AmazingFeature`) +3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) +4. 推送到分支 (`git push origin feature/AmazingFeature`) +5. 開啟 Pull Request + +## 許可證 + +MIT License - 詳見 [LICENSE](LICENSE) 文件 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ + diff --git a/__pycache__/__init__.cpython-313.pyc b/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d13269c5e5493d3bf702c500e9b4d7db6328773a Binary files /dev/null and b/__pycache__/__init__.cpython-313.pyc differ diff --git a/__pycache__/database.cpython-313.pyc b/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..589d9b7d8262571d2d69bf6687936a803865b97a Binary files /dev/null and b/__pycache__/database.cpython-313.pyc differ diff --git a/__pycache__/init_db.cpython-313.pyc b/__pycache__/init_db.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a762c5434e9b741f8c2e52021edae9fcabf2ade Binary files /dev/null and b/__pycache__/init_db.cpython-313.pyc differ diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0dd1bbbbf99e89b80cee98076ff3efe1fe76db3b Binary files /dev/null and b/__pycache__/main.cpython-313.pyc differ diff --git a/app.cpython-313.pyc b/app.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..231535dd5b045fff462b480376b45f1ba6f31849 Binary files /dev/null and b/app.cpython-313.pyc differ diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ + diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000000000000000000000000000000000000..111aa4c624e6bc14d47ff41862aba34010af0c62 --- /dev/null +++ b/app/database.py @@ -0,0 +1,31 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +import os + +# 確保資料庫目錄存在 +DB_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") +os.makedirs(DB_DIR, exist_ok=True) + +# 資料庫 URL +SQLALCHEMY_DATABASE_URL = f"sqlite:///{os.path.join(DB_DIR, 'health_assistant.db')}" + +# 創建資料庫引擎 +engine = create_engine( + SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False} # SQLite 特定配置 +) + +# 創建 SessionLocal 類 +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# 創建 Base 類 +Base = declarative_base() + +# 獲取資料庫會話的依賴項 +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/app/init_db.py b/app/init_db.py new file mode 100644 index 0000000000000000000000000000000000000000..4f72b9e2bdc61bedfefbf0b4521d6fe97597d7b3 --- /dev/null +++ b/app/init_db.py @@ -0,0 +1,70 @@ +from .database import Base, engine, SessionLocal +from .models.meal_log import MealLog +from .models.nutrition import Nutrition +import json + +def init_db(): + """初始化資料庫並填入初始營養數據""" + print("Creating database tables...") + # 根據模型建立所有表格 + Base.metadata.create_all(bind=engine) + print("Database tables created successfully!") + + # 檢查是否已有資料,避免重複新增 + db = SessionLocal() + if db.query(Nutrition).count() == 0: + print("Populating nutrition table with initial data...") + + # 從 main.py 移植過來的模擬資料 + mock_nutrition_data = [ + { + "food_name": "hamburger", "chinese_name": "漢堡", "calories": 540, "protein": 25, "fat": 31, "carbs": 40, + "fiber": 3, "sugar": 6, "sodium": 1040, "health_score": 45, + "recommendations": ["脂肪和鈉含量過高,建議減少食用頻率。"], + "warnings": ["高熱量", "高脂肪", "高鈉"] + }, + { + "food_name": "pizza", "chinese_name": "披薩", "calories": 266, "protein": 11, "fat": 10, "carbs": 33, + "fiber": 2, "sugar": 4, "sodium": 598, "health_score": 65, + "recommendations": ["可搭配沙拉以增加纖維攝取。"], + "warnings": ["高鈉"] + }, + { + "food_name": "sushi", "chinese_name": "壽司", "calories": 200, "protein": 12, "fat": 8, "carbs": 20, + "fiber": 1, "sugar": 2, "sodium": 380, "health_score": 85, + "recommendations": ["優質的蛋白質和碳水化合物來源。"], + "warnings": [] + }, + { + "food_name": "fried rice", "chinese_name": "炒飯", "calories": 238, "protein": 8, "fat": 12, "carbs": 26, + "fiber": 2, "sugar": 3, "sodium": 680, "health_score": 60, + "recommendations": ["注意油脂和鈉含量。"], + "warnings": ["高鈉"] + }, + { + "food_name": "chicken wings", "chinese_name": "雞翅", "calories": 203, "protein": 18, "fat": 14, "carbs": 0, + "fiber": 0, "sugar": 0, "sodium": 380, "health_score": 70, + "recommendations": ["蛋白質的良好來源。"], + "warnings": [] + }, + { + "food_name": "salad", "chinese_name": "沙拉", "calories": 33, "protein": 3, "fat": 0.2, "carbs": 6, + "fiber": 3, "sugar": 3, "sodium": 65, "health_score": 95, + "recommendations": ["低熱量高纖維,是健康的選擇。"], + "warnings": [] + } + ] + + for food_data in mock_nutrition_data: + db_item = Nutrition(**food_data) + db.add(db_item) + + db.commit() + print(f"{len(mock_nutrition_data)} items populated.") + else: + print("Nutrition table already contains data. Skipping population.") + + db.close() + +if __name__ == "__main__": + init_db() diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..611804bd4ef511cad91ed795799c276416e3a8b6 --- /dev/null +++ b/app/main.py @@ -0,0 +1,39 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from app.routers import ai_router, meal_router +from app.database import engine, Base + +# 創建資料庫表 +Base.metadata.create_all(bind=engine) + +app = FastAPI(title="Health Assistant API") + +# 配置 CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:5173"], # React 開發伺服器的位址 + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 註冊路由 +app.include_router(ai_router.router) +app.include_router(meal_router.router) + +@app.get("/") +async def root(): + return {"message": "Health Assistant API is running"} + +@app.get("/health") +async def health_check(): + """健康檢查端點""" + return { + "status": "healthy", + "routers": ["ai_router", "meal_router"], + "endpoints": [ + "/ai/analyze-food-image/", + "/ai/analyze-food-image-with-weight/", + "/ai/health" + ] + } diff --git a/app/models/meal_log.py b/app/models/meal_log.py new file mode 100644 index 0000000000000000000000000000000000000000..1d5910688a8e03c51348bf0376168ec584804f16 --- /dev/null +++ b/app/models/meal_log.py @@ -0,0 +1,19 @@ +from sqlalchemy import Column, Integer, String, Float, DateTime, JSON +from ..database import Base + +class MealLog(Base): + __tablename__ = "meal_logs" + + id = Column(Integer, primary_key=True, index=True) + food_name = Column(String, index=True) + meal_type = Column(String) # breakfast, lunch, dinner, snack + portion_size = Column(String) # small, medium, large + calories = Column(Float) + protein = Column(Float) + carbs = Column(Float) + fat = Column(Float) + fiber = Column(Float) + meal_date = Column(DateTime, index=True) + image_url = Column(String) + ai_analysis = Column(JSON) # 儲存完整的 AI 分析結果 + created_at = Column(DateTime) diff --git a/app/models/nutrition.py b/app/models/nutrition.py new file mode 100644 index 0000000000000000000000000000000000000000..fd118c5ddfd95c474724c16a4414f0be91842fd5 --- /dev/null +++ b/app/models/nutrition.py @@ -0,0 +1,22 @@ +# backend/app/models/nutrition.py +from sqlalchemy import Column, Integer, String, Float, JSON +from ..database import Base + +class Nutrition(Base): + __tablename__ = "nutrition" + + id = Column(Integer, primary_key=True, index=True) + food_name = Column(String, unique=True, index=True, nullable=False) + chinese_name = Column(String) + calories = Column(Float) + protein = Column(Float) + fat = Column(Float) + carbs = Column(Float) + fiber = Column(Float) + sugar = Column(Float) + sodium = Column(Float) + # For more complex data like vitamins, minerals, etc. + details = Column(JSON) + health_score = Column(Integer) + recommendations = Column(JSON) + warnings = Column(JSON) \ No newline at end of file diff --git a/app/routers/__init__.py b/app/routers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0519ecba6ea913e21689ec692e81e9e4973fbf73 --- /dev/null +++ b/app/routers/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/routers/ai_router.py b/app/routers/ai_router.py new file mode 100644 index 0000000000000000000000000000000000000000..e9a697919a16d2c5a7eab4b47b2fe1bdd77a324b --- /dev/null +++ b/app/routers/ai_router.py @@ -0,0 +1,72 @@ +# 檔案路徑: app/routers/ai_router.py + +from fastapi import APIRouter, File, UploadFile, HTTPException +from pydantic import BaseModel +from typing import Dict, Any, List, Optional + +router = APIRouter( + prefix="/ai", + tags=["AI"], +) + +# 新增 Pydantic 模型定義 +class WeightEstimationResponse(BaseModel): + food_type: str + estimated_weight: float + weight_confidence: float + weight_error_range: List[float] + nutrition: Dict[str, Any] + reference_object: Optional[str] = None + note: str + +class FoodAnalysisResponse(BaseModel): + food_name: str + nutrition_info: Dict[str, Any] + +@router.post("/analyze-food-image/") +async def analyze_food_image_endpoint(file: UploadFile = File(...)): + """ + 這個端點接收使用者上傳的食物圖片,使用 AI 模型進行辨識, + 並返回辨識出的食物名稱。 + """ + # 檢查上傳的檔案是否為圖片格式 + if not file.content_type or not file.content_type.startswith("image/"): + raise HTTPException(status_code=400, detail="上傳的檔案不是圖片格式。") + + # 暫時返回測試回應 + return {"food_name": "測試食物", "nutrition_info": {"calories": 100}} + +@router.post("/analyze-food-image-with-weight/", response_model=WeightEstimationResponse) +async def analyze_food_image_with_weight_endpoint(file: UploadFile = File(...)): + """ + 整合食物辨識、重量估算與營養分析的端點。 + 包含信心度與誤差範圍,支援參考物偵測。 + """ + # 檢查上傳的檔案是否為圖片格式 + if not file.content_type or not file.content_type.startswith("image/"): + raise HTTPException(status_code=400, detail="上傳的檔案不是圖片格式。") + + # 暫時返回測試回應 + return WeightEstimationResponse( + food_type="測試食物", + estimated_weight=150.0, + weight_confidence=0.85, + weight_error_range=[130.0, 170.0], + nutrition={"calories": 100, "protein": 5, "fat": 2, "carbs": 15}, + reference_object="硬幣", + note="測試重量估算結果" + ) + +@router.get("/health") +async def health_check(): + """ + 健康檢查端點,確認 AI 服務是否正常運作 + """ + return { + "status": "healthy", + "services": { + "food_classification": "available", + "weight_estimation": "available", + "nutrition_api": "available" + } + } \ No newline at end of file diff --git a/app/routers/meal_router.py b/app/routers/meal_router.py new file mode 100644 index 0000000000000000000000000000000000000000..15c4055aaaebd74f8aeb2682016bcd45ece24da0 --- /dev/null +++ b/app/routers/meal_router.py @@ -0,0 +1,103 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List, Dict, Any, Optional +from datetime import datetime +from ..services.meal_service import MealService +from ..database import get_db +from pydantic import BaseModel + +router = APIRouter(prefix="/api/meals", tags=["Meals"]) + +class MealCreate(BaseModel): + food_name: str + meal_type: str + portion_size: str + meal_date: datetime + nutrition: Dict[str, float] + image_url: Optional[str] = None + ai_analysis: Optional[Dict[str, Any]] = None + +class DateRange(BaseModel): + start_date: datetime + end_date: datetime + meal_type: Optional[str] = None + +@router.post("/log") +async def create_meal_log( + meal: MealCreate, + db: Session = Depends(get_db) +) -> Dict[str, Any]: + """創建新的用餐記錄""" + meal_service = MealService(db) + try: + meal_log = meal_service.create_meal_log( + food_name=meal.food_name, + meal_type=meal.meal_type, + portion_size=meal.portion_size, + nutrition=meal.nutrition, + meal_date=meal.meal_date, + image_url=meal.image_url, + ai_analysis=meal.ai_analysis + ) + return { + "success": True, + "message": "用餐記錄已創建", + "data": { + "id": meal_log.id, + "food_name": meal_log.food_name, + "meal_type": meal_log.meal_type, + "meal_date": meal_log.meal_date + } + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/list") +async def get_meal_logs( + date_range: DateRange, + db: Session = Depends(get_db) +) -> Dict[str, Any]: + """獲取用餐記錄列表""" + meal_service = MealService(db) + try: + logs = meal_service.get_meal_logs( + start_date=date_range.start_date, + end_date=date_range.end_date, + meal_type=date_range.meal_type + ) + return { + "success": True, + "data": [{ + "id": log.id, + "food_name": log.food_name, + "meal_type": log.meal_type, + "portion_size": log.portion_size, + "calories": log.calories, + "protein": log.protein, + "carbs": log.carbs, + "fat": log.fat, + "meal_date": log.meal_date, + "image_url": log.image_url + } for log in logs] + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/nutrition-summary") +async def get_nutrition_summary( + date_range: DateRange, + db: Session = Depends(get_db) +) -> Dict[str, Any]: + """獲取營養攝入總結""" + meal_service = MealService(db) + try: + summary = meal_service.get_nutrition_summary( + start_date=date_range.start_date, + end_date=date_range.end_date + ) + return { + "success": True, + "data": summary + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0f39f8b94933990e7d7ba2c458cfbab7333218ef --- /dev/null +++ b/app/services/__init__.py @@ -0,0 +1,5 @@ +# This file makes the services directory a Python package +# Import the HybridFoodAnalyzer class to make it available when importing from app.services +from .food_analyzer_service import HybridFoodAnalyzer + +__all__ = ['HybridFoodAnalyzer'] diff --git a/app/services/ai_service.py b/app/services/ai_service.py new file mode 100644 index 0000000000000000000000000000000000000000..2663a8ac018f2cdeb2bb5a3af69117acf5530646 --- /dev/null +++ b/app/services/ai_service.py @@ -0,0 +1,95 @@ +# 檔案路徑: backend/app/services/ai_service.py + +from transformers.pipelines import pipeline +from PIL import Image +import io +import logging + +# 設置日誌 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# 全局變量 +image_classifier = None + +def load_model(): + """載入模型的函數""" + global image_classifier + + try: + logger.info("正在載入食物辨識模型...") + # 載入模型 - 移除不支持的參數 + image_classifier = pipeline( + "image-classification", + model="juliensimon/autotrain-food101-1471154053", + device=-1 # 使用CPU + ) + logger.info("模型載入成功!") + return True + except Exception as e: + logger.error(f"模型載入失敗: {str(e)}") + image_classifier = None + return False + +def classify_food_image(image_bytes: bytes) -> str: + """ + 接收圖片的二進位制數據,進行分類並返回可能性最高的食物名稱。 + """ + global image_classifier + + # 如果模型未載入,嘗試重新載入 + if image_classifier is None: + logger.warning("模型未載入,嘗試重新載入...") + if not load_model(): + return "Error: Model not loaded" + + if image_classifier is None: + return "Error: Model could not be loaded" + + try: + # 驗證圖片數據 + if not image_bytes: + return "Error: Empty image data" + + # 從記憶體中的 bytes 打開圖片 + image = Image.open(io.BytesIO(image_bytes)) + + # 確保圖片是RGB格式 + if image.mode != 'RGB': + image = image.convert('RGB') + + logger.info(f"處理圖片,尺寸: {image.size}") + + # 使用模型管線進行分類 + pipeline_output = image_classifier(image) + + logger.info(f"模型輸出: {pipeline_output}") + + # 處理輸出結果 + if not pipeline_output: + return "Unknown" + + # pipeline_output 通常是一個列表 + if isinstance(pipeline_output, list) and len(pipeline_output) > 0: + result = pipeline_output[0] + if isinstance(result, dict) and 'label' in result: + label = result['label'] + confidence = result.get('score', 0) + + logger.info(f"辨識結果: {label}, 信心度: {confidence:.2f}") + + # 標籤可能包含底線,我們將其替換為空格,並讓首字母大寫 + formatted_label = str(label).replace('_', ' ').title() + return formatted_label + + return "Unknown" + + except Exception as e: + logger.error(f"圖片分類過程中發生錯誤: {str(e)}") + return f"Error: {str(e)}" + +# 在模塊載入時嘗試載入模型 +logger.info("初始化 AI 服務...") +load_model() + +__all__ = ["classify_food_image"] \ No newline at end of file diff --git a/app/services/food_analyzer_service.py b/app/services/food_analyzer_service.py new file mode 100644 index 0000000000000000000000000000000000000000..b3bb8e695b3c30bb12f7e8883b92ff6804cecd4a --- /dev/null +++ b/app/services/food_analyzer_service.py @@ -0,0 +1,256 @@ +import torch +import json +from typing import Dict, Any, Optional +from PIL import Image +from io import BytesIO +import base64 +import os +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +class HybridFoodAnalyzer: + def __init__(self, claude_api_key: Optional[str] = None): + """ + Initialize the HybridFoodAnalyzer with HuggingFace model and Claude API. + + Args: + claude_api_key: Optional API key for Claude. If not provided, will try to get from environment variable CLAUDE_API_KEY. + """ + # Initialize HuggingFace model + from transformers import AutoImageProcessor, AutoModelForImageClassification + + print("Loading HuggingFace food recognition model...") + self.processor = AutoImageProcessor.from_pretrained("nateraw/food") + self.model = AutoModelForImageClassification.from_pretrained("nateraw/food") + self.model.eval() # Set model to evaluation mode + + # Initialize Claude API + print("Initializing Claude API...") + import anthropic + self.claude_api_key = claude_api_key or os.getenv('CLAUDE_API_KEY') + if not self.claude_api_key: + raise ValueError("Claude API key is required. Please set CLAUDE_API_KEY environment variable or pass it as an argument.") + + self.claude_client = anthropic.Anthropic(api_key=self.claude_api_key) + + def recognize_food(self, image: Image.Image) -> Dict[str, Any]: + """ + Recognize food from an image using HuggingFace model. + + Args: + image: PIL Image object containing the food image + + Returns: + Dictionary containing food name and confidence score + """ + try: + print("Processing image for food recognition...") + inputs = self.processor(images=image, return_tensors="pt") + + with torch.no_grad(): + outputs = self.model(**inputs) + predictions = torch.nn.functional.softmax(outputs.logits, dim=-1) + + predicted_class_id = int(predictions.argmax().item()) + confidence = predictions[0][predicted_class_id].item() + food_name = self.model.config.id2label[predicted_class_id] + + # Map common food names to Chinese + food_name_mapping = { + "hamburger": "漢堡", + "pizza": "披薩", + "sushi": "壽司", + "fried rice": "炒飯", + "chicken wings": "雞翅", + "salad": "沙拉", + "apple": "蘋果", + "banana": "香蕉", + "orange": "橙子", + "noodles": "麵條" + } + + chinese_name = food_name_mapping.get(food_name.lower(), food_name) + + return { + "food_name": food_name, + "chinese_name": chinese_name, + "confidence": confidence, + "success": True + } + + except Exception as e: + print(f"Error in food recognition: {str(e)}") + return { + "success": False, + "error": str(e) + } + + def analyze_nutrition(self, food_name: str) -> Dict[str, Any]: + """ + Analyze nutrition information for a given food using Claude API. + + Args: + food_name: Name of the food to analyze + + Returns: + Dictionary containing nutrition information + """ + try: + print(f"Analyzing nutrition for {food_name}...") + prompt = f""" + 請分析 {food_name} 的營養成分(每100g),並以JSON格式回覆: + {{ + "calories": 數值, + "protein": 數值, + "fat": 數值, + "carbs": 數值, + "fiber": 數值, + "sugar": 數值, + "sodium": 數值 + }} + """ + + message = self.claude_client.messages.create( + model="claude-3-sonnet-20240229", + max_tokens=500, + messages=[{"role": "user", "content": prompt}] + ) + + # Extract and parse the JSON response + response_text = message.content[0].get("text", "") if isinstance(message.content[0], dict) else str(message.content[0]) + try: + nutrition_data = json.loads(response_text.strip()) + return { + "success": True, + "nutrition": nutrition_data + } + except json.JSONDecodeError as e: + print(f"Error parsing Claude response: {e}") + return { + "success": False, + "error": f"Failed to parse nutrition data: {e}", + "raw_response": response_text + } + + except Exception as e: + print(f"Error in nutrition analysis: {str(e)}") + return { + "success": False, + "error": str(e) + } + + def process_image(self, image_data: bytes) -> Dict[str, Any]: + """ + Process an image to recognize food and analyze its nutrition. + + Args: + image_data: Binary image data + + Returns: + Dictionary containing recognition and analysis results + """ + try: + # Convert bytes to PIL Image + image = Image.open(BytesIO(image_data)) + + # Step 1: Recognize food + recognition_result = self.recognize_food(image) + if not recognition_result.get("success"): + return recognition_result + + # Step 2: Analyze nutrition + nutrition_result = self.analyze_nutrition(recognition_result["food_name"]) + if not nutrition_result.get("success"): + return nutrition_result + + # Calculate health score + nutrition = nutrition_result["nutrition"] + health_score = self.calculate_health_score(nutrition) + + # Generate recommendations and warnings + recommendations = self.generate_recommendations(nutrition) + warnings = self.generate_warnings(nutrition) + + return { + "success": True, + "food_name": recognition_result["food_name"], + "chinese_name": recognition_result["chinese_name"], + "confidence": recognition_result["confidence"], + "nutrition": nutrition, + "analysis": { + "healthScore": health_score, + "recommendations": recommendations, + "warnings": warnings + } + } + + except Exception as e: + return { + "success": False, + "error": f"Failed to process image: {str(e)}" + } + + def calculate_health_score(self, nutrition: Dict[str, float]) -> int: + """Calculate a health score based on nutritional values""" + score = 100 + + # 熱量評分 + if nutrition["calories"] > 400: + score -= 20 + elif nutrition["calories"] > 300: + score -= 10 + + # 脂肪評分 + if nutrition["fat"] > 20: + score -= 15 + elif nutrition["fat"] > 15: + score -= 8 + + # 蛋白質評分 + if nutrition["protein"] > 15: + score += 10 + elif nutrition["protein"] < 5: + score -= 10 + + # 鈉含量評分 + if "sodium" in nutrition and nutrition["sodium"] > 800: + score -= 15 + elif "sodium" in nutrition and nutrition["sodium"] > 600: + score -= 8 + + return max(0, min(100, score)) + + def generate_recommendations(self, nutrition: Dict[str, float]) -> list: + """Generate dietary recommendations based on nutrition data""" + recommendations = [] + + if nutrition["protein"] < 10: + recommendations.append("建議增加蛋白質攝取,可搭配雞蛋或豆腐") + + if nutrition["fat"] > 20: + recommendations.append("脂肪含量較高,建議適量食用") + + if "fiber" in nutrition and nutrition["fiber"] < 3: + recommendations.append("纖維含量不足,建議搭配蔬菜沙拉") + + if "sodium" in nutrition and nutrition["sodium"] > 600: + recommendations.append("鈉含量偏高,建議多喝水並減少其他鹽分攝取") + + return recommendations + + def generate_warnings(self, nutrition: Dict[str, float]) -> list: + """Generate dietary warnings based on nutrition data""" + warnings = [] + + if nutrition["calories"] > 500: + warnings.append("高熱量食物") + + if nutrition["fat"] > 25: + warnings.append("高脂肪食物") + + if "sodium" in nutrition and nutrition["sodium"] > 1000: + warnings.append("高鈉食物") + + return warnings diff --git a/app/services/meal_service.py b/app/services/meal_service.py new file mode 100644 index 0000000000000000000000000000000000000000..38d39dcf7d4f60b1322f2c6bf7d1a28317617ca0 --- /dev/null +++ b/app/services/meal_service.py @@ -0,0 +1,89 @@ +from datetime import datetime +from typing import List, Dict, Any, Optional +from sqlalchemy.orm import Session +from ..models.meal_log import MealLog + +class MealService: + def __init__(self, db: Session): + self.db = db + + def create_meal_log( + self, + food_name: str, + meal_type: str, + portion_size: str, + nutrition: Dict[str, float], + meal_date: datetime, + image_url: Optional[str] = None, + ai_analysis: Optional[Dict[str, Any]] = None + ) -> MealLog: + """創建新的用餐記錄""" + meal_log = MealLog( + food_name=food_name, + meal_type=meal_type, + portion_size=portion_size, + calories=nutrition.get('calories', 0), + protein=nutrition.get('protein', 0), + carbs=nutrition.get('carbs', 0), + fat=nutrition.get('fat', 0), + fiber=nutrition.get('fiber', 0), + meal_date=meal_date, + image_url=image_url, + ai_analysis=ai_analysis, + created_at=datetime.utcnow() + ) + + self.db.add(meal_log) + self.db.commit() + self.db.refresh(meal_log) + return meal_log + + def get_meal_logs( + self, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + meal_type: Optional[str] = None + ) -> List[MealLog]: + """獲取用餐記錄""" + query = self.db.query(MealLog) + + if start_date: + query = query.filter(MealLog.meal_date >= start_date) + if end_date: + query = query.filter(MealLog.meal_date <= end_date) + if meal_type: + query = query.filter(MealLog.meal_type == meal_type) + + return query.order_by(MealLog.meal_date.desc()).all() + + def get_nutrition_summary( + self, + start_date: datetime, + end_date: datetime + ) -> Dict[str, float]: + """獲取指定時間範圍內的營養攝入總結""" + meals = self.get_meal_logs(start_date, end_date) + + summary = { + 'total_calories': 0, + 'total_protein': 0, + 'total_carbs': 0, + 'total_fat': 0, + 'total_fiber': 0 + } + + for meal in meals: + # 根據份量大小調整營養值 + multiplier = { + 'small': 0.7, + 'medium': 1.0, + 'large': 1.3 + }.get(meal.portion_size, 1.0) + + summary['total_calories'] += meal.calories * multiplier + summary['total_protein'] += meal.protein * multiplier + summary['total_carbs'] += meal.carbs * multiplier + summary['total_fat'] += meal.fat * multiplier + summary['total_fiber'] += meal.fiber * multiplier + + return summary diff --git a/app/services/nutrition_api_service.py b/app/services/nutrition_api_service.py new file mode 100644 index 0000000000000000000000000000000000000000..23a2e0dcc207c069ee1c840c5942c9ce71af908b --- /dev/null +++ b/app/services/nutrition_api_service.py @@ -0,0 +1,101 @@ +# backend/app/services/nutrition_api_service.py +import os +import requests +from dotenv import load_dotenv +import logging + +# 設置日誌 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# 載入環境變數 +load_dotenv() + +# 從環境變數中獲取 API 金鑰 +USDA_API_KEY = os.getenv("USDA_API_KEY", "DEMO_KEY") +USDA_API_URL = "https://api.nal.usda.gov/fdc/v1/foods/search" + +# 我們關心的主要營養素及其在 USDA API 中的名稱或編號 +# 我們可以透過 nutrient.nutrientNumber 或 nutrient.name 來匹配 +NUTRIENT_MAP = { + 'calories': 'Energy', + 'protein': 'Protein', + 'fat': 'Total lipid (fat)', + 'carbs': 'Carbohydrate, by difference', + 'fiber': 'Fiber, total dietary', + 'sugar': 'Total sugars', + 'sodium': 'Sodium, Na' +} + +def fetch_nutrition_data(food_name: str): + """ + 從 USDA FoodData Central API 獲取食物的營養資訊。 + + :param food_name: 要查詢的食物名稱 (例如 "Donuts")。 + :return: 包含營養資訊的字典,如果找不到則返回 None。 + """ + if not USDA_API_KEY: + logger.error("USDA_API_KEY 未設定,無法查詢營養資訊。") + return None + + params = { + 'query': food_name, + 'api_key': USDA_API_KEY, + 'dataType': 'Branded', # 優先搜尋品牌食品,結果通常更符合預期 + 'pageSize': 1 # 我們只需要最相關的一筆結果 + } + + try: + logger.info(f"正在向 USDA API 查詢食物:{food_name}") + response = requests.get(USDA_API_URL, params=params) + response.raise_for_status() # 如果請求失敗 (例如 4xx 或 5xx),則會拋出異常 + + data = response.json() + + # 檢查是否有找到食物 + if data.get('foods') and len(data['foods']) > 0: + food_data = data['foods'][0] # 取第一個最相關的結果 + logger.info(f"從 API 成功獲取到食物 '{food_data.get('description')}' 的資料") + + nutrition_info = { + "food_name": food_data.get('description', food_name).capitalize(), + "chinese_name": None, # USDA API 不提供中文名 + } + + # 遍歷我們需要的營養素 + extracted_nutrients = {key: 0.0 for key in NUTRIENT_MAP.keys()} # 初始化 + + for nutrient in food_data.get('foodNutrients', []): + for key, name in NUTRIENT_MAP.items(): + if nutrient.get('nutrientName').strip().lower() == name.strip().lower(): + # 將值存入我們的格式 + extracted_nutrients[key] = float(nutrient.get('value', 0.0)) + break # 找到後就跳出內層迴圈 + + nutrition_info.update(extracted_nutrients) + + # 由於 USDA 不直接提供健康建議,我們先回傳原始數據 + # 後續可以在 main.py 中根據這些數據生成我們自己的建議 + return nutrition_info + + else: + logger.warning(f"在 USDA API 中找不到食物:{food_name}") + return None + + except requests.exceptions.RequestException as e: + logger.error(f"請求 USDA API 時發生錯誤: {e}") + return None + except Exception as e: + logger.error(f"處理 API 回應時發生未知錯誤: {e}") + return None + +if __name__ == '__main__': + # 測試此模組的功能 + test_food = "donuts" + nutrition = fetch_nutrition_data(test_food) + if nutrition: + import json + print(f"成功獲取 '{test_food}' 的營養資訊:") + print(json.dumps(nutrition, indent=2)) + else: + print(f"無法獲取 '{test_food}' 的營養資訊。") \ No newline at end of file diff --git a/app/services/weight_estimation_service.py b/app/services/weight_estimation_service.py new file mode 100644 index 0000000000000000000000000000000000000000..f02e0fd5e8305830f5182a5c4220dab9aadd42d0 --- /dev/null +++ b/app/services/weight_estimation_service.py @@ -0,0 +1,296 @@ +# 檔案路徑: backend/app/services/weight_estimation_service.py + +import logging +import numpy as np +from PIL import Image +import io +from typing import Dict, Any, List, Optional, Tuple +import torch + +# 設置日誌 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# 食物密度表 (g/cm³) - 常見食物的平均密度 +FOOD_DENSITY_TABLE = { + "rice": 0.8, # 米飯 + "fried_rice": 0.7, # 炒飯 + "noodles": 0.6, # 麵條 + "bread": 0.3, # 麵包 + "meat": 1.0, # 肉類 + "fish": 1.1, # 魚類 + "vegetables": 0.4, # 蔬菜 + "fruits": 0.8, # 水果 + "soup": 1.0, # 湯類 + "default": 0.8 # 預設密度 +} + +# 參考物尺寸表 (cm) +REFERENCE_OBJECTS = { + "plate": {"diameter": 24.0}, # 標準餐盤直徑 + "bowl": {"diameter": 15.0}, # 標準碗直徑 + "spoon": {"length": 15.0}, # 湯匙長度 + "fork": {"length": 20.0}, # 叉子長度 + "default": {"diameter": 24.0} # 預設參考物 +} + +class WeightEstimationService: + def __init__(self): + """初始化重量估算服務""" + self.sam_model = None + self.dpt_model = None + self.detection_model = None + self._load_models() + + def _load_models(self): + """載入所需的 AI 模型""" + try: + # 載入 SAM 分割模型 + from transformers import SamModel, SamProcessor + logger.info("正在載入 SAM 分割模型...") + self.sam_model = SamModel.from_pretrained("facebook/sam-vit-base") + self.sam_processor = SamProcessor.from_pretrained("facebook/sam-vit-base") + + # 載入 DPT 深度估計模型 + from transformers import pipeline + logger.info("正在載入 DPT 深度估計模型...") + self.dpt_model = pipeline("depth-estimation", model="Intel/dpt-large") + + # 載入物件偵測模型(用於偵測參考物) + logger.info("正在載入物件偵測模型...") + self.detection_model = pipeline("object-detection", model="ultralytics/yolov5") + + logger.info("所有模型載入完成!") + + except Exception as e: + logger.error(f"模型載入失敗: {str(e)}") + raise + + def detect_reference_objects(self, image: Image.Image) -> Optional[Dict[str, Any]]: + """偵測圖片中的參考物(餐盤、餐具等)""" + try: + # 使用 YOLOv5 偵測物件 + results = self.detection_model(image) + + reference_objects = [] + for result in results: + label = result["label"].lower() + confidence = result["score"] + + # 檢查是否為參考物 + if any(ref in label for ref in ["plate", "bowl", "spoon", "fork", "knife"]): + reference_objects.append({ + "type": label, + "confidence": confidence, + "bbox": result["box"] + }) + + if reference_objects: + # 選擇信心度最高的參考物 + best_ref = max(reference_objects, key=lambda x: x["confidence"]) + return best_ref + + return None + + except Exception as e: + logger.warning(f"參考物偵測失敗: {str(e)}") + return None + + def segment_food(self, image: Image.Image) -> np.ndarray: + """使用 SAM 分割食物區域""" + try: + # 使用 SAM 進行分割 + inputs = self.sam_processor(image, return_tensors="pt") + + with torch.no_grad(): + outputs = self.sam_model(**inputs) + + # 取得分割遮罩 + masks = self.sam_processor.image_processor.post_process_masks( + outputs.pred_masks.sigmoid(), + inputs["original_sizes"], + inputs["reshaped_input_sizes"] + )[0] + + # 選擇最大的遮罩作為食物區域 + mask = masks[0].numpy() # 簡化處理,選擇第一個遮罩 + + return mask + + except Exception as e: + logger.error(f"食物分割失敗: {str(e)}") + # 回傳一個簡單的遮罩(整個圖片) + return np.ones((image.height, image.width), dtype=bool) + + def estimate_depth(self, image: Image.Image) -> np.ndarray: + """使用 DPT 進行深度估計""" + try: + # 使用 DPT 進行深度估計 + depth_result = self.dpt_model(image) + depth_map = depth_result["depth"] + + return np.array(depth_map) + + except Exception as e: + logger.error(f"深度估計失敗: {str(e)}") + # 回傳一個預設的深度圖 + return np.ones((image.height, image.width)) + + def calculate_volume_and_weight(self, + mask: np.ndarray, + depth_map: np.ndarray, + food_type: str, + reference_object: Optional[Dict[str, Any]] = None) -> Tuple[float, float, float]: + """計算體積和重量""" + try: + # 計算食物區域的像素數量 + food_pixels = np.sum(mask) + + # 計算食物區域的平均深度 + food_depth = np.mean(depth_map[mask]) + + # 估算體積(相對體積) + relative_volume = food_pixels * food_depth + + # 如果有參考物,進行尺寸校正 + if reference_object: + ref_type = reference_object["type"] + if ref_type in REFERENCE_OBJECTS: + ref_size = REFERENCE_OBJECTS[ref_type] + # 根據參考物尺寸校正體積 + if "diameter" in ref_size: + # 圓形參考物(如餐盤) + pixel_to_cm_ratio = ref_size["diameter"] / np.sqrt(food_pixels / np.pi) + else: + # 線性參考物(如餐具) + pixel_to_cm_ratio = ref_size["length"] / np.sqrt(food_pixels) + + # 校正體積 + actual_volume = relative_volume * (pixel_to_cm_ratio ** 3) + confidence = 0.85 # 有參考物時信心度較高 + error_range = 0.15 # ±15% 誤差 + else: + actual_volume = relative_volume * 0.1 # 預設校正係數 + confidence = 0.6 + error_range = 0.3 + else: + # 無參考物,使用預設值 + actual_volume = relative_volume * 0.1 # 預設校正係數 + confidence = 0.5 # 無參考物時信心度較低 + error_range = 0.4 # ±40% 誤差 + + # 根據食物類型取得密度 + density = FOOD_DENSITY_TABLE.get(food_type.lower(), FOOD_DENSITY_TABLE["default"]) + + # 計算重量 (g) + weight = actual_volume * density + + return weight, confidence, error_range + + except Exception as e: + logger.error(f"體積重量計算失敗: {str(e)}") + return 150.0, 0.3, 0.5 # 預設值 + + def get_food_density(self, food_name: str) -> float: + """根據食物名稱取得密度""" + food_name_lower = food_name.lower() + + # 簡單的關鍵字匹配 + if any(keyword in food_name_lower for keyword in ["rice", "飯"]): + return FOOD_DENSITY_TABLE["rice"] + elif any(keyword in food_name_lower for keyword in ["noodle", "麵"]): + return FOOD_DENSITY_TABLE["noodles"] + elif any(keyword in food_name_lower for keyword in ["meat", "肉"]): + return FOOD_DENSITY_TABLE["meat"] + elif any(keyword in food_name_lower for keyword in ["vegetable", "菜"]): + return FOOD_DENSITY_TABLE["vegetables"] + else: + return FOOD_DENSITY_TABLE["default"] + +# 全域服務實例 +weight_service = WeightEstimationService() + +async def estimate_food_weight(image_bytes: bytes) -> Dict[str, Any]: + """ + 整合食物辨識、重量估算與營養分析的主函數 + """ + try: + # 將 bytes 轉換為 PIL Image + image = Image.open(io.BytesIO(image_bytes)) + + # 1. 食物辨識(使用現有的 AI 服務) + from .ai_service import classify_food_image + food_name = classify_food_image(image_bytes) + + # 2. 偵測參考物 + reference_object = weight_service.detect_reference_objects(image) + + # 3. 食物分割 + food_mask = weight_service.segment_food(image) + + # 4. 深度估計 + depth_map = weight_service.estimate_depth(image) + + # 5. 計算體積和重量 + weight, confidence, error_range = weight_service.calculate_volume_and_weight( + food_mask, depth_map, food_name, reference_object + ) + + # 6. 查詢營養資訊 + from .nutrition_api_service import fetch_nutrition_data + nutrition_info = fetch_nutrition_data(food_name) + + if nutrition_info is None: + nutrition_info = { + "calories": 150, + "protein": 5, + "carbs": 25, + "fat": 3, + "fiber": 2 + } + + # 7. 根據重量調整營養素 + weight_ratio = weight / 100 # 假設營養資訊是每100g的數據 + adjusted_nutrition = { + key: value * weight_ratio + for key, value in nutrition_info.items() + } + + # 8. 計算誤差範圍 + error_min = weight * (1 - error_range) + error_max = weight * (1 + error_range) + + # 9. 生成備註 + if reference_object: + note = f"檢測到參考物:{reference_object['type']},準確度較高" + else: + note = "未檢測到參考物,重量為估算值,僅供參考" + + return { + "food_type": food_name, + "estimated_weight": round(weight, 1), + "weight_confidence": round(confidence, 2), + "weight_error_range": [round(error_min, 1), round(error_max, 1)], + "nutrition": adjusted_nutrition, + "reference_object": reference_object["type"] if reference_object else None, + "note": note + } + + except Exception as e: + logger.error(f"重量估算失敗: {str(e)}") + # 回傳預設結果 + return { + "food_type": "Unknown", + "estimated_weight": 150.0, + "weight_confidence": 0.3, + "weight_error_range": [100.0, 200.0], + "nutrition": { + "calories": 150, + "protein": 5, + "carbs": 25, + "fat": 3, + "fiber": 2 + }, + "reference_object": None, + "note": "分析失敗,顯示預設值" + } \ No newline at end of file diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000000000000000000000000000000000000..b3fc3d543aa48ab7c7601f0eb1b7f24dedf76802 --- /dev/null +++ b/backend/.env @@ -0,0 +1,13 @@ +# 資料庫設定 +DATABASE_URL=sqlite:///./data/health_assistant.db + +# Redis 設定 +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_DB=0 + +# API 設定 +API_HOST=localhost +API_PORT=8000 + +USDA_API_KEY="0bZCszzPSbc5r6RXfm0aHKtWGX2V0SX1hLiMmXwi" diff --git a/backend/__pycache__/app.cpython-313.pyc b/backend/__pycache__/app.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..231535dd5b045fff462b480376b45f1ba6f31849 Binary files /dev/null and b/backend/__pycache__/app.cpython-313.pyc differ diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b7de3f97bdee2e5cf6da3ea552cf0f96d0378f7 Binary files /dev/null and b/backend/__pycache__/main.cpython-313.pyc differ diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..ed52d271653c689e625e2b8fcd8de6bb4cb5b46e --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,8 @@ +fastapi +uvicorn +transformers +torch +pillow +opencv-python +python-multipart +requests \ No newline at end of file diff --git a/backend/setup.py b/backend/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..8053af6eed72a75288a8ee5bd5f6b835780d9dd6 --- /dev/null +++ b/backend/setup.py @@ -0,0 +1,47 @@ +import os +import sys +from pathlib import Path + +def setup_project(): + """設置專案目錄結構和必要檔案""" + # 獲取專案根目錄 + project_root = Path(__file__).parent + + # 創建必要的目錄 + directories = [ + 'data', # 資料庫目錄 + 'logs', # 日誌目錄 + 'uploads' # 上傳檔案目錄 + ] + + for directory in directories: + dir_path = project_root / directory + dir_path.mkdir(exist_ok=True) + print(f"Created directory: {dir_path}") + + # 創建 .env 檔案(如果不存在) + env_file = project_root / '.env' + if not env_file.exists(): + with open(env_file, 'w', encoding='utf-8') as f: + f.write("""# 資料庫設定 +DATABASE_URL=sqlite:///./data/health_assistant.db + +# Redis 設定 +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_DB=0 + +# API 設定 +API_HOST=localhost +API_PORT=8000 +""") + print(f"Created .env file: {env_file}") + + print("\nProject setup completed successfully!") + print("\nNext steps:") + print("1. Install required packages: pip install -r requirements.txt") + print("2. Start the Redis server") + print("3. Run the application: uvicorn app.main:app --reload") + +if __name__ == "__main__": + setup_project() diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..aa226358c7dabd3f318f77fa77eff852f57de9df --- /dev/null +++ b/conftest.py @@ -0,0 +1,7 @@ +import os +import sys +from pathlib import Path + +# Add the backend directory to the Python path +sys.path.insert(0, str(Path(__file__).parent / "backend")) +sys.path.insert(0, str(Path(__file__).parent)) diff --git a/coverage.ini b/coverage.ini new file mode 100644 index 0000000000000000000000000000000000000000..119a0d4950b1f5962a7006e198262724bd9e66e2 --- /dev/null +++ b/coverage.ini @@ -0,0 +1,22 @@ +[run] +source = . +omit = + */tests/* + */venv/* + */.venv/* + */env/* + */.env/* + */site-packages/* + +[report] +exclude_lines = + pragma: no cover + def __repr__ + if self\.debug + raise AssertionError + raise NotImplementedError + if 0: + if __name__ == .__main__. + +[html] +directory = htmlcov diff --git a/database.py b/database.py new file mode 100644 index 0000000000000000000000000000000000000000..5fca1305a7fff7ff16722a008cb590fe8cb0ea08 --- /dev/null +++ b/database.py @@ -0,0 +1,28 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +import os + + +DB_DIR = "/tmp/data" +os.makedirs(DB_DIR, exist_ok=True) + +SQLALCHEMY_DATABASE_URL = f"sqlite:///{os.path.join(DB_DIR, 'health_assistant.db')}" + + + +# 創建資料庫引擎 +engine = create_engine( + SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False} +) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/deploy.ps1 b/deploy.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..9ac8c34c17ea282b02fd4bfe562a6899c4f298dd --- /dev/null +++ b/deploy.ps1 @@ -0,0 +1,38 @@ +# 部署腳本 - 確保代碼推送到 GitHub 和 Hugging Face Spaces + +Write-Host "🚀 開始部署流程..." -ForegroundColor Green + +# 檢查是否有未提交的更改 +$status = git status --porcelain +if ($status) { + Write-Host "📝 發現未提交的更改,正在添加..." -ForegroundColor Yellow + git add . + + Write-Host "💾 提交更改..." -ForegroundColor Yellow + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + git commit -m "Auto-deploy: $timestamp" +} else { + Write-Host "✅ 沒有未提交的更改" -ForegroundColor Green +} + +# 推送到 GitHub +Write-Host "📤 推送到 GitHub..." -ForegroundColor Yellow +git push origin main + +if ($LASTEXITCODE -eq 0) { + Write-Host "✅ GitHub 推送成功!" -ForegroundColor Green + Write-Host "" + Write-Host "🔗 GitHub 倉庫: https://github.com/ting1234555/health_assistant" -ForegroundColor Cyan + Write-Host "🔗 Hugging Face Space: https://huggingface.co/spaces/yuting111222/health-assistant" -ForegroundColor Cyan + Write-Host "" + Write-Host "📋 下一步操作:" -ForegroundColor Yellow + Write-Host "1. 訪問 Hugging Face Spaces 設定頁面" -ForegroundColor White + Write-Host "2. 點擊 'Factory rebuild' 按鈕" -ForegroundColor White + Write-Host "3. 等待重建完成(1-5分鐘)" -ForegroundColor White + Write-Host "4. 檢查 /docs 頁面確認 API 端點" -ForegroundColor White + Write-Host "" + Write-Host "🎯 部署完成!" -ForegroundColor Green +} else { + Write-Host "❌ GitHub 推送失敗!" -ForegroundColor Red + exit 1 +} \ No newline at end of file diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000000000000000000000000000000000000..b57d5dc9c809db214138179d6f90045346ed8275 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# 部署腳本 - 確保代碼推送到 GitHub 和 Hugging Face Spaces + +echo "🚀 開始部署流程..." + +# 檢查是否有未提交的更改 +if [ -n "$(git status --porcelain)" ]; then + echo "📝 發現未提交的更改,正在添加..." + git add . + + echo "💾 提交更改..." + git commit -m "Auto-deploy: $(date)" +else + echo "✅ 沒有未提交的更改" +fi + +# 推送到 GitHub +echo "📤 推送到 GitHub..." +git push origin main + +if [ $? -eq 0 ]; then + echo "✅ GitHub 推送成功!" + echo "" + echo "🔗 GitHub 倉庫: https://github.com/ting1234555/health_assistant" + echo "🔗 Hugging Face Space: https://huggingface.co/spaces/yuting111222/health-assistant" + echo "" + echo "📋 下一步操作:" + echo "1. 訪問 Hugging Face Spaces 設定頁面" + echo "2. 點擊 'Factory rebuild' 按鈕" + echo "3. 等待重建完成(1-5分鐘)" + echo "4. 檢查 /docs 頁面確認 API 端點" + echo "" + echo "🎯 部署完成!" +else + echo "❌ GitHub 推送失敗!" + exit 1 +fi \ No newline at end of file diff --git a/env_example.txt b/env_example.txt new file mode 100644 index 0000000000000000000000000000000000000000..d475393e7331b0c52a7a9d2984de27975ff70892 --- /dev/null +++ b/env_example.txt @@ -0,0 +1,8 @@ +# USDA API Key for nutrition data (get from https://fdc.nal.usda.gov/api-key-signup.html) +USDA_API_KEY=your_usda_api_key_here + +# Database configuration (optional, defaults to SQLite) +DATABASE_URL=sqlite:///tmp/data/health_assistant.db + +# Hugging Face Spaces configuration +HF_SPACE_ID=your_space_id \ No newline at end of file diff --git a/food_analyzer.py b/food_analyzer.py new file mode 100644 index 0000000000000000000000000000000000000000..143dd33eab4bef93634d47a71ae49a09a758b3e5 --- /dev/null +++ b/food_analyzer.py @@ -0,0 +1,339 @@ +from fastapi import FastAPI, UploadFile, File, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from PIL import Image +import io +import base64 +from transformers import pipeline +import requests +import json +from typing import Dict, Any +from pydantic import BaseModel +import uvicorn + +app = FastAPI(title="Health Assistant AI - Food Recognition API") + +# CORS設定 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # 生產環境請設定具體的域名 + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 初始化Hugging Face模型 +try: + # 使用nateraw/food專門的食物分類模型 + food_classifier = pipeline( + "image-classification", + model="nateraw/food", + device=-1 # 使用CPU,如果有GPU可以設為0 + ) + print("nateraw/food 食物辨識模型載入成功") +except Exception as e: + print(f"模型載入失敗: {e}") + food_classifier = None + +# 食物營養資料庫(擴展版,涵蓋nateraw/food模型常見的食物類型) +NUTRITION_DATABASE = { + # 水果類 + "apple": {"name": "蘋果", "calories_per_100g": 52, "protein": 0.3, "carbs": 14, "fat": 0.2, "fiber": 2.4, "sugar": 10.4, "vitamin_c": 4.6}, + "banana": {"name": "香蕉", "calories_per_100g": 89, "protein": 1.1, "carbs": 23, "fat": 0.3, "fiber": 2.6, "sugar": 12.2, "potassium": 358}, + "orange": {"name": "橘子", "calories_per_100g": 47, "protein": 0.9, "carbs": 12, "fat": 0.1, "fiber": 2.4, "sugar": 9.4, "vitamin_c": 53.2}, + "strawberry": {"name": "草莓", "calories_per_100g": 32, "protein": 0.7, "carbs": 7.7, "fat": 0.3, "fiber": 2, "sugar": 4.9, "vitamin_c": 58.8}, + "grape": {"name": "葡萄", "calories_per_100g": 62, "protein": 0.6, "carbs": 16.8, "fat": 0.2, "fiber": 0.9, "sugar": 16.1}, + + # 主食類 + "bread": {"name": "麵包", "calories_per_100g": 265, "protein": 9, "carbs": 49, "fat": 3.2, "fiber": 2.7, "sodium": 491}, + "rice": {"name": "米飯", "calories_per_100g": 130, "protein": 2.7, "carbs": 28, "fat": 0.3, "fiber": 0.4}, + "pasta": {"name": "義大利麵", "calories_per_100g": 131, "protein": 5, "carbs": 25, "fat": 1.1, "fiber": 1.8}, + "noodles": {"name": "麵條", "calories_per_100g": 138, "protein": 4.5, "carbs": 25, "fat": 2.2, "fiber": 1.2}, + "pizza": {"name": "披薩", "calories_per_100g": 266, "protein": 11, "carbs": 33, "fat": 10, "sodium": 598}, + + # 肉類 + "chicken": {"name": "雞肉", "calories_per_100g": 165, "protein": 31, "carbs": 0, "fat": 3.6, "iron": 0.9}, + "beef": {"name": "牛肉", "calories_per_100g": 250, "protein": 26, "carbs": 0, "fat": 15, "iron": 2.6, "zinc": 4.8}, + "pork": {"name": "豬肉", "calories_per_100g": 242, "protein": 27, "carbs": 0, "fat": 14, "thiamine": 0.7}, + "fish": {"name": "魚肉", "calories_per_100g": 206, "protein": 22, "carbs": 0, "fat": 12, "omega_3": "豐富"}, + + # 蔬菜類 + "broccoli": {"name": "花椰菜", "calories_per_100g": 34, "protein": 2.8, "carbs": 7, "fat": 0.4, "fiber": 2.6, "vitamin_c": 89.2}, + "carrot": {"name": "胡蘿蔔", "calories_per_100g": 41, "protein": 0.9, "carbs": 10, "fat": 0.2, "fiber": 2.8, "vitamin_a": 835}, + "tomato": {"name": "番茄", "calories_per_100g": 18, "protein": 0.9, "carbs": 3.9, "fat": 0.2, "fiber": 1.2, "vitamin_c": 13.7}, + "lettuce": {"name": "萵苣", "calories_per_100g": 15, "protein": 1.4, "carbs": 2.9, "fat": 0.2, "fiber": 1.3, "folate": 38}, + + # 飲品類 + "coffee": {"name": "咖啡", "calories_per_100g": 2, "protein": 0.3, "carbs": 0, "fat": 0, "caffeine": 95}, + "tea": {"name": "茶", "calories_per_100g": 1, "protein": 0, "carbs": 0.3, "fat": 0, "antioxidants": "豐富"}, + "milk": {"name": "牛奶", "calories_per_100g": 42, "protein": 3.4, "carbs": 5, "fat": 1, "calcium": 113}, + "juice": {"name": "果汁", "calories_per_100g": 45, "protein": 0.7, "carbs": 11, "fat": 0.2, "vitamin_c": "因果汁種類而異"}, + + # 甜點類 + "cake": {"name": "蛋糕", "calories_per_100g": 257, "protein": 4, "carbs": 46, "fat": 6, "sugar": 35}, + "cookie": {"name": "餅乾", "calories_per_100g": 502, "protein": 5.9, "carbs": 64, "fat": 25, "sugar": 39}, + "ice_cream": {"name": "冰淇淋", "calories_per_100g": 207, "protein": 3.5, "carbs": 24, "fat": 11, "sugar": 21}, + "chocolate": {"name": "巧克力", "calories_per_100g": 546, "protein": 4.9, "carbs": 61, "fat": 31, "sugar": 48}, + + # 其他常見食物 + "egg": {"name": "雞蛋", "calories_per_100g": 155, "protein": 13, "carbs": 1.1, "fat": 11, "choline": 294}, + "cheese": {"name": "起司", "calories_per_100g": 113, "protein": 7, "carbs": 1, "fat": 9, "calcium": 200}, + "yogurt": {"name": "優格", "calories_per_100g": 59, "protein": 10, "carbs": 3.6, "fat": 0.4, "probiotics": "豐富"}, + "nuts": {"name": "堅果", "calories_per_100g": 607, "protein": 15, "carbs": 7, "fat": 54, "vitamin_e": 26}, + "salad": {"name": "沙拉", "calories_per_100g": 20, "protein": 1.5, "carbs": 4, "fat": 0.2, "fiber": 2, "vitamins": "多種維生素"} +} + +# 回應模型 +class FoodAnalysisResponse(BaseModel): + success: bool + food_name: str + confidence: float + nutrition_info: Dict[str, Any] + ai_suggestions: list + message: str + +class HealthResponse(BaseModel): + status: str + message: str + +def get_nutrition_info(food_name: str) -> Dict[str, Any]: + """根據食物名稱獲取營養資訊""" + # 將食物名稱轉為小寫並清理 + food_key = food_name.lower().strip() + + # 移除常見的修飾詞和格式化字符 + food_key = food_key.replace("_", " ").replace("-", " ") + + # 直接匹配 + if food_key in NUTRITION_DATABASE: + return NUTRITION_DATABASE[food_key] + + # 模糊匹配 - 檢查是否包含關鍵字 + for key, value in NUTRITION_DATABASE.items(): + if key in food_key or food_key in key: + return value + # 也檢查中文名稱 + if value["name"] in food_name: + return value + + # 更智能的匹配 - 處理複合詞 + food_words = food_key.split() + for word in food_words: + for key, value in NUTRITION_DATABASE.items(): + if word == key or word in key: + return value + + # 特殊情況處理 + special_mappings = { + "french fries": "potato", + "hamburger": "beef", + "sandwich": "bread", + "soda": "juice", + "water": {"name": "水", "calories_per_100g": 0, "protein": 0, "carbs": 0, "fat": 0}, + "soup": {"name": "湯", "calories_per_100g": 50, "protein": 2, "carbs": 8, "fat": 1, "sodium": 400} + } + + for special_key, mapping in special_mappings.items(): + if special_key in food_key: + if isinstance(mapping, str): + return NUTRITION_DATABASE.get(mapping, {"name": food_name, "message": "營養資料不完整"}) + else: + return mapping + + # 如果沒有找到,返回預設值 + return { + "name": food_name, + "calories_per_100g": "未知", + "protein": "未知", + "carbs": "未知", + "fat": "未知", + "message": f"抱歉,暫時沒有「{food_name}」的詳細營養資料,建議查詢專業營養資料庫" + } + +def generate_ai_suggestions(food_name: str, nutrition_info: Dict) -> list: + """根據食物和營養資訊生成AI建議""" + suggestions = [] + food_name_lower = food_name.lower() + + # 檢查是否有完整的營養資訊 + if isinstance(nutrition_info.get("calories_per_100g"), (int, float)): + calories = nutrition_info["calories_per_100g"] + + # 熱量相關建議 + if calories > 400: + suggestions.append("⚠️ 這是高熱量食物,建議控制份量,搭配運動") + elif calories > 200: + suggestions.append("🍽️ 中等熱量食物,適量食用,建議搭配蔬菜") + elif calories < 50: + suggestions.append("✅ 低熱量食物,適合減重期間食用") + + # 營養素相關建議 + protein = nutrition_info.get("protein", 0) + if isinstance(protein, (int, float)) and protein > 20: + suggestions.append("💪 高蛋白食物,有助於肌肉發展和修復") + + fiber = nutrition_info.get("fiber", 0) + if isinstance(fiber, (int, float)) and fiber > 3: + suggestions.append("🌿 富含纖維,有助於消化健康和增加飽足感") + + sugar = nutrition_info.get("sugar", 0) + if isinstance(sugar, (int, float)) and sugar > 20: + suggestions.append("🍯 含糖量較高,建議適量食用,避免血糖快速上升") + + # 特殊營養素 + if nutrition_info.get("vitamin_c", 0) > 30: + suggestions.append("🍊 富含維生素C,有助於增強免疫力和抗氧化") + + if nutrition_info.get("calcium", 0) > 100: + suggestions.append("🦴 富含鈣質,有助於骨骼和牙齒健康") + + if nutrition_info.get("omega_3"): + suggestions.append("🐟 含有Omega-3脂肪酸,對心血管健康有益") + + # 根據食物類型給出特定建議 + if any(fruit in food_name_lower for fruit in ["apple", "banana", "orange", "strawberry", "grape"]): + suggestions.append("🍎 建議在餐前或運動前食用,提供天然糖分和維生素") + + elif any(meat in food_name_lower for meat in ["chicken", "beef", "pork", "fish"]): + suggestions.append("🥩 建議搭配蔬菜食用,選擇健康的烹調方式(烤、蒸、煮)") + + elif any(sweet in food_name_lower for sweet in ["cake", "cookie", "ice_cream", "chocolate"]): + suggestions.append("🍰 甜點建議偶爾享用,可在運動後適量食用") + suggestions.append("💡 可以考慮與朋友分享,減少單次攝取量") + + elif any(drink in food_name_lower for drink in ["coffee", "tea"]): + suggestions.append("☕ 建議控制咖啡因攝取量,避免影響睡眠") + + elif "salad" in food_name_lower: + suggestions.append("🥗 很棒的選擇!可以添加堅果或橄欖油增加健康脂肪") + + # 通用健康建議 + if not suggestions: + suggestions.extend([ + "🍽️ 建議均衡飲食,搭配多樣化的食物", + "💧 記得多喝水,保持身體水分充足", + "🏃‍♂️ 搭配適量運動,維持健康生活型態" + ]) + else: + # 添加一些通用的健康提醒 + suggestions.append("💧 記得多喝水,幫助營養吸收") + if len(suggestions) < 4: + suggestions.append("⚖️ 注意食物份量,適量攝取是健康飲食的關鍵") + + return suggestions[:5] # 限制建議數量,避免過多 + +@app.get("/", response_model=HealthResponse) +async def root(): + """API根路径""" + return HealthResponse( + status="success", + message="Health Assistant AI - Food Recognition API is running!" + ) + +@app.get("/health", response_model=HealthResponse) +async def health_check(): + """健康檢查端點""" + model_status = "正常" if food_classifier else "模型載入失敗" + return HealthResponse( + status="success", + message=f"API運行正常,模型狀態: {model_status}" + ) + +@app.post("/analyze-food", response_model=FoodAnalysisResponse) +async def analyze_food(file: UploadFile = File(...)): + """分析上傳的食物圖片""" + try: + # 檢查模型是否載入成功 + if not food_classifier: + raise HTTPException(status_code=500, detail="AI模型尚未載入,請稍後再試") + + # 檢查文件類型 + if not file.content_type.startswith("image/"): + raise HTTPException(status_code=400, detail="請上傳圖片文件") + + # 讀取圖片 + image_data = await file.read() + image = Image.open(io.BytesIO(image_data)) + + # 確保圖片是RGB格式 + if image.mode != "RGB": + image = image.convert("RGB") + + # 使用AI模型進行食物辨識 + results = food_classifier(image) + + # 獲取最高信心度的結果 + top_result = results[0] + food_name = top_result["label"] + confidence = top_result["score"] + + # 獲取營養資訊 + nutrition_info = get_nutrition_info(food_name) + + # 生成AI建議 + ai_suggestions = generate_ai_suggestions(food_name, nutrition_info) + + return FoodAnalysisResponse( + success=True, + food_name=food_name, + confidence=round(confidence * 100, 2), + nutrition_info=nutrition_info, + ai_suggestions=ai_suggestions, + message="食物分析完成" + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"分析失敗: {str(e)}") + +@app.post("/analyze-food-base64", response_model=FoodAnalysisResponse) +async def analyze_food_base64(image_data: dict): + """分析base64編碼的食物圖片""" + try: + # 檢查模型是否載入成功 + if not food_classifier: + raise HTTPException(status_code=500, detail="AI模型尚未載入,請稍後再試") + + # 解碼base64圖片 + base64_string = image_data.get("image", "") + if not base64_string: + raise HTTPException(status_code=400, detail="缺少圖片資料") + + # 移除base64前綴(如果有的話) + if "," in base64_string: + base64_string = base64_string.split(",")[1] + + # 解碼圖片 + image_bytes = base64.b64decode(base64_string) + image = Image.open(io.BytesIO(image_bytes)) + + # 確保圖片是RGB格式 + if image.mode != "RGB": + image = image.convert("RGB") + + # 使用AI模型進行食物辨識 + results = food_classifier(image) + + # 獲取最高信心度的結果 + top_result = results[0] + food_name = top_result["label"] + confidence = top_result["score"] + + # 獲取營養資訊 + nutrition_info = get_nutrition_info(food_name) + + # 生成AI建議 + ai_suggestions = generate_ai_suggestions(food_name, nutrition_info) + + return FoodAnalysisResponse( + success=True, + food_name=food_name, + confidence=round(confidence * 100, 2), + nutrition_info=nutrition_info, + ai_suggestions=ai_suggestions, + message="食物分析完成" + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"分析失敗: {str(e)}") + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/frontend/ai_food_analyzer.html b/frontend/ai_food_analyzer.html new file mode 100644 index 0000000000000000000000000000000000000000..03808799f09fb38f30b901ea11bc56aacf3e23aa --- /dev/null +++ b/frontend/ai_food_analyzer.html @@ -0,0 +1,1679 @@ + + + + + + AI食物營養分析器 - 個人化版本 + + + + +
+
+

🍎 AI食物營養分析器

+

智能分析您的飲食營養

+
+ +
+ + + +
+ + +
+ +
+
+ + +
+ +
+ + + + +
+ +
+
+

AI正在分析食物中...

+
+ +
+
+

識別的食物

+ +
+ +
+
+
健康指數
+
+
+
+
--/100
+
+
+
升糖指數
+
+
+
+
--/100
+
+
+ +

這裡會顯示食物的詳細描述和營養價值。

+ +
+ +
+
+

基本營養素

+
+
+
+

維生素

+
+
+
+

礦物質

+
+
+
+ +
+
+
+
+ + +
+
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ + +
+
+ + +
+ +
+
+ +
+
+
🔥
+
+
0
+
今日攝取熱量
+
+
+
+
🎯
+
+
0
+
每日目標熱量
+
+
+
+ +
+

今日目標進度

+
+
+ +
+
+

今日食物記錄

+ +
+
+
+ +
+
+

本週熱量趨勢

+
+
+ +
+
+
+
+
+ + + \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000000000000000000000000000000000000..0623df6dc14930ba1b8936573924d1ad472af525 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + 健康助手 + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..58117b68d254395c85ed0ab3eccb06a2fec6de6e --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,6802 @@ +{ + "name": "health-assistant", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "health-assistant", + "version": "0.1.0", + "dependencies": { + "@headlessui/react": "^1.7.17", + "@heroicons/react": "^2.0.18", + "@tensorflow-models/mobilenet": "^2.1.1", + "@tensorflow/tfjs": "^4.11.0", + "axios": "^1.6.2", + "chart.js": "^4.4.0", + "lucide-react": "^0.525.0", + "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0", + "react-toastify": "^9.1.3", + "recharts": "^3.0.2" + }, + "devDependencies": { + "@types/react": "^18.2.37", + "@types/react-dom": "^18.2.15", + "@vitejs/plugin-react": "^4.2.0", + "autoprefixer": "^10.4.16", + "eslint": "^8.53.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.4", + "postcss": "^8.4.31", + "tailwindcss": "^3.3.5", + "vite": "^5.0.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/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", + "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", + "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz", + "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "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.6.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", + "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", + "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/eslintrc/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/@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/@headlessui/react": { + "version": "1.7.19", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz", + "integrity": "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==", + "license": "MIT", + "dependencies": { + "@tanstack/react-virtual": "^3.0.0-beta.60", + "client-only": "^0.0.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, + "node_modules/@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.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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/@isaacs/cliui/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/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "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/@isaacs/cliui/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/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "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/@reduxjs/toolkit": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", + "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "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/@rollup/rollup-android-arm-eabi": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", + "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz", + "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz", + "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz", + "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz", + "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz", + "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz", + "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz", + "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz", + "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz", + "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz", + "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz", + "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz", + "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz", + "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz", + "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", + "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz", + "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz", + "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz", + "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz", + "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.6.tgz", + "integrity": "sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.6", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.6.tgz", + "integrity": "sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tensorflow-models/mobilenet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@tensorflow-models/mobilenet/-/mobilenet-2.1.1.tgz", + "integrity": "sha512-tv4s4UFzG74PkIwl4gT64AyRnCcNUq+s8wSzge+LN/Puc1VUuInZghrobvpNlWjZtVi1x1d1NsBD//TfOr2ssA==", + "license": "Apache-2.0", + "peerDependencies": { + "@tensorflow/tfjs-converter": "^4.9.0", + "@tensorflow/tfjs-core": "^4.9.0" + } + }, + "node_modules/@tensorflow/tfjs": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs/-/tfjs-4.22.0.tgz", + "integrity": "sha512-0TrIrXs6/b7FLhLVNmfh8Sah6JgjBPH4mZ8JGb7NU6WW+cx00qK5BcAZxw7NCzxj6N8MRAIfHq+oNbPUNG5VAg==", + "license": "Apache-2.0", + "dependencies": { + "@tensorflow/tfjs-backend-cpu": "4.22.0", + "@tensorflow/tfjs-backend-webgl": "4.22.0", + "@tensorflow/tfjs-converter": "4.22.0", + "@tensorflow/tfjs-core": "4.22.0", + "@tensorflow/tfjs-data": "4.22.0", + "@tensorflow/tfjs-layers": "4.22.0", + "argparse": "^1.0.10", + "chalk": "^4.1.0", + "core-js": "3.29.1", + "regenerator-runtime": "^0.13.5", + "yargs": "^16.0.3" + }, + "bin": { + "tfjs-custom-module": "dist/tools/custom_module/cli.js" + } + }, + "node_modules/@tensorflow/tfjs-backend-cpu": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-4.22.0.tgz", + "integrity": "sha512-1u0FmuLGuRAi8D2c3cocHTASGXOmHc/4OvoVDENJayjYkS119fcTcQf4iHrtLthWyDIPy3JiPhRrZQC9EwnhLw==", + "license": "Apache-2.0", + "dependencies": { + "@types/seedrandom": "^2.4.28", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@tensorflow/tfjs-backend-webgl": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-4.22.0.tgz", + "integrity": "sha512-H535XtZWnWgNwSzv538czjVlbJebDl5QTMOth4RXr2p/kJ1qSIXE0vZvEtO+5EC9b00SvhplECny2yDewQb/Yg==", + "license": "Apache-2.0", + "dependencies": { + "@tensorflow/tfjs-backend-cpu": "4.22.0", + "@types/offscreencanvas": "~2019.3.0", + "@types/seedrandom": "^2.4.28", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@tensorflow/tfjs-converter": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.22.0.tgz", + "integrity": "sha512-PT43MGlnzIo+YfbsjM79Lxk9lOq6uUwZuCc8rrp0hfpLjF6Jv8jS84u2jFb+WpUeuF4K33ZDNx8CjiYrGQ2trQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@tensorflow/tfjs-core": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.22.0.tgz", + "integrity": "sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A==", + "license": "Apache-2.0", + "dependencies": { + "@types/long": "^4.0.1", + "@types/offscreencanvas": "~2019.7.0", + "@types/seedrandom": "^2.4.28", + "@webgpu/types": "0.1.38", + "long": "4.0.0", + "node-fetch": "~2.6.1", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + } + }, + "node_modules/@tensorflow/tfjs-core/node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, + "node_modules/@tensorflow/tfjs-data": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-data/-/tfjs-data-4.22.0.tgz", + "integrity": "sha512-dYmF3LihQIGvtgJrt382hSRH4S0QuAp2w1hXJI2+kOaEqo5HnUPG0k5KA6va+S1yUhx7UBToUKCBHeLHFQRV4w==", + "license": "Apache-2.0", + "dependencies": { + "@types/node-fetch": "^2.1.2", + "node-fetch": "~2.6.1", + "string_decoder": "^1.3.0" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0", + "seedrandom": "^3.0.5" + } + }, + "node_modules/@tensorflow/tfjs-layers": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-layers/-/tfjs-layers-4.22.0.tgz", + "integrity": "sha512-lybPj4ZNj9iIAPUj7a8ZW1hg8KQGfqWLlCZDi9eM/oNKCCAgchiyzx8OrYoWmRrB+AM6VNEeIT+2gZKg5ReihA==", + "license": "Apache-2.0 AND MIT", + "peerDependencies": { + "@tensorflow/tfjs-core": "4.22.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.3.0", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz", + "integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.20", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz", + "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.6.tgz", + "integrity": "sha512-nf22//wEbKXusP6E9pfOCDwFdHAX4u172eaJI4YkDRQEZiorm6KfYnSC2SWLDMVWUOWPERmJnN0ujeAfTBLvrw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/seedrandom": { + "version": "2.4.34", + "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.34.tgz", + "integrity": "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A==", + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "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.4.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", + "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.10", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/@webgpu/types": { + "version": "0.1.38", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.38.tgz", + "integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA==", + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/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": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/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.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/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.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.30001715", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", + "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", + "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==", + "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/chart.js": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", + "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "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/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/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/core-js": { + "version": "3.29.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.29.1.tgz", + "integrity": "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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.139", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.139.tgz", + "integrity": "sha512-GGnRYOTdN5LYpwbIr0rwP/ZHOQSvAF6TG0LSzp28uCBb9JiXHJGmaaKw29qjNJc5bGnnp6kXJqRnGMQoELwi5w==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "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/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-toolkit": { + "version": "1.39.5", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.5.tgz", + "integrity": "sha512-z9V0qU4lx1TBXDNFWfAASWk6RNU6c6+TJBKE+FLIg8u0XJ6Yw58Hi0yX8ftEouj6p1QARRlXLFfHbIli93BdQQ==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "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==", + "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": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "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.19", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz", + "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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-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/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/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/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-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.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/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/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "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-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "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/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.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-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "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/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/js-yaml/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/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/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "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/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "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.525.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", + "integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==", + "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/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "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/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/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-import/node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "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/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "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-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.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-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", + "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", + "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.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", + "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-toastify": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz", + "integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==", + "license": "MIT", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "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/recharts": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.0.2.tgz", + "integrity": "sha512-eDc3ile9qJU9Dp/EekSthQPhAVPG48/uM47jk+PF7VBQngxeW3cwQpPHb/GHC1uqwyCRWXcIrDzuHRVrnRryoQ==", + "license": "MIT", + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "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.40.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", + "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.0", + "@rollup/rollup-android-arm64": "4.40.0", + "@rollup/rollup-darwin-arm64": "4.40.0", + "@rollup/rollup-darwin-x64": "4.40.0", + "@rollup/rollup-freebsd-arm64": "4.40.0", + "@rollup/rollup-freebsd-x64": "4.40.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", + "@rollup/rollup-linux-arm-musleabihf": "4.40.0", + "@rollup/rollup-linux-arm64-gnu": "4.40.0", + "@rollup/rollup-linux-arm64-musl": "4.40.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-musl": "4.40.0", + "@rollup/rollup-linux-s390x-gnu": "4.40.0", + "@rollup/rollup-linux-x64-gnu": "4.40.0", + "@rollup/rollup-linux-x64-musl": "4.40.0", + "@rollup/rollup-win32-arm64-msvc": "4.40.0", + "@rollup/rollup-win32-ia32-msvc": "4.40.0", + "@rollup/rollup-win32-x64-msvc": "4.40.0", + "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/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-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.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "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==", + "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/tailwindcss/node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/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/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "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/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/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.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/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "5.4.18", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz", + "integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==", + "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/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "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/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "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/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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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..f5123e00480c80abbc92857c1c3238fd354c8e36 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,40 @@ +{ + "name": "health-assistant", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@headlessui/react": "^1.7.17", + "@heroicons/react": "^2.0.18", + "@tensorflow-models/mobilenet": "^2.1.1", + "@tensorflow/tfjs": "^4.11.0", + "axios": "^1.6.2", + "chart.js": "^4.4.0", + "lucide-react": "^0.525.0", + "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0", + "react-toastify": "^9.1.3", + "recharts": "^3.0.2" + }, + "devDependencies": { + "@types/react": "^18.2.37", + "@types/react-dom": "^18.2.15", + "@vitejs/plugin-react": "^4.2.0", + "autoprefixer": "^10.4.16", + "eslint": "^8.53.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.4", + "postcss": "^8.4.31", + "tailwindcss": "^3.3.5", + "vite": "^5.0.0" + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7f6ab33b6fe049c30175ed96d3573d074e19e417 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,9 @@ +import React from 'react'; +import AIFoodAnalyzer from './pages/AIFoodAnalyzer'; +import './index.css'; + +function App() { + return ; +} + +export default App; diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e3971e63613b978c24f02820c53289d0f7a5957f --- /dev/null +++ b/frontend/src/components/Navbar.jsx @@ -0,0 +1,130 @@ +import React from 'react'; +import { Fragment } from 'react'; +import { Link } from 'react-router-dom'; +import { Disclosure, Menu, Transition } from '@headlessui/react'; +import { Bars3Icon, XMarkIcon, UserCircleIcon } from '@heroicons/react/24/outline'; + +const navigation = [ + { name: '儀表板', href: '/', current: true }, + { name: '飲食追蹤', href: '/food', current: false }, + { name: 'AI食物分析', href: '/food-ai', current: false }, + { name: '飲水紀錄', href: '/water', current: false }, + { name: '運動紀錄', href: '/exercise', current: false }, +]; + +function classNames(...classes) { + return classes.filter(Boolean).join(' '); +} + +export default function Navbar() { + return ( + + {({ open }) => ( + <> +
+
+
+
+ 健康助手 +
+
+ {navigation.map((item) => ( + + {item.name} + + ))} +
+
+ +
+ +
+ + +
+ + + + {({ active }) => ( + + 個人資料 + + )} + + + {({ active }) => ( + + 登出 + + )} + + + +
+
+ +
+ + {open ? ( + +
+
+
+ + +
+ {navigation.map((item) => ( + + {item.name} + + ))} +
+
+ + )} +
+ ); +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000000000000000000000000000000000000..46880c61fa30fe6dadf82ccf705939609f9b9e29 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,27 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* 全局 reset 與字型 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +body { + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + justify-content: center; + align-items: center; + padding: 1rem; +} + +#root { + width: 100%; + display: flex; + justify-content: center; + align-items: center; +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000000000000000000000000000000000000..54b39dd1d900e866bb91ee441d372a8924b9d87a --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) diff --git a/frontend/src/pages/AIFoodAnalyzer.css b/frontend/src/pages/AIFoodAnalyzer.css new file mode 100644 index 0000000000000000000000000000000000000000..7764d2969d8faac99e1d5e0ae69b12fc2e4b00a7 --- /dev/null +++ b/frontend/src/pages/AIFoodAnalyzer.css @@ -0,0 +1,568 @@ +/* 全局樣式 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +/* 主體樣式 */ +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + padding: 1rem; +} + +.app-container { + background: rgba(255, 255, 255, 0.95); + border-radius: 20px; + padding: 2rem; + box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1); + max-width: 500px; + width: 100%; + max-height: 90vh; + overflow-y: auto; +} + +.header { + text-align: center; + margin-bottom: 2rem; +} + +.title { + color: #333; + font-size: 1.8rem; + font-weight: bold; + margin-bottom: 0.5rem; +} + +.subtitle { + color: #666; + font-size: 0.9rem; +} + +.tab-nav { + display: flex; + background: rgba(102, 126, 234, 0.1); + border-radius: 15px; + margin-bottom: 2rem; + padding: 0.3rem; +} + +.tab-btn { + flex: 1; + padding: 0.8rem; + border: none; + background: transparent; + border-radius: 12px; + font-weight: bold; + cursor: pointer; + transition: all 0.3s; + color: #666; +} + +.tab-btn.active { + background: linear-gradient(45deg, #667eea, #764ba2); + color: white; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); +} + +/* 個人資料表單 */ +.profile-form { + display: grid; + gap: 1.5rem; +} + +.input-group { + display: flex; + flex-direction: column; +} + +.input-group label { + font-weight: bold; + color: #333; + margin-bottom: 0.5rem; + font-size: 0.9rem; +} + +.input-group input, .input-group select { + padding: 12px; + border: 2px solid #e0e0e0; + border-radius: 10px; + font-size: 1rem; + transition: border-color 0.3s; +} + +.input-group input:focus, .input-group select:focus { + outline: none; + border-color: #667eea; +} + +.input-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.save-profile-btn { + background: linear-gradient(45deg, #43cea2, #185a9d); + color: white; + border: none; + padding: 15px; + border-radius: 15px; + font-weight: bold; + cursor: pointer; + font-size: 1rem; + transition: transform 0.2s; +} + +.save-profile-btn:hover { + transform: translateY(-2px); +} + +/* 食物分析區域 */ +.camera-container { + position: relative; + margin-bottom: 1.5rem; + text-align: center; +} + +.controls { + display: flex; + gap: 1rem; + justify-content: center; + margin: 1.5rem 0; + flex-wrap: wrap; +} + +button { + background: linear-gradient(45deg, #667eea, #764ba2); + color: white; + border: none; + padding: 12px 20px; + border-radius: 25px; + cursor: pointer; + font-weight: bold; + transition: transform 0.2s; + font-size: 0.9rem; +} + +button:hover { + transform: translateY(-2px); +} + +button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.upload-btn { + background: linear-gradient(45deg, #43cea2, #185a9d); +} + +.file-input { + display: none; +} + +.loading { + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + margin: 1rem 0; + text-align: center; +} + +.spinner { + border: 3px solid #f3f3f3; + border-top: 3px solid #667eea; + border-radius: 50%; + width: 30px; + height: 30px; + animation: spin 1s linear infinite; + margin: 0 auto 1rem; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.result { + margin-top: 2rem; + padding: 1.5rem; + background: rgba(102, 126, 234, 0.1); + border-radius: 15px; + display: none; +} + +.food-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.food-name { + font-size: 1.3rem; + font-weight: bold; + color: #333; +} + +.add-to-diary { + background: linear-gradient(45deg, #ff6b6b, #ee5a24); + padding: 8px 16px; + border-radius: 20px; + font-size: 0.8rem; +} + +.nutrition-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.8rem; + margin-bottom: 1.5rem; +} + +.nutrition-item { + background: white; + padding: 0.8rem; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + text-align: center; +} + +.nutrition-label { + font-size: 0.9rem; + color: #666; + margin-bottom: 0.3rem; +} + +.nutrition-value { + font-size: 1.1rem; + font-weight: bold; + color: #333; +} + +.daily-recommendation { + background: linear-gradient(45deg, rgba(255, 107, 107, 0.1), rgba(238, 90, 36, 0.1)); + padding: 1rem; + border-radius: 10px; + border-left: 4px solid #ff6b6b; + margin-top: 1.5rem; +} + +.recommendation-title { + font-weight: bold; + color: #333; + margin-bottom: 0.5rem; + font-size: 0.9rem; +} + +.recommendation-text { + font-size: 0.8rem; + color: #666; + line-height: 1.4; +} + +/* 健康指數樣式 */ +.health-indices { + display: flex; + gap: 1rem; + margin: 1rem 0 1.5rem; +} + +.health-index { + flex: 1; + background: white; + padding: 0.8rem; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.index-label { + font-size: 0.8rem; + color: #666; + margin-bottom: 0.5rem; +} + +.index-meter { + height: 6px; + background: #f0f0f0; + border-radius: 3px; + margin-bottom: 0.5rem; + overflow: hidden; +} + +.meter-fill { + height: 100%; + width: 0%; + border-radius: 3px; + transition: width 0.5s ease; +} + +.index-value { + font-size: 0.8rem; + font-weight: bold; + color: #333; + text-align: right; +} + +.food-description { + background: rgba(102, 126, 234, 0.1); + padding: 1rem; + border-radius: 10px; + font-size: 0.9rem; + color: #555; + line-height: 1.5; + margin-bottom: 1rem; +} + +.benefits-tags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1.5rem; +} + +.benefit-tag { + background: rgba(67, 206, 162, 0.1); + color: #43cea2; + padding: 0.3rem 0.8rem; + border-radius: 20px; + font-size: 0.8rem; + font-weight: 500; + display: inline-block; +} + +.nutrition-section-title { + color: #333; + font-size: 1rem; + margin: 1.5rem 0 0.8rem; +} + +.nutrition-details { + display: grid; + grid-template-columns: 1fr; + gap: 1rem; + margin-top: 1.5rem; +} + +.nutrition-section { + background: white; + padding: 1rem; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +/* 追蹤紀錄專用樣式 */ +.tracking-tab { + padding: 1.5rem; + max-width: 1200px; + margin: 0 auto; +} + +.overview-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; +} + +.overview-card { + background: white; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); + display: flex; + align-items: center; + transition: transform 0.2s, box-shadow 0.2s; +} + +.overview-card:hover { + transform: translateY(-3px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1); +} + +.overview-label { + font-size: 0.875rem; + color: #7f8c8d; + margin-bottom: 0.25rem; +} + +.overview-value { + font-size: 1.5rem; + font-weight: 700; + color: #2c3e50; + margin-bottom: 0.25rem; +} + +.overview-value span { + font-size: 1rem; + color: #7f8c8d; +} + +/* 進度條樣式 */ +.progress-section { + display: grid; + gap: 1.25rem; + margin-bottom: 2rem; +} + +.progress-row { + margin-bottom: 1rem; +} + +.progress-label { + font-weight: 600; + color: #2c3e50; + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.progress-bar-bg { + height: 8px; + background: #f0f2f5; + border-radius: 4px; + overflow: hidden; + margin-bottom: 0.5rem; +} + +.progress-bar-fill { + height: 100%; + border-radius: 4px; + transition: width 0.5s ease; +} + +.progress-value { + color: #7f8c8d; + font-size: 0.875rem; +} + +/* 食物記錄 */ +.food-diary-section { + background: white; + border-radius: 12px; + padding: 1.5rem; + margin-bottom: 1.5rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); +} + +.food-diary-title { + margin-bottom: 1.5rem; + font-size: 1.25rem; + color: #2c3e50; +} + +.no-diary { + text-align: center; + padding: 3rem 1rem; + color: #7f8c8d; +} + +.food-diary-list { + display: grid; + gap: 1rem; +} + +.food-diary-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + background: #f8f9fa; + border-radius: 8px; +} + +.food-diary-name { + font-weight: 600; + color: #2c3e50; +} + +.food-diary-time { + color: #7f8c8d; + font-size: 0.875rem; +} + +.food-diary-calories { + font-weight: 600; + color: #667eea; +} + +/* 圖表卡片 */ +.week-chart-section { + background: white; + border-radius: 12px; + padding: 1.5rem; + margin-bottom: 1.5rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); +} + +.week-chart-title { + margin-bottom: 1.5rem; + font-size: 1.25rem; + color: #2c3e50; +} + +.week-chart-wrapper { + height: 200px; + position: relative; +} + +/* 響應式設計 */ +@media (max-width: 768px) { + .app-container { + max-width: 100%; + margin: 0; + border-radius: 0; + max-height: 100vh; + } + + .input-row { + grid-template-columns: 1fr; + } + + .health-indices { + flex-direction: column; + } + + .nutrition-grid { + grid-template-columns: 1fr; + } + + .overview-cards { + grid-template-columns: 1fr; + } + + .controls { + flex-direction: column; + align-items: center; + } + + button { + width: 100%; + max-width: 200px; + } +} + +@media (max-width: 480px) { + .app-container { + padding: 1rem; + } + + .title { + font-size: 1.5rem; + } + + .tab-nav { + flex-direction: column; + gap: 0.5rem; + } + + .tab-btn { + border-radius: 8px; + } +} diff --git a/frontend/src/pages/AIFoodAnalyzer.jsx b/frontend/src/pages/AIFoodAnalyzer.jsx new file mode 100644 index 0000000000000000000000000000000000000000..48f5df0abf0aa777f4c91b475cef8138298d7afb --- /dev/null +++ b/frontend/src/pages/AIFoodAnalyzer.jsx @@ -0,0 +1,478 @@ +import React, { useState, useRef, useEffect } from 'react'; +import Chart from 'chart.js/auto'; +import './AIFoodAnalyzer.css'; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://127.0.0.1:8000'; + +const defaultProfile = { + name: '', age: '', gender: '', height: '', weight: '', + activityLevel: 'sedentary', healthGoal: 'maintain', + dailyCalories: 0, proteinGoal: 0, fiberGoal: 25, waterGoal: 2000 +}; + +function AIFoodAnalyzer() { + // 分頁狀態 + const [tab, setTab] = useState('analyzer'); + // 個人資料 + const [profile, setProfile] = useState(() => { + const stored = localStorage.getItem('userProfile'); + return stored ? JSON.parse(stored) : { ...defaultProfile }; + }); + // 食物日記 + const [foodDiary, setFoodDiary] = useState([]); + // 分析狀態 + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [result, setResult] = useState(null); + // 相機/上傳 + const videoRef = useRef(null); + const canvasRef = useRef(null); + const fileInputRef = useRef(null); + const [stream, setStream] = useState(null); + // Chart + const chartRef = useRef(null); + // 新增一個 state 來存放預覽圖片的 URL + const [previewImage, setPreviewImage] = useState(null); + + // 初始化日記 + useEffect(() => { + loadFoodDiary(); + // 清理相機 + return () => { if (stream) stream.getTracks().forEach(t => t.stop()); }; + }, []); + + // 分頁切換時載入日記 + useEffect(() => { + if (tab === 'tracking') loadFoodDiary(); + }, [tab]); + + // 分頁切換 + const switchTab = (t) => { setTab(t); setError(''); setResult(null); }; + + // 個人資料儲存 + const handleProfileChange = e => { + const { name, value } = e.target; + setProfile(p => ({ ...p, [name]: value })); + }; + const saveProfile = e => { + e.preventDefault(); + // 計算每日建議 + const bmr = profile.gender === 'male' + ? 88.362 + (13.397 * profile.weight) + (4.799 * profile.height) - (5.677 * profile.age) + : 447.593 + (9.247 * profile.weight) + (3.098 * profile.height) - (4.330 * profile.age); + const activityMultipliers = { sedentary: 1.2, light: 1.375, moderate: 1.55, active: 1.725, extra: 1.9 }; + let calories = bmr * (activityMultipliers[profile.activityLevel] || 1.2); + const goalAdjustments = { lose: -300, gain: 300, muscle: 200 }; + calories += goalAdjustments[profile.healthGoal] || 0; + const dailyCalories = Math.round(calories); + const proteinGoal = Math.round(dailyCalories * 0.25 / 4); + setProfile(p => { + const newP = { ...p, dailyCalories, proteinGoal, fiberGoal: 25, waterGoal: 2000 }; + localStorage.setItem('userProfile', JSON.stringify(newP)); + return newP; + }); + alert('個人資料已儲存!'); + setTab('analyzer'); + }; + + // 日記 + function loadFoodDiary() { + const today = new Date().toISOString().slice(0, 10); + const stored = localStorage.getItem(`foodDiary_${today}`); + setFoodDiary(stored ? JSON.parse(stored) : []); + } + function saveFoodDiary(diary) { + const today = new Date().toISOString().slice(0, 10); + localStorage.setItem(`foodDiary_${today}`, JSON.stringify(diary)); + setFoodDiary(diary); + } + function addToFoodDiary() { + if (!result) return alert('沒有可加入的分析結果。'); + const meal = { ...result, id: Date.now(), timestamp: new Date().toISOString() }; + const newDiary = [...foodDiary, meal]; + saveFoodDiary(newDiary); + alert(`${meal.foodName} 已加入記錄!`); + } + + // 相機/圖片分析 + const startCamera = async () => { + setError(''); + try { + const mediaStream = await navigator.mediaDevices.getUserMedia({ video: true }); + if (videoRef.current) { + videoRef.current.srcObject = mediaStream; + setStream(mediaStream); + } + } catch (err) { + setError('無法開啟相機,請檢查權限。'); + } + }; + const capturePhoto = () => { + setError(''); + const video = videoRef.current; + const canvas = canvasRef.current; + if (!video || !canvas) return; + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + const ctx = canvas.getContext('2d'); + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + canvas.toBlob(blob => { + if (blob) { + setPreviewImage(URL.createObjectURL(blob)); + processImage(blob); + } + else setError('無法擷取圖片,請再試一次'); + }, 'image/jpeg'); + }; + const handleFileUpload = (e) => { + const file = e.target.files[0]; + if (file) { + setPreviewImage(URL.createObjectURL(file)); + processImage(file); + e.target.value = ''; + } + }; + const processImage = async (imageSource) => { + setLoading(true); + setResult(null); + setError(''); + const formData = new FormData(); + formData.append('file', imageSource); + try { + const aiResponse = await fetch(`${API_BASE_URL}/ai/analyze-food-image-with-weight/`, { + method: 'POST', body: formData, + }); + if (!aiResponse.ok) { + const errorData = await aiResponse.json(); + throw new Error(errorData.detail || `AI辨識失敗 (狀態碼: ${aiResponse.status})`); + } + const aiData = await aiResponse.json(); + const foodName = aiData.food_type; + if (!foodName || foodName === 'Unknown') throw new Error('AI無法辨識出食物名稱。'); + let analysis = { + foodName, + description: `AI 辨識結果:${foodName}` + (aiData.note ? `(${aiData.note})` : ''), + healthIndex: 75, + glycemicIndex: 50, + benefits: [`含有 ${foodName} 的營養成分`], + nutrition: aiData.nutrition || { calories: 150, protein: 8, carbs: 20, fat: 5, fiber: 3, sugar: 2 }, + vitamins: { 'Vitamin C': 15, 'Vitamin A': 10 }, + minerals: { 'Iron': 2, 'Calcium': 50 }, + estimatedWeight: aiData.estimated_weight, + weightConfidence: aiData.weight_confidence, + weightErrorRange: aiData.weight_error_range, + referenceObject: aiData.reference_object, + note: aiData.note + }; + setResult(analysis); + } catch (err) { + setError(`分析失敗: ${err.message}`); + } finally { + setLoading(false); + } + }; + + // Chart.js 本週熱量趨勢 + useEffect(() => { + if (tab !== 'tracking' || !chartRef.current) return; + if (chartRef.current._chart) chartRef.current._chart.destroy(); + // 取得本週資料 + const days = ['日','一','二','三','四','五','六']; + const week = []; + const now = new Date(); + for (let i = 6; i >= 0; i--) { + const d = new Date(now); + d.setDate(now.getDate() - i); + const key = d.toISOString().slice(0,10); + const diary = JSON.parse(localStorage.getItem(`foodDiary_${key}`) || '[]'); + const total = diary.reduce((sum, m) => sum + (m.nutrition?.calories||0), 0); + week.push({ day: days[d.getDay()], calories: total }); + } + chartRef.current._chart = new Chart(chartRef.current.getContext('2d'), { + type: 'line', + data: { + labels: week.map(d=>d.day), + datasets: [{ + label: '熱量', + data: week.map(d=>d.calories), + borderColor: '#667eea', + backgroundColor: 'rgba(102, 126, 234, 0.1)', + tension: 0.3, pointRadius: 4, fill: true + }] + }, + options: { + plugins: { legend: { display: false } }, + scales: { y: { beginAtZero: true, ticks: { stepSize: 200 } } }, + responsive: true, maintainAspectRatio: false + } + }); + }, [tab, foodDiary]); + + // UI + return ( +
+
+

🍎 AI食物營養分析器

+

智能分析您的飲食營養

+
+ +
+ + + +
+ + {tab === 'analyzer' && ( +
+
+ {/* 預覽圖片優先顯示,否則顯示相機畫面 */} + {previewImage ? ( + 預覽圖片 + ) : ( +
+ +
+ + + + +
+ + {loading && ( +
+
+

AI正在分析食物中...

+
+ )} + + {error &&
{error}
} + + {result && ( +
+
+

{result.foodName}

+ +
+ {/* 顯示重量、信心度、誤差範圍 */} +
+
預估重量:{result.estimatedWeight ? `${result.estimatedWeight} g` : '--'}
+
信心度:{result.weightConfidence ? `${Math.round(result.weightConfidence*100)}%` : '--'}
+
誤差範圍:{result.weightErrorRange ? `${result.weightErrorRange[0]} ~ ${result.weightErrorRange[1]} g` : '--'}
+ {result.referenceObject &&
參考物:{result.referenceObject}
} + {result.note &&
{result.note}
} +
+ +
+
+
健康指數
+
+
+
+
{result.healthIndex||75}/100
+
+
+
升糖指數
+
+
+
+
{result.glycemicIndex||50}/100
+
+
+ +

{result.description || `這是 ${result.foodName}`}

+ +
+ {(result.benefits||[`${result.foodName} 的營養價值`]).map((b,i)=>( + {b} + ))} +
+ +
+
+

基本營養素

+
+ {Object.entries(result.nutrition||{}).map(([k,v])=>( +
+
{k}
+
{v}
+
+ ))} +
+
+ +
+

維生素

+
+ {result.vitamins && Object.entries(result.vitamins).map(([k,v])=>( +
+
{k}
+
{v} mg
+
+ ))} +
+
+ +
+

礦物質

+
+ {result.minerals && Object.entries(result.minerals).map(([k,v])=>( +
+
{k}
+
{v} mg
+
+ ))} +
+
+
+ + {profile && ( +
+
💡 個人化建議
+
+ 這份食物約佔您每日熱量建議的 {Math.round((result.nutrition?.calories / profile.dailyCalories) * 100)}%。 +
+
+ )} +
+ )} +
+ )} + + {tab === 'profile' && ( +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ + + + {profile && ( +
+

您的每日建議:

+
    +
  • 熱量:{profile.dailyCalories} 卡
  • +
  • 蛋白質:{profile.proteinGoal} g
  • +
  • 纖維:{profile.fiberGoal} g
  • +
  • 水分:{profile.waterGoal} ml
  • +
+
+ )} +
+ )} + + {tab === 'tracking' && ( +
+
+
+
今日攝取熱量
+
{Math.round(foodDiary.reduce((t,m)=>t+(m.nutrition?.calories||0),0))}
+
+
+
蛋白質
+
{Math.round(foodDiary.reduce((t,m)=>t+(m.nutrition?.protein||0),0))} g
+
+
+
纖維
+
{Math.round(foodDiary.reduce((t,m)=>t+(m.nutrition?.fiber||0),0))} g
+
+
+ +
+ {[{label:'熱量',current:foodDiary.reduce((t,m)=>t+(m.nutrition?.calories||0),0),goal:profile.dailyCalories,unit:'卡',color:'#43cea2'}, + {label:'蛋白質',current:foodDiary.reduce((t,m)=>t+(m.nutrition?.protein||0),0),goal:profile.proteinGoal,unit:'g',color:'#185a9d'}, + {label:'纖維',current:foodDiary.reduce((t,m)=>t+(m.nutrition?.fiber||0),0),goal:profile.fiberGoal,unit:'g',color:'#ff9a9e'}].map(({label,current,goal,unit,color})=>{ + const percent = goal ? Math.min(100, (current/goal)*100) : 0; + return ( +
+
{label}
+
+
+
+
{Math.round(current)} / {goal} {unit}
+
+ ); + })} +
+ +
+
今日食物記錄
+ {foodDiary.length === 0 ? ( +
今天尚未記錄任何食物 🍽️
+ ) : ( +
+ {foodDiary.map(meal => ( +
+
{meal.foodName}
+
{new Date(meal.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
+
{Math.round(meal.nutrition?.calories||0)} 卡
+
+ ))} +
+ )} +
+ +
+
本週熱量趨勢
+
+ +
+
+
+ )} +
+ ); +} + +export default AIFoodAnalyzer; diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx new file mode 100644 index 0000000000000000000000000000000000000000..33a4b651c22e0bfaf0d50a4aaa15703c1c36e76b --- /dev/null +++ b/frontend/src/pages/Dashboard.jsx @@ -0,0 +1,177 @@ +import React, { useState } from 'react'; +import { UserIcon, CalculatorIcon, CalendarIcon, CloudArrowUpIcon, ClipboardDocumentListIcon, ArrowPathIcon } from '@heroicons/react/24/outline'; + +export default function Dashboard() { + // 狀態區 + const [user, setUser] = useState({ name: '', age: '', height: '', weight: '', id: '' }); + const [userCreated, setUserCreated] = useState(false); + const [foodDate, setFoodDate] = useState(''); + const [foodCal, setFoodCal] = useState(''); + const [foodCarb, setFoodCarb] = useState(''); + const [foodProtein, setFoodProtein] = useState(''); + const [foodMsg, setFoodMsg] = useState(''); + const [bmi, setBmi] = useState(null); + const [bmiMsg, setBmiMsg] = useState(''); + const [waterDate, setWaterDate] = useState(''); + const [water, setWater] = useState(''); + const [waterMsg, setWaterMsg] = useState(''); + const [aiResult, setAiResult] = useState(''); + const [history, setHistory] = useState([]); + + // 處理邏輯 + const handleCreateUser = (e) => { e.preventDefault(); setUserCreated(true); setUser({ ...user, id: '6' }); }; + const handleAddFood = (e) => { e.preventDefault(); setFoodMsg('Food log added successfully.'); setHistory([...history, { date: foodDate, cal: foodCal, carb: foodCarb, protein: foodProtein }]); }; + const handleCalcBmi = () => { + if (user.height && user.weight) { + const bmiVal = (user.weight / ((user.height / 100) ** 2)).toFixed(2); + setBmi(bmiVal); + let msg = ''; + if (bmiVal < 18.5) msg = '體重過輕,建議均衡飲食與適度運動'; + else if (bmiVal < 24) msg = '正常範圍,請繼續保持'; + else msg = '體重過重,建議增加運動與飲食控制'; + setBmiMsg(msg); + } + }; + const handleAddWater = (e) => { e.preventDefault(); let msg = ''; if (water >= 2000) msg = '今日補水充足!'; else msg = '今日飲水量不足,請多補充水分!'; setWaterMsg(msg); }; + const handleAi = () => { setAiResult('AI 分析結果:雞肉沙拉,約 350 kcal'); }; + const handleQueryHistory = () => { setHistory(history); }; + + return ( +
+
+ {/* 上方切換選單間隔 */} +
+ 用戶 + 飲食 + BMI + 水分 + AI + 歷史 +
+
+ {/* 用戶卡片 */} +
+
+ + 用戶資料 +
+
+ setUser({ ...user, name: e.target.value })} /> + setUser({ ...user, age: e.target.value })} /> + setUser({ ...user, height: e.target.value })} /> + setUser({ ...user, weight: e.target.value })} /> + +
+ {userCreated && ( + <> +
用戶ID:6
+ +
+
姓名:{user.name}
+
年齡:{user.age}
+
身高:{user.height} cm
+
體重:{user.weight} kg
+
+ + )} +
+ {/* 飲食紀錄卡片 */} +
+
+ + 飲食紀錄 +
+
+ setFoodDate(e.target.value)} /> + setFoodCal(e.target.value)} /> + setFoodCarb(e.target.value)} /> + setFoodProtein(e.target.value)} /> + +
+ {foodMsg &&
{foodMsg}
} +
+ {/* BMI 卡片 */} +
+
+ + BMI 與建議 +
+ + {bmi &&
BMI:{bmi}
} + {bmiMsg &&
{bmiMsg}
} +
+ {/* 水分卡片 */} +
+
+ + 水分攝取 +
+
+ setWaterDate(e.target.value)} /> + setWater(e.target.value)} /> + +
+ {waterMsg &&
{waterMsg}
} +
+ {/* AI 卡片 */} +
+
+ + AI 食物辨識 +
+ + + {aiResult &&
{aiResult}
} +
+ {/* 歷史紀錄卡片 */} +
+
+ + 歷史紀錄查詢 +
+ +
+ + + + + + + + + + + {history.map((h, i) => ( + + + + + + + ))} + +
日期熱量碳水蛋白質
{h.date}{h.cal}{h.carb}{h.protein}
+
+ {userCreated && ( +
+
用戶資料:
+
姓名:{user.name}
+
年齡:{user.age}
+
身高:{user.height} cm
+
體重:{user.weight} kg
+
用戶固定測試ID:6
+
+ )} +
+
+
+ {/* 簡單樣式 */} + +
+ ); +} diff --git a/frontend/src/pages/ExerciseTracker.jsx b/frontend/src/pages/ExerciseTracker.jsx new file mode 100644 index 0000000000000000000000000000000000000000..53cde54d3d8f84865b49e110bf53570d23a3ea0d --- /dev/null +++ b/frontend/src/pages/ExerciseTracker.jsx @@ -0,0 +1,138 @@ +import React from 'react'; +import { useState } from 'react'; +import { PlusIcon } from '@heroicons/react/24/outline'; + +const exerciseTypes = [ + { id: 'walking', name: '步行', caloriesPerMinute: 4 }, + { id: 'running', name: '跑步', caloriesPerMinute: 10 }, + { id: 'cycling', name: '騎自行車', caloriesPerMinute: 7 }, + { id: 'swimming', name: '游泳', caloriesPerMinute: 8 }, + { id: 'yoga', name: '瑜伽', caloriesPerMinute: 3 }, + { id: 'weightlifting', name: '重訓', caloriesPerMinute: 6 }, +]; + +export default function ExerciseTracker() { + const [selectedExercise, setSelectedExercise] = useState(''); + const [duration, setDuration] = useState(''); + + const calculateCalories = () => { + const exercise = exerciseTypes.find(e => e.id === selectedExercise); + if (exercise && duration) { + return exercise.caloriesPerMinute * parseInt(duration); + } + return 0; + }; + + return ( +
+
+

記錄運動

+ +
+
+
+ + +
+ +
+ + setDuration(e.target.value)} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" + placeholder="0" + min="0" + /> +
+ + {selectedExercise && duration && ( +
+

預計消耗卡路里: {calculateCalories()} kcal

+
+ )} + + +
+ +
+

本週運動統計

+
+
+
+ 總運動時間 + 180 分鐘 +
+
+
+
+
+
+
+ 消耗卡路里 + 1,200 kcal +
+
+
+
+
+
+
+
+
+ + {/* 運動記錄 */} +
+

最近的運動記錄

+
+ + + + + + + + + + + + + + + + + + + + + + + +
日期運動類型時間 (分鐘)消耗卡路里
2025-04-21跑步30300
2025-04-20重訓45270
+
+
+
+ ); +} diff --git a/frontend/src/pages/FoodTracker.jsx b/frontend/src/pages/FoodTracker.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9cf69dcb2ab935d47041c5e9fee911f78d30ef09 --- /dev/null +++ b/frontend/src/pages/FoodTracker.jsx @@ -0,0 +1,866 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { XMarkIcon, InboxArrowDownIcon, CalendarIcon, ClockIcon, ChartBarIcon, CameraIcon, PhotoIcon } from '@heroicons/react/24/outline'; +import axios from 'axios'; +import Chart from 'chart.js/auto'; + +export default function FoodTracker() { + // 狀態管理 + const [activeTab, setActiveTab] = useState('analyzer'); + const [selectedImage, setSelectedImage] = useState(null); + const [isAnalyzing, setIsAnalyzing] = useState(false); + const [analysisResults, setAnalysisResults] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + // 個人資料狀態 + const [userProfile, setUserProfile] = useState({ + name: '', + age: '', + gender: '', + weight: '', + height: '', + activityLevel: 'moderate', + goal: 'maintain', + dailyCalories: 2000, + proteinGoal: 150, + fiberGoal: 25 + }); + + // 食物記錄狀態 + const [foodDiary, setFoodDiary] = useState([]); + const [nutritionSummary, setNutritionSummary] = useState(null); + const [recentMeals, setRecentMeals] = useState([]); + + // 相機相關 + const videoRef = useRef(null); + const canvasRef = useRef(null); + const chartRef = useRef(null); + const chartInstance = useRef(null); + + // 初始化 + useEffect(() => { + loadStoredData(); + if (activeTab === 'tracking') { + updateTrackingPage(); + } + }, [activeTab]); + + useEffect(() => { + if (activeTab === 'tracking' && chartRef.current) { + renderWeeklyChart(); + } + }, [activeTab, foodDiary]); + + // 載入儲存的資料 + const loadStoredData = () => { + try { + const storedProfile = localStorage.getItem('userProfile'); + const storedDiary = localStorage.getItem('foodDiary'); + + if (storedProfile) { + setUserProfile(JSON.parse(storedProfile)); + } + + if (storedDiary) { + setFoodDiary(JSON.parse(storedDiary)); + } + } catch (error) { + console.error('Error loading stored data:', error); + } + }; + + // 儲存資料 + const saveData = () => { + localStorage.setItem('userProfile', JSON.stringify(userProfile)); + localStorage.setItem('foodDiary', JSON.stringify(foodDiary)); + }; + + useEffect(() => { + saveData(); + }, [userProfile, foodDiary]); + + // 相機功能 + const startCamera = async () => { + try { + if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { + const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + if (videoRef.current) { + videoRef.current.srcObject = stream; + } + } + } catch (error) { + console.error('Error accessing camera:', error); + alert('無法存取相機,請檢查權限設定'); + } + }; + + const capturePhoto = () => { + if (videoRef.current && canvasRef.current) { + const context = canvasRef.current.getContext('2d'); + canvasRef.current.width = videoRef.current.videoWidth; + canvasRef.current.height = videoRef.current.videoHeight; + context.drawImage(videoRef.current, 0, 0, canvasRef.current.width, canvasRef.current.height); + + const imageData = canvasRef.current.toDataURL('image/png'); + setSelectedImage(imageData); + analyzeImage(imageData); + } + }; + + // 檔案上傳處理 + const handleDrop = async (event) => { + event.preventDefault(); + setIsDragging(false); + if (event.dataTransfer.files && event.dataTransfer.files.length > 0) { + await handleImageUpload({ target: { files: event.dataTransfer.files } }); + } + }; + + const handleDragOver = (event) => { + event.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = (event) => { + event.preventDefault(); + setIsDragging(false); + }; + + const handleImageUpload = async (event) => { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setSelectedImage(reader.result); + }; + reader.readAsDataURL(file); + analyzeImage(reader.result); + } + }; + + // 圖片分析 + const analyzeImage = async (imageData) => { + setIsAnalyzing(true); + try { + // 將 base64 圖片數據轉換為 Blob + const base64Response = await fetch(imageData); + const blob = await base64Response.blob(); + + // 創建 FormData 並添加圖片 + const formData = new FormData(); + formData.append('file', blob, 'food-image.jpg'); + + // 發送到後端 API + const response = await fetch('http://localhost:8000/api/ai/analyze-food-image/', { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + throw new Error('分析請求失敗'); + } + + const data = await response.json(); + + // 模擬詳細的分析結果 + const mockAnalysis = { + foodName: data.food_name, + description: `這是 ${data.food_name},富含營養價值`, + healthIndex: 75, + glycemicIndex: 50, + benefits: ['富含維生素', '低熱量', '高纖維'], + nutrition: { + calories: 150, + protein: 8, + carbs: 20, + fat: 5, + fiber: 3, + sugar: 2 + }, + vitamins: { + '維生素C': 25, + '維生素A': 15 + }, + minerals: { + '鈣': 50, + '鐵': 2 + } + }; + + setAnalysisResults(mockAnalysis); + + } catch (error) { + console.error('Error analyzing image:', error); + alert('圖片分析失敗,請稍後再試'); + } finally { + setIsAnalyzing(false); + } + }; + + // 個人資料處理 + const handleProfileChange = (field, value) => { + setUserProfile(prev => ({ + ...prev, + [field]: value + })); + }; + + const saveProfile = () => { + if (!userProfile.name || !userProfile.age || !userProfile.gender || !userProfile.height || !userProfile.weight) { + alert('請填寫所有必填欄位!'); + return; + } + + // 計算基礎代謝率和每日熱量需求 + const bmr = calculateBMR(userProfile); + const dailyCalories = calculateDailyCalories(userProfile); + + const updatedProfile = { + ...userProfile, + bmr, + dailyCalories, + bmi: (userProfile.weight / Math.pow(userProfile.height / 100, 2)).toFixed(1) + }; + + setUserProfile(updatedProfile); + alert('個人資料已儲存成功!✅'); + setActiveTab('analyzer'); + }; + + const calculateBMR = (profile) => { + let bmr; + if (profile.gender === 'male') { + bmr = 88.362 + (13.397 * profile.weight) + (4.799 * profile.height) - (5.677 * profile.age); + } else { + bmr = 447.593 + (9.247 * profile.weight) + (3.098 * profile.height) - (4.330 * profile.age); + } + return Math.round(bmr); + }; + + const calculateDailyCalories = (profile) => { + const activityMultipliers = { + sedentary: 1.2, + light: 1.375, + moderate: 1.55, + active: 1.725, + extra: 1.9 + }; + + let calories = profile.bmr * activityMultipliers[profile.activityLevel]; + + if (profile.goal === 'lose') { + calories -= 300; + } else if (profile.goal === 'gain') { + calories += 300; + } + + return Math.round(calories); + }; + + // 食物記錄功能 + const addToFoodDiary = () => { + if (!analysisResults) { + alert('沒有可加入的分析結果。'); + return; + } + + const meal = { + ...analysisResults, + id: Date.now(), + timestamp: new Date().toISOString() + }; + + setFoodDiary(prev => [...prev, meal]); + alert(`${meal.foodName} 已加入記錄!`); + setAnalysisResults(null); + setSelectedImage(null); + }; + + // 追蹤頁面更新 + const updateTrackingPage = () => { + if (!userProfile.name) return; + + const todayTotals = foodDiary.reduce((totals, meal) => { + totals.calories += meal.nutrition.calories || 0; + totals.protein += meal.nutrition.protein || 0; + totals.fiber += meal.nutrition.fiber || 0; + return totals; + }, { calories: 0, protein: 0, fiber: 0 }); + + setNutritionSummary(todayTotals); + }; + + // 圖表渲染 + const renderWeeklyChart = () => { + if (chartInstance.current) { + chartInstance.current.destroy(); + } + + const ctx = chartRef.current.getContext('2d'); + + // 模擬一週的數據 + const days = ['週一', '週二', '週三', '週四', '週五', '週六', '週日']; + const caloriesData = Array(7).fill(0).map(() => Math.floor(Math.random() * 1000 + 1000)); + + // 今天的數據 + const today = new Date().getDay() || 7; + caloriesData[today - 1] = foodDiary.reduce((total, meal) => total + (meal.nutrition.calories || 0), 0) || + Math.floor(Math.random() * 1000 + 1000); + + chartInstance.current = new Chart(ctx, { + type: 'line', + data: { + labels: days, + datasets: [ + { + label: '熱量 (卡)', + data: caloriesData, + borderColor: 'rgb(255, 99, 132)', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + tension: 0.3 + } + ] + }, + options: { + responsive: true, + plugins: { + legend: { + position: 'top', + } + }, + scales: { + y: { + beginAtZero: true + } + } + } + }); + }; + + // 通知功能 + const showNotification = (message, type = 'info') => { + const notification = document.createElement('div'); + notification.className = `notification ${type}`; + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 12px 20px; + margin-bottom: 10px; + border-radius: 4px; + color: white; + opacity: 0; + transition: opacity 0.3s ease-in-out; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + z-index: 1000; + background-color: ${type === 'success' ? '#4CAF50' : type === 'error' ? '#F44336' : type === 'warning' ? '#FF9800' : '#2196F3'}; + `; + notification.textContent = message; + + document.body.appendChild(notification); + + setTimeout(() => { + notification.style.opacity = '1'; + }, 10); + + setTimeout(() => { + notification.style.opacity = '0'; + setTimeout(() => { + notification.remove(); + }, 300); + }, 3000); + }; + + return ( +
+
+ {/* 標題 */} +
+

🍎 AI食物營養分析器

+

智能分析您的飲食營養

+
+ + {/* 標籤導航 */} +
+
+ + + +
+
+ + {/* 內容區域 */} +
+ {/* 食物分析頁面 */} + {activeTab === 'analyzer' && ( +
+ {!userProfile.name && ( +
+

+ 請先到「個人資料」頁面完成設定,以獲得個人化營養建議和追蹤功能 👆 +

+
+ )} + +
+ {/* 相機區域 */} +
+
+
+ + {/* 控制按鈕 */} +
+ + + +
+
+ + {/* 載入中 */} + {isAnalyzing && ( +
+
+

AI正在分析食物...

+
+ )} + + {/* 分析結果 */} + {analysisResults && !isAnalyzing && ( +
+
+

{analysisResults.foodName}

+ +
+ +

{analysisResults.description}

+ + {/* 健康指數 */} +
+
+
+ 健康指數 + {analysisResults.healthIndex}/100 +
+
+
+
+
+
+
+ 升糖指數 + {analysisResults.glycemicIndex}/100 +
+
+
+
+
+
+ + {/* 營養益處 */} +
+

營養益處

+
+ {analysisResults.benefits.map((benefit, index) => ( + + {benefit} + + ))} +
+
+ + {/* 營養資訊 */} +
+

營養資訊 (每100g)

+
+ {Object.entries(analysisResults.nutrition).map(([key, value]) => ( +
+
+ {key === 'calories' ? '熱量' : + key === 'protein' ? '蛋白質' : + key === 'carbs' ? '碳水化合物' : + key === 'fat' ? '脂肪' : + key === 'fiber' ? '纖維' : '糖分'} +
+
+ {value} {key === 'calories' ? '卡' : 'g'} +
+
+ ))} +
+
+ + {/* 維生素和礦物質 */} + {(analysisResults.vitamins || analysisResults.minerals) && ( +
+ {analysisResults.vitamins && ( +
+

維生素

+
+ {Object.entries(analysisResults.vitamins).map(([key, value]) => ( +
+
{key}
+
{value} mg
+
+ ))} +
+
+ )} + {analysisResults.minerals && ( +
+

礦物質

+
+ {Object.entries(analysisResults.minerals).map(([key, value]) => ( +
+
{key}
+
{value} mg
+
+ ))} +
+
+ )} +
+ )} + + {/* 個人化建議 */} + {userProfile.name && ( +
+
💡 個人化建議
+
+ 這份食物約佔您每日熱量建議的 {Math.round((analysisResults.nutrition.calories / userProfile.dailyCalories) * 100)}%。 +
+
+ )} +
+ )} +
+
+ )} + + {/* 個人資料頁面 */} + {activeTab === 'profile' && ( +
+
+

個人資料設定

+ +
+
+ + handleProfileChange('name', e.target.value)} + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="請輸入您的姓名" + /> +
+ +
+
+ + handleProfileChange('age', e.target.value)} + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="歲" + /> +
+
+ + +
+
+ +
+
+ + handleProfileChange('height', e.target.value)} + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="公分" + /> +
+
+ + handleProfileChange('weight', e.target.value)} + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="公斤" + /> +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ )} + + {/* 營養追蹤頁面 */} + {activeTab === 'tracking' && ( +
+ {!userProfile.name ? ( +
+
📊
+

請先完成個人資料設定

+

開始追蹤您的營養攝取 👆

+
+ ) : ( +
+ {/* 歡迎訊息 */} +
+

+ 👋 你好, {userProfile.name}! +

+

這是您今天的營養總覽。

+
+ + {/* 統計卡片 */} +
+
+
+ {nutritionSummary?.calories || 0} +
+
今日熱量
+
+
+
+ {nutritionSummary?.protein || 0}g +
+
蛋白質
+
+
+
+ {nutritionSummary?.fiber || 0}g +
+
纖維
+
+
+
+ {userProfile.dailyCalories} +
+
目標熱量
+
+
+ + {/* 目標進度 */} +
+

目標進度

+
+ {[ + { label: '熱量', current: nutritionSummary?.calories || 0, goal: userProfile.dailyCalories, color: 'from-red-400 to-red-600' }, + { label: '蛋白質', current: nutritionSummary?.protein || 0, goal: userProfile.proteinGoal, color: 'from-blue-400 to-blue-600' }, + { label: '纖維', current: nutritionSummary?.fiber || 0, goal: userProfile.fiberGoal, color: 'from-green-400 to-green-600' } + ].map((item, index) => { + const progress = item.goal > 0 ? Math.min(100, (item.current / item.goal) * 100) : 0; + return ( +
+
+ {item.label} + + {item.current} / {item.goal} {item.label === '熱量' ? '卡' : 'g'} + +
+
+
+
+
+ ); + })} +
+
+ + {/* 本週趨勢圖 */} +
+

本週營養攝取趨勢

+
+ +
+
+ + {/* 今日飲食記錄 */} +
+
+

今日飲食記錄

+ + {foodDiary.length} 項記錄 + +
+ + {foodDiary.length === 0 ? ( +
+
🍽️
+

今天尚未記錄任何食物

+
+ ) : ( +
+ {foodDiary.map((meal) => ( +
+
+

{meal.foodName}

+ + {new Date(meal.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + +
+
+ {Math.round(meal.nutrition.calories)} 卡 +
+
+ ))} +
+ )} +
+
+ )} +
+ )} +
+
+
+ ); +} diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx new file mode 100644 index 0000000000000000000000000000000000000000..703fe74f0168964cf7f5f199ce58e47b6804e86a --- /dev/null +++ b/frontend/src/pages/Login.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +export default function Login() { + return ( +
+

登入

+
+
+ + +
+
+ + +
+ +
+
+ ); +} diff --git a/frontend/src/pages/Profile.jsx b/frontend/src/pages/Profile.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9e1f9ebd1c453f4ace6172135a7d05626a78cb9b --- /dev/null +++ b/frontend/src/pages/Profile.jsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export default function Profile() { + return ( +
+

個人資料

+

這裡可以顯示和編輯您的個人資訊。

+
+ ); +} diff --git a/frontend/src/pages/Register.jsx b/frontend/src/pages/Register.jsx new file mode 100644 index 0000000000000000000000000000000000000000..4273ec32768a8fb50217180f8d7f1f6cc5e17857 --- /dev/null +++ b/frontend/src/pages/Register.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +export default function Register() { + return ( +
+

註冊

+
+
+ + +
+
+ + +
+ +
+
+ ); +} diff --git a/frontend/src/pages/WaterTracker.jsx b/frontend/src/pages/WaterTracker.jsx new file mode 100644 index 0000000000000000000000000000000000000000..fc67728beca250573308636fb637680f92a3371b --- /dev/null +++ b/frontend/src/pages/WaterTracker.jsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { useState } from 'react'; +import { PlusIcon, MinusIcon } from '@heroicons/react/24/outline'; + +export default function WaterTracker() { + const [waterIntake, setWaterIntake] = useState(0); + const goal = 2000; // 每日目標 (ml) + + const addWater = (amount) => { + setWaterIntake(prev => Math.min(prev + amount, 5000)); + }; + + const removeWater = (amount) => { + setWaterIntake(prev => Math.max(prev - amount, 0)); + }; + + const progress = (waterIntake / goal) * 100; + + return ( +
+
+

今日飲水追蹤

+ + {/* 進度顯示 */} +
+
+ 目前進度 + {waterIntake} / {goal} ml +
+
+
+
+
+ + {/* 快速添加按鈕 */} +
+ + + + +
+ + {/* 自定義輸入 */} +
+
+ + +
+ +
+
+ + {/* 今日記錄 */} +
+

今日記錄

+
+ + + + + + + + + + + + + + + + + + + + + +
時間數量 (ml)
14:30300
12:00500
09:15200
+
+
+
+ ); +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000000000000000000000000000000000000..dca8ba02d37dfd402f17d6004160b24574432251 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/health-assistant b/health-assistant new file mode 160000 index 0000000000000000000000000000000000000000..f09f40f2e528e117c4c0b66fca5629330632643b --- /dev/null +++ b/health-assistant @@ -0,0 +1 @@ +Subproject commit f09f40f2e528e117c4c0b66fca5629330632643b diff --git a/health_assistant.db b/health_assistant.db new file mode 100644 index 0000000000000000000000000000000000000000..290a68ae798a548336cf26e46aa3fc829352f084 Binary files /dev/null and b/health_assistant.db differ diff --git a/health_assistant/HEAD b/health_assistant/HEAD new file mode 100644 index 0000000000000000000000000000000000000000..b870d82622c1a9ca6bcaf5df639680424a1904b0 --- /dev/null +++ b/health_assistant/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/health_assistant/config b/health_assistant/config new file mode 100644 index 0000000000000000000000000000000000000000..f32463ec7604f24e9a0e416ef7d8940637cc3dbc --- /dev/null +++ b/health_assistant/config @@ -0,0 +1,13 @@ +[core] + repositoryformatversion = 0 + filemode = false + bare = false + logallrefupdates = true + symlinks = false + ignorecase = true +[remote "origin"] + url = https://huggingface.co/spaces/yuting111222/health-assistant + fetch = +refs/heads/*:refs/remotes/origin/* +[branch "main"] + remote = origin + merge = refs/heads/main diff --git a/health_assistant/description b/health_assistant/description new file mode 100644 index 0000000000000000000000000000000000000000..498b267a8c7812490d6479839c5577eaaec79d62 --- /dev/null +++ b/health_assistant/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/health_assistant/hooks/applypatch-msg.sample b/health_assistant/hooks/applypatch-msg.sample new file mode 100644 index 0000000000000000000000000000000000000000..a5d7b84a673458d14d9aab082183a1968c2c7492 --- /dev/null +++ b/health_assistant/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/health_assistant/hooks/commit-msg.sample b/health_assistant/hooks/commit-msg.sample new file mode 100644 index 0000000000000000000000000000000000000000..b58d1184a9d43a39c0d95f32453efc78581877d6 --- /dev/null +++ b/health_assistant/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/health_assistant/hooks/fsmonitor-watchman.sample b/health_assistant/hooks/fsmonitor-watchman.sample new file mode 100644 index 0000000000000000000000000000000000000000..23e856f5deeb7f564afc22f2beed54449c2d3afb --- /dev/null +++ b/health_assistant/hooks/fsmonitor-watchman.sample @@ -0,0 +1,174 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; + +# An example hook script to integrate Watchman +# (https://facebook.github.io/watchman/) with git to speed up detecting +# new and modified files. +# +# The hook is passed a version (currently 2) and last update token +# formatted as a string and outputs to stdout a new update token and +# all files that have been modified since the update token. Paths must +# be relative to the root of the working tree and separated by a single NUL. +# +# To enable this hook, rename this file to "query-watchman" and set +# 'git config core.fsmonitor .git/hooks/query-watchman' +# +my ($version, $last_update_token) = @ARGV; + +# Uncomment for debugging +# print STDERR "$0 $version $last_update_token\n"; + +# Check the hook interface version +if ($version ne 2) { + die "Unsupported query-fsmonitor hook version '$version'.\n" . + "Falling back to scanning...\n"; +} + +my $git_work_tree = get_working_dir(); + +my $retry = 1; + +my $json_pkg; +eval { + require JSON::XS; + $json_pkg = "JSON::XS"; + 1; +} or do { + require JSON::PP; + $json_pkg = "JSON::PP"; +}; + +launch_watchman(); + +sub launch_watchman { + my $o = watchman_query(); + if (is_work_tree_watched($o)) { + output_result($o->{clock}, @{$o->{files}}); + } +} + +sub output_result { + my ($clockid, @files) = @_; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # binmode $fh, ":utf8"; + # print $fh "$clockid\n@files\n"; + # close $fh; + + binmode STDOUT, ":utf8"; + print $clockid; + print "\0"; + local $, = "\0"; + print @files; +} + +sub watchman_clock { + my $response = qx/watchman clock "$git_work_tree"/; + die "Failed to get clock id on '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + + return $json_pkg->new->utf8->decode($response); +} + +sub watchman_query { + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') + or die "open2() failed: $!\n" . + "Falling back to scanning...\n"; + + # In the query expression below we're asking for names of files that + # changed since $last_update_token but not from the .git folder. + # + # To accomplish this, we're using the "since" generator to use the + # recency index to select candidate nodes and "fields" to limit the + # output to file names only. Then we're using the "expression" term to + # further constrain the results. + my $last_update_line = ""; + if (substr($last_update_token, 0, 1) eq "c") { + $last_update_token = "\"$last_update_token\""; + $last_update_line = qq[\n"since": $last_update_token,]; + } + my $query = <<" END"; + ["query", "$git_work_tree", {$last_update_line + "fields": ["name"], + "expression": ["not", ["dirname", ".git"]] + }] + END + + # Uncomment for debugging the watchman query + # open (my $fh, ">", ".git/watchman-query.json"); + # print $fh $query; + # close $fh; + + print CHLD_IN $query; + close CHLD_IN; + my $response = do {local $/; }; + + # Uncomment for debugging the watch response + # open ($fh, ">", ".git/watchman-response.json"); + # print $fh $response; + # close $fh; + + die "Watchman: command returned no output.\n" . + "Falling back to scanning...\n" if $response eq ""; + die "Watchman: command returned invalid output: $response\n" . + "Falling back to scanning...\n" unless $response =~ /^\{/; + + return $json_pkg->new->utf8->decode($response); +} + +sub is_work_tree_watched { + my ($output) = @_; + my $error = $output->{error}; + if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { + $retry--; + my $response = qx/watchman watch "$git_work_tree"/; + die "Failed to make watchman watch '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + $output = $json_pkg->new->utf8->decode($response); + $error = $output->{error}; + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # close $fh; + + # Watchman will always return all files on the first query so + # return the fast "everything is dirty" flag to git and do the + # Watchman query just to get it over with now so we won't pay + # the cost in git to look up each individual file. + my $o = watchman_clock(); + $error = $output->{error}; + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + output_result($o->{clock}, ("/")); + $last_update_token = $o->{clock}; + + eval { launch_watchman() }; + return 0; + } + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + return 1; +} + +sub get_working_dir { + my $working_dir; + if ($^O =~ 'msys' || $^O =~ 'cygwin') { + $working_dir = Win32::GetCwd(); + $working_dir =~ tr/\\/\//; + } else { + require Cwd; + $working_dir = Cwd::cwd(); + } + + return $working_dir; +} diff --git a/health_assistant/hooks/post-update.sample b/health_assistant/hooks/post-update.sample new file mode 100644 index 0000000000000000000000000000000000000000..ec17ec1939b7c3e86b7cb6c0c4de6b0818a7e75e --- /dev/null +++ b/health_assistant/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/health_assistant/hooks/pre-applypatch.sample b/health_assistant/hooks/pre-applypatch.sample new file mode 100644 index 0000000000000000000000000000000000000000..4142082bcb939bbc17985a69ba748491ac6b62a5 --- /dev/null +++ b/health_assistant/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/health_assistant/hooks/pre-commit.sample b/health_assistant/hooks/pre-commit.sample new file mode 100644 index 0000000000000000000000000000000000000000..29ed5ee486a4f07c3f0558101ef8efc46f3d6ab7 --- /dev/null +++ b/health_assistant/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=$(git hash-object -t tree /dev/null) +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --type=bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff-index --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/health_assistant/hooks/pre-merge-commit.sample b/health_assistant/hooks/pre-merge-commit.sample new file mode 100644 index 0000000000000000000000000000000000000000..399eab1924e39da570b389b0bef1ca713b3b05c3 --- /dev/null +++ b/health_assistant/hooks/pre-merge-commit.sample @@ -0,0 +1,13 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git merge" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message to +# stderr if it wants to stop the merge commit. +# +# To enable this hook, rename this file to "pre-merge-commit". + +. git-sh-setup +test -x "$GIT_DIR/hooks/pre-commit" && + exec "$GIT_DIR/hooks/pre-commit" +: diff --git a/health_assistant/hooks/pre-push.sample b/health_assistant/hooks/pre-push.sample new file mode 100644 index 0000000000000000000000000000000000000000..4ce688d32b7532862767345f2b991ae856f7d4a8 --- /dev/null +++ b/health_assistant/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/health_assistant/hooks/pre-rebase.sample b/health_assistant/hooks/pre-rebase.sample new file mode 100644 index 0000000000000000000000000000000000000000..6cbef5c370d8c3486ca85423dd70440c5e0a2aa2 --- /dev/null +++ b/health_assistant/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up to date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +<<\DOC_END + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". + +DOC_END diff --git a/health_assistant/hooks/pre-receive.sample b/health_assistant/hooks/pre-receive.sample new file mode 100644 index 0000000000000000000000000000000000000000..a1fd29ec14823d8bc4a8d1a2cfe35451580f5118 --- /dev/null +++ b/health_assistant/hooks/pre-receive.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to make use of push options. +# The example simply echoes all push options that start with 'echoback=' +# and rejects all pushes when the "reject" push option is used. +# +# To enable this hook, rename this file to "pre-receive". + +if test -n "$GIT_PUSH_OPTION_COUNT" +then + i=0 + while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" + do + eval "value=\$GIT_PUSH_OPTION_$i" + case "$value" in + echoback=*) + echo "echo from the pre-receive-hook: ${value#*=}" >&2 + ;; + reject) + exit 1 + esac + i=$((i + 1)) + done +fi diff --git a/health_assistant/hooks/prepare-commit-msg.sample b/health_assistant/hooks/prepare-commit-msg.sample new file mode 100644 index 0000000000000000000000000000000000000000..10fa14c5ab0134436e2ae435138bf921eb477c60 --- /dev/null +++ b/health_assistant/hooks/prepare-commit-msg.sample @@ -0,0 +1,42 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first one removes the +# "# Please enter the commit message..." help message. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" + +# case "$COMMIT_SOURCE,$SHA1" in +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; +# *) ;; +# esac + +# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" +# if test -z "$COMMIT_SOURCE" +# then +# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" +# fi diff --git a/health_assistant/hooks/push-to-checkout.sample b/health_assistant/hooks/push-to-checkout.sample new file mode 100644 index 0000000000000000000000000000000000000000..af5a0c0018b5e9c04b56ac52f21b4d28f48d99ea --- /dev/null +++ b/health_assistant/hooks/push-to-checkout.sample @@ -0,0 +1,78 @@ +#!/bin/sh + +# An example hook script to update a checked-out tree on a git push. +# +# This hook is invoked by git-receive-pack(1) when it reacts to git +# push and updates reference(s) in its repository, and when the push +# tries to update the branch that is currently checked out and the +# receive.denyCurrentBranch configuration variable is set to +# updateInstead. +# +# By default, such a push is refused if the working tree and the index +# of the remote repository has any difference from the currently +# checked out commit; when both the working tree and the index match +# the current commit, they are updated to match the newly pushed tip +# of the branch. This hook is to be used to override the default +# behaviour; however the code below reimplements the default behaviour +# as a starting point for convenient modification. +# +# The hook receives the commit with which the tip of the current +# branch is going to be updated: +commit=$1 + +# It can exit with a non-zero status to refuse the push (when it does +# so, it must not modify the index or the working tree). +die () { + echo >&2 "$*" + exit 1 +} + +# Or it can make any necessary changes to the working tree and to the +# index to bring them to the desired state when the tip of the current +# branch is updated to the new commit, and exit with a zero status. +# +# For example, the hook can simply run git read-tree -u -m HEAD "$1" +# in order to emulate git fetch that is run in the reverse direction +# with git push, as the two-tree form of git read-tree -u -m is +# essentially the same as git switch or git checkout that switches +# branches while keeping the local changes in the working tree that do +# not interfere with the difference between the branches. + +# The below is a more-or-less exact translation to shell of the C code +# for the default behaviour for git's push-to-checkout hook defined in +# the push_to_deploy() function in builtin/receive-pack.c. +# +# Note that the hook will be executed from the repository directory, +# not from the working tree, so if you want to perform operations on +# the working tree, you will have to adapt your code accordingly, e.g. +# by adding "cd .." or using relative paths. + +if ! git update-index -q --ignore-submodules --refresh +then + die "Up-to-date check failed" +fi + +if ! git diff-files --quiet --ignore-submodules -- +then + die "Working directory has unstaged changes" +fi + +# This is a rough translation of: +# +# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX +if git cat-file -e HEAD 2>/dev/null +then + head=HEAD +else + head=$(git hash-object -t tree --stdin &2 + exit 1 +} + +unset GIT_DIR GIT_WORK_TREE +cd "$worktree" && + +if grep -q "^diff --git " "$1" +then + validate_patch "$1" +else + validate_cover_letter "$1" +fi && + +if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL" +then + git config --unset-all sendemail.validateWorktree && + trap 'git worktree remove -ff "$worktree"' EXIT && + validate_series +fi diff --git a/health_assistant/hooks/update.sample b/health_assistant/hooks/update.sample new file mode 100644 index 0000000000000000000000000000000000000000..c4d426bc6ee9430ee7813263ce6d5da7ec78c3c6 --- /dev/null +++ b/health_assistant/hooks/update.sample @@ -0,0 +1,128 @@ +#!/bin/sh +# +# An example hook script to block unannotated tags from entering. +# Called by "git receive-pack" with arguments: refname sha1-old sha1-new +# +# To enable this hook, rename this file to "update". +# +# Config +# ------ +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# hooks.allowdeletetag +# This boolean sets whether deleting tags will be allowed in the +# repository. By default they won't be. +# hooks.allowmodifytag +# This boolean sets whether a tag may be modified after creation. By default +# it won't be. +# hooks.allowdeletebranch +# This boolean sets whether deleting branches will be allowed in the +# repository. By default they won't be. +# hooks.denycreatebranch +# This boolean sets whether remotely creating branches will be denied +# in the repository. By default this is allowed. +# + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --type=bool hooks.allowunannotated) +allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch) +denycreatebranch=$(git config --type=bool hooks.denycreatebranch) +allowdeletetag=$(git config --type=bool hooks.allowdeletetag) +allowmodifytag=$(git config --type=bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero=$(git hash-object --stdin &2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/health_assistant/index b/health_assistant/index new file mode 100644 index 0000000000000000000000000000000000000000..8267283d250b714df40e1f375034761ca686c1de Binary files /dev/null and b/health_assistant/index differ diff --git a/health_assistant/info/exclude b/health_assistant/info/exclude new file mode 100644 index 0000000000000000000000000000000000000000..a5196d1be8fb59edf8062bef36d3a602e0812139 --- /dev/null +++ b/health_assistant/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/health_assistant/logs/HEAD b/health_assistant/logs/HEAD new file mode 100644 index 0000000000000000000000000000000000000000..00c5c18ad3d1341ab00b87e7b6878819c5fb0bc4 --- /dev/null +++ b/health_assistant/logs/HEAD @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 f09f40f2e528e117c4c0b66fca5629330632643b ting1234555 1751803650 +0800 clone: from https://huggingface.co/spaces/yuting111222/health-assistant diff --git a/health_assistant/logs/refs/heads/main b/health_assistant/logs/refs/heads/main new file mode 100644 index 0000000000000000000000000000000000000000..00c5c18ad3d1341ab00b87e7b6878819c5fb0bc4 --- /dev/null +++ b/health_assistant/logs/refs/heads/main @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 f09f40f2e528e117c4c0b66fca5629330632643b ting1234555 1751803650 +0800 clone: from https://huggingface.co/spaces/yuting111222/health-assistant diff --git a/health_assistant/logs/refs/remotes/origin/HEAD b/health_assistant/logs/refs/remotes/origin/HEAD new file mode 100644 index 0000000000000000000000000000000000000000..00c5c18ad3d1341ab00b87e7b6878819c5fb0bc4 --- /dev/null +++ b/health_assistant/logs/refs/remotes/origin/HEAD @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 f09f40f2e528e117c4c0b66fca5629330632643b ting1234555 1751803650 +0800 clone: from https://huggingface.co/spaces/yuting111222/health-assistant diff --git a/health_assistant/objects/03/808799f09fb38f30b901ea11bc56aacf3e23aa b/health_assistant/objects/03/808799f09fb38f30b901ea11bc56aacf3e23aa new file mode 100644 index 0000000000000000000000000000000000000000..4617fb490c314a710efad38a06ff379fcdf83331 Binary files /dev/null and b/health_assistant/objects/03/808799f09fb38f30b901ea11bc56aacf3e23aa differ diff --git a/health_assistant/objects/04/a3266886b16b05e1975a3105a421c3a8021577 b/health_assistant/objects/04/a3266886b16b05e1975a3105a421c3a8021577 new file mode 100644 index 0000000000000000000000000000000000000000..2ea378df7e02112a21964fc891dfd6e5c9aa92ee --- /dev/null +++ b/health_assistant/objects/04/a3266886b16b05e1975a3105a421c3a8021577 @@ -0,0 +1,3 @@ +xeRM0<"h9Es(B] KȱNJ.vMZe2o>^KM~~uWԑú~H򍟄2FNFX]-mĖΒԜbXT + qe!ZΕ2Hb֢%`B-n4\.nl.6[Qra4̛_,A'1&ZF ERo9.?% Bm&RS +~8q*>warn)tw&:O$t_WPJc3yS E!Pj![b⎽gJKcȣd[&@+8㣝;,o_ߟ}3$ia{K-"ȢT2Cy}q(IU_ SH׎dmov \ No newline at end of file diff --git a/health_assistant/objects/0f/39f8b94933990e7d7ba2c458cfbab7333218ef b/health_assistant/objects/0f/39f8b94933990e7d7ba2c458cfbab7333218ef new file mode 100644 index 0000000000000000000000000000000000000000..057985f1ec599d0acf77bb6d584b61c8ba36fab5 --- /dev/null +++ b/health_assistant/objects/0f/39f8b94933990e7d7ba2c458cfbab7333218ef @@ -0,0 +1 @@ +xe10F+A1.iL'ƻ/J+8ck0 ?s-{D {8LԼ7m \SgG6}Gc),6ZpĖ%g]USuNrҚ'<^?Y, \ No newline at end of file diff --git a/health_assistant/objects/11/1aa4c624e6bc14d47ff41862aba34010af0c62 b/health_assistant/objects/11/1aa4c624e6bc14d47ff41862aba34010af0c62 new file mode 100644 index 0000000000000000000000000000000000000000..05a8e2455149afee2b6d6516d0178644af7f77e5 Binary files /dev/null and b/health_assistant/objects/11/1aa4c624e6bc14d47ff41862aba34010af0c62 differ diff --git a/health_assistant/objects/11/9a0d4950b1f5962a7006e198262724bd9e66e2 b/health_assistant/objects/11/9a0d4950b1f5962a7006e198262724bd9e66e2 new file mode 100644 index 0000000000000000000000000000000000000000..2c3c3e4e771ee81e6fa4f1c7135068b65b244edc --- /dev/null +++ b/health_assistant/objects/11/9a0d4950b1f5962a7006e198262724bd9e66e2 @@ -0,0 +1,5 @@ +xUPN@ޯ$r tR + + +~FV#DMg[XC(KLɋ8PK+eiRuv)s >]Xp q;E{=0s F + iz*>g|6׬/qHOIwLd#;0GsSk i:Ӊ7/` \ No newline at end of file diff --git a/health_assistant/objects/14/3dd33eab4bef93634d47a71ae49a09a758b3e5 b/health_assistant/objects/14/3dd33eab4bef93634d47a71ae49a09a758b3e5 new file mode 100644 index 0000000000000000000000000000000000000000..f0a45ef54a13672522c799144a8ba203003fc862 Binary files /dev/null and b/health_assistant/objects/14/3dd33eab4bef93634d47a71ae49a09a758b3e5 differ diff --git a/health_assistant/objects/15/c4055aaaebd74f8aeb2682016bcd45ece24da0 b/health_assistant/objects/15/c4055aaaebd74f8aeb2682016bcd45ece24da0 new file mode 100644 index 0000000000000000000000000000000000000000..e096aed65ea48dbcfb99dfb62bf67015dacc1ced Binary files /dev/null and b/health_assistant/objects/15/c4055aaaebd74f8aeb2682016bcd45ece24da0 differ diff --git a/health_assistant/objects/1d/5910688a8e03c51348bf0376168ec584804f16 b/health_assistant/objects/1d/5910688a8e03c51348bf0376168ec584804f16 new file mode 100644 index 0000000000000000000000000000000000000000..ce79e434b2b659e108097b97c56b3000e5cc942d Binary files /dev/null and b/health_assistant/objects/1d/5910688a8e03c51348bf0376168ec584804f16 differ diff --git a/health_assistant/objects/23/a2e0dcc207c069ee1c840c5942c9ce71af908b b/health_assistant/objects/23/a2e0dcc207c069ee1c840c5942c9ce71af908b new file mode 100644 index 0000000000000000000000000000000000000000..67abf9d050c6c845b1228a42fc9f5e9931eaa861 Binary files /dev/null and b/health_assistant/objects/23/a2e0dcc207c069ee1c840c5942c9ce71af908b differ diff --git a/health_assistant/objects/26/63a8ac018f2cdeb2bb5a3af69117acf5530646 b/health_assistant/objects/26/63a8ac018f2cdeb2bb5a3af69117acf5530646 new file mode 100644 index 0000000000000000000000000000000000000000..86c111a31ec4b15a85f070f461b008067e60dd79 Binary files /dev/null and b/health_assistant/objects/26/63a8ac018f2cdeb2bb5a3af69117acf5530646 differ diff --git a/health_assistant/objects/33/a4b651c22e0bfaf0d50a4aaa15703c1c36e76b b/health_assistant/objects/33/a4b651c22e0bfaf0d50a4aaa15703c1c36e76b new file mode 100644 index 0000000000000000000000000000000000000000..e05251607ef654cab3ad932cf41642ac09421c04 Binary files /dev/null and b/health_assistant/objects/33/a4b651c22e0bfaf0d50a4aaa15703c1c36e76b differ diff --git a/health_assistant/objects/38/d39dcf7d4f60b1322f2c6bf7d1a28317617ca0 b/health_assistant/objects/38/d39dcf7d4f60b1322f2c6bf7d1a28317617ca0 new file mode 100644 index 0000000000000000000000000000000000000000..1ace9895a4b27ddc3dac8bc85664d42c69229a43 Binary files /dev/null and b/health_assistant/objects/38/d39dcf7d4f60b1322f2c6bf7d1a28317617ca0 differ diff --git a/health_assistant/objects/3b/522f444b89f7b88717f76dfed5146c19f3d4a5 b/health_assistant/objects/3b/522f444b89f7b88717f76dfed5146c19f3d4a5 new file mode 100644 index 0000000000000000000000000000000000000000..1bd0cea7eb565571607ffb5fcf7e09153a8b5412 --- /dev/null +++ b/health_assistant/objects/3b/522f444b89f7b88717f76dfed5146c19f3d4a5 @@ -0,0 +1,2 @@ +x51 WlBLVj{![޽}z [_PsNRB c6h_yӴUC%9 RkrXA&_>|ϝj*<-2JT{MN>l7ĔSXdlhG_;>-nOhJx$7fbq;k\ Ψ7sPV˩]#mcmeS \ No newline at end of file diff --git a/health_assistant/objects/43/f1e426257de200947d3dd1fd49bc33c44d9c83 b/health_assistant/objects/43/f1e426257de200947d3dd1fd49bc33c44d9c83 new file mode 100644 index 0000000000000000000000000000000000000000..c7ef31cb10138c112af05174787e27968870f6bd Binary files /dev/null and b/health_assistant/objects/43/f1e426257de200947d3dd1fd49bc33c44d9c83 differ diff --git a/health_assistant/objects/46/880c61fa30fe6dadf82ccf705939609f9b9e29 b/health_assistant/objects/46/880c61fa30fe6dadf82ccf705939609f9b9e29 new file mode 100644 index 0000000000000000000000000000000000000000..49d26b3e7f861f5106695d4f1b26e69dd2246c6a Binary files /dev/null and b/health_assistant/objects/46/880c61fa30fe6dadf82ccf705939609f9b9e29 differ diff --git a/health_assistant/objects/48/de39c526a6766ebb4ff4df91c4a9f2b849d573 b/health_assistant/objects/48/de39c526a6766ebb4ff4df91c4a9f2b849d573 new file mode 100644 index 0000000000000000000000000000000000000000..a9a027604ef156d30617ef699d9c1ffef43442c2 Binary files /dev/null and b/health_assistant/objects/48/de39c526a6766ebb4ff4df91c4a9f2b849d573 differ diff --git a/health_assistant/objects/49/ff637b1a0bb9187421abdca60b3ad52e610ba3 b/health_assistant/objects/49/ff637b1a0bb9187421abdca60b3ad52e610ba3 new file mode 100644 index 0000000000000000000000000000000000000000..687c9543dd4174022cc6d2cf59ac85b669c8e321 Binary files /dev/null and b/health_assistant/objects/49/ff637b1a0bb9187421abdca60b3ad52e610ba3 differ diff --git a/health_assistant/objects/4f/72b9e2bdc61bedfefbf0b4521d6fe97597d7b3 b/health_assistant/objects/4f/72b9e2bdc61bedfefbf0b4521d6fe97597d7b3 new file mode 100644 index 0000000000000000000000000000000000000000..bae6966a27b5e7744a178cf70dfc42dac396cd6a Binary files /dev/null and b/health_assistant/objects/4f/72b9e2bdc61bedfefbf0b4521d6fe97597d7b3 differ diff --git a/health_assistant/objects/52/b3fbbcfca2eb4cdec108e5bd035126f217b293 b/health_assistant/objects/52/b3fbbcfca2eb4cdec108e5bd035126f217b293 new file mode 100644 index 0000000000000000000000000000000000000000..1856b546e0f5219d45efe08602d39f7f9127df80 --- /dev/null +++ b/health_assistant/objects/52/b3fbbcfca2eb4cdec108e5bd035126f217b293 @@ -0,0 +1,4 @@ +xmM +0D]YL7ҍ[~4&iin" +n7ghGfT`n8vܪjI"QGXx +8 Q^iBHkcR/BaO/W3!B!I7r&,O^ \ No newline at end of file diff --git a/health_assistant/objects/53/cde54d3d8f84865b49e110bf53570d23a3ea0d b/health_assistant/objects/53/cde54d3d8f84865b49e110bf53570d23a3ea0d new file mode 100644 index 0000000000000000000000000000000000000000..2ac6914b153d77adfc2a120bd73079ff70b05c17 Binary files /dev/null and b/health_assistant/objects/53/cde54d3d8f84865b49e110bf53570d23a3ea0d differ diff --git a/health_assistant/objects/54/b39dd1d900e866bb91ee441d372a8924b9d87a b/health_assistant/objects/54/b39dd1d900e866bb91ee441d372a8924b9d87a new file mode 100644 index 0000000000000000000000000000000000000000..352e1e16511fefe061a1482023d024ff14b79e07 --- /dev/null +++ b/health_assistant/objects/54/b39dd1d900e866bb91ee441d372a8924b9d87a @@ -0,0 +1,2 @@ +xe +0D=+rK [=x(BtK"ZٺZ7ۅ=(+(T!sx)ʒצurD@3C3uG0àƸ OEbf<=ńf*rCȄt%Yܱr@'._b)rNO \ No newline at end of file diff --git a/health_assistant/objects/57/d9f1a96b174340b1184274f446a84b7809f550 b/health_assistant/objects/57/d9f1a96b174340b1184274f446a84b7809f550 new file mode 100644 index 0000000000000000000000000000000000000000..0419d16e824cef4fa7452c86238490ffff4aebc1 Binary files /dev/null and b/health_assistant/objects/57/d9f1a96b174340b1184274f446a84b7809f550 differ diff --git a/health_assistant/objects/58/117b68d254395c85ed0ab3eccb06a2fec6de6e b/health_assistant/objects/58/117b68d254395c85ed0ab3eccb06a2fec6de6e new file mode 100644 index 0000000000000000000000000000000000000000..a55bf08ef70e1bbbb5205f424c85b2bb0d31fb1a Binary files /dev/null and b/health_assistant/objects/58/117b68d254395c85ed0ab3eccb06a2fec6de6e differ diff --git a/health_assistant/objects/67/fdcccd62bab1dee563ec316ed3167eada7405a b/health_assistant/objects/67/fdcccd62bab1dee563ec316ed3167eada7405a new file mode 100644 index 0000000000000000000000000000000000000000..4e9467328cc4f92426c224ce215b2449bd8ad6cf --- /dev/null +++ b/health_assistant/objects/67/fdcccd62bab1dee563ec316ed3167eada7405a @@ -0,0 +1,2 @@ +x= 0D9m$BB@ywZZ"q7S NxZ+I mgb(\ |ޯr^+T=ߩVLnzl̨~-@M@"ShJ=V, +D0B*[GGC7sg['wO I \ No newline at end of file diff --git a/health_assistant/objects/70/3fe74f0168964cf7f5f199ce58e47b6804e86a b/health_assistant/objects/70/3fe74f0168964cf7f5f199ce58e47b6804e86a new file mode 100644 index 0000000000000000000000000000000000000000..404a8f7b0fee2fe2534d963773809d4427afb61c Binary files /dev/null and b/health_assistant/objects/70/3fe74f0168964cf7f5f199ce58e47b6804e86a differ diff --git a/health_assistant/objects/77/64d2969d8faac99e1d5e0ae69b12fc2e4b00a7 b/health_assistant/objects/77/64d2969d8faac99e1d5e0ae69b12fc2e4b00a7 new file mode 100644 index 0000000000000000000000000000000000000000..b04756e86b4f5eb3f0fa0b7f3cecc1a641c1562d Binary files /dev/null and b/health_assistant/objects/77/64d2969d8faac99e1d5e0ae69b12fc2e4b00a7 differ diff --git a/health_assistant/objects/7d/102024b5c3c2bcb666ae0f713592f4f67b3b84 b/health_assistant/objects/7d/102024b5c3c2bcb666ae0f713592f4f67b3b84 new file mode 100644 index 0000000000000000000000000000000000000000..415e9e25709932bb5aade679a0892696a5f72d03 Binary files /dev/null and b/health_assistant/objects/7d/102024b5c3c2bcb666ae0f713592f4f67b3b84 differ diff --git a/health_assistant/objects/7f/6ab33b6fe049c30175ed96d3573d074e19e417 b/health_assistant/objects/7f/6ab33b6fe049c30175ed96d3573d074e19e417 new file mode 100644 index 0000000000000000000000000000000000000000..e0c1d69caa21f4731d5a1103d4d8ae91a159df76 Binary files /dev/null and b/health_assistant/objects/7f/6ab33b6fe049c30175ed96d3573d074e19e417 differ diff --git a/health_assistant/objects/7f/afef93a79547a5582a9a9a7184aa756db6d19c b/health_assistant/objects/7f/afef93a79547a5582a9a9a7184aa756db6d19c new file mode 100644 index 0000000000000000000000000000000000000000..c9347951e165ec1c6302ae220a4dc2c77a221ca3 Binary files /dev/null and b/health_assistant/objects/7f/afef93a79547a5582a9a9a7184aa756db6d19c differ diff --git a/health_assistant/objects/80/53af6eed72a75288a8ee5bd5f6b835780d9dd6 b/health_assistant/objects/80/53af6eed72a75288a8ee5bd5f6b835780d9dd6 new file mode 100644 index 0000000000000000000000000000000000000000..18ab8d76dc670d5de3e42ccdb82afea8f7c42c0d Binary files /dev/null and b/health_assistant/objects/80/53af6eed72a75288a8ee5bd5f6b835780d9dd6 differ diff --git a/health_assistant/objects/82/c56be3d0801144b689b585331c55250878ffb2 b/health_assistant/objects/82/c56be3d0801144b689b585331c55250878ffb2 new file mode 100644 index 0000000000000000000000000000000000000000..5f2f71d0c447db7bdecd248745ca3bee9ac18a26 Binary files /dev/null and b/health_assistant/objects/82/c56be3d0801144b689b585331c55250878ffb2 differ diff --git a/health_assistant/objects/8b/137891791fe96927ad78e64b0aad7bded08bdc b/health_assistant/objects/8b/137891791fe96927ad78e64b0aad7bded08bdc new file mode 100644 index 0000000000000000000000000000000000000000..9d8f60531ebc0489c29cf79c42b6fc0e3583f9d9 Binary files /dev/null and b/health_assistant/objects/8b/137891791fe96927ad78e64b0aad7bded08bdc differ diff --git a/health_assistant/objects/8e/56fd781edb5d62a070b46ad49b8927516d90f0 b/health_assistant/objects/8e/56fd781edb5d62a070b46ad49b8927516d90f0 new file mode 100644 index 0000000000000000000000000000000000000000..47cb5fc5fd1c5b8dd887885eaae3e366133b83fa Binary files /dev/null and b/health_assistant/objects/8e/56fd781edb5d62a070b46ad49b8927516d90f0 differ diff --git a/health_assistant/objects/95/c5ab74551bf74921d0dddabb2907833f6791ff b/health_assistant/objects/95/c5ab74551bf74921d0dddabb2907833f6791ff new file mode 100644 index 0000000000000000000000000000000000000000..10a25d0743c09e95796ebd52e91567ea8291b890 Binary files /dev/null and b/health_assistant/objects/95/c5ab74551bf74921d0dddabb2907833f6791ff differ diff --git a/health_assistant/objects/9c/f69dcb2ab935d47041c5e9fee911f78d30ef09 b/health_assistant/objects/9c/f69dcb2ab935d47041c5e9fee911f78d30ef09 new file mode 100644 index 0000000000000000000000000000000000000000..1d50438c390629955064abc52913a339662434b2 Binary files /dev/null and b/health_assistant/objects/9c/f69dcb2ab935d47041c5e9fee911f78d30ef09 differ diff --git a/health_assistant/objects/9e/1f9ebd1c453f4ace6172135a7d05626a78cb9b b/health_assistant/objects/9e/1f9ebd1c453f4ace6172135a7d05626a78cb9b new file mode 100644 index 0000000000000000000000000000000000000000..6f49dbecb0323d4953112eb5213e15f618c57be1 Binary files /dev/null and b/health_assistant/objects/9e/1f9ebd1c453f4ace6172135a7d05626a78cb9b differ diff --git a/health_assistant/objects/a6/344aac8c09253b3b630fb776ae94478aa0275b b/health_assistant/objects/a6/344aac8c09253b3b630fb776ae94478aa0275b new file mode 100644 index 0000000000000000000000000000000000000000..9f125cc2655c4fddcf16576fb0da4bdb4658f1d9 --- /dev/null +++ b/health_assistant/objects/a6/344aac8c09253b3b630fb776ae94478aa0275b @@ -0,0 +1,2 @@ +x10 E9EHB %SB$JOO| /bvU6KCTkR0JMmd]H(@WQ +[#+ Nq=En+It9,Q `n[F4/w9[Id7Mǣ1G` ۾(V(?V&p| ,(zzSR2Ă \ No newline at end of file diff --git a/health_assistant/objects/aa/226358c7dabd3f318f77fa77eff852f57de9df b/health_assistant/objects/aa/226358c7dabd3f318f77fa77eff852f57de9df new file mode 100644 index 0000000000000000000000000000000000000000..173b7d7b5d8ded30f1b7d7bc4b46701de2e526cf --- /dev/null +++ b/health_assistant/objects/aa/226358c7dabd3f318f77fa77eff852f57de9df @@ -0,0 +1 @@ +xK 7 )2f ~>Tխjje`%Χnz7%@H7IrU'gL"/di֔9E G \ No newline at end of file diff --git a/health_assistant/objects/b3/050f4fa9a5a96587877757640152f5bf03b143 b/health_assistant/objects/b3/050f4fa9a5a96587877757640152f5bf03b143 new file mode 100644 index 0000000000000000000000000000000000000000..6bc3c2a13953cf9d1222f44526387d0be4eb1f0c Binary files /dev/null and b/health_assistant/objects/b3/050f4fa9a5a96587877757640152f5bf03b143 differ diff --git a/health_assistant/objects/b3/bb8e695b3c30bb12f7e8883b92ff6804cecd4a b/health_assistant/objects/b3/bb8e695b3c30bb12f7e8883b92ff6804cecd4a new file mode 100644 index 0000000000000000000000000000000000000000..55e3f93350d62f069584ae45936db20cc3cc02be Binary files /dev/null and b/health_assistant/objects/b3/bb8e695b3c30bb12f7e8883b92ff6804cecd4a differ diff --git a/health_assistant/objects/be/195d5756cded81c857953750dcfb3cad60ef00 b/health_assistant/objects/be/195d5756cded81c857953750dcfb3cad60ef00 new file mode 100644 index 0000000000000000000000000000000000000000..a48a51a23b6639a76a65f24ab4c35727e3e30acb Binary files /dev/null and b/health_assistant/objects/be/195d5756cded81c857953750dcfb3cad60ef00 differ diff --git a/health_assistant/objects/bf/e53d6d4e533cb5c43e4abe407fe13f7f924fcb b/health_assistant/objects/bf/e53d6d4e533cb5c43e4abe407fe13f7f924fcb new file mode 100644 index 0000000000000000000000000000000000000000..b32567ccc688ee730b9add40c822ea015540f6a8 --- /dev/null +++ b/health_assistant/objects/bf/e53d6d4e533cb5c43e4abe407fe13f7f924fcb @@ -0,0 +1 @@ +xmOO0 9SX=p1iIHm* m4 I:mT@|+-am,b8)qdB~4Լ +bR(7^"']11e'_b&FZEuO{qG%~>h. \ No newline at end of file diff --git a/health_assistant/objects/cd/0aac54270fface569a1c79fd97a7e76b1f2d20 b/health_assistant/objects/cd/0aac54270fface569a1c79fd97a7e76b1f2d20 new file mode 100644 index 0000000000000000000000000000000000000000..b3313a28572a83d7d34171a076d7bd06e5220df5 Binary files /dev/null and b/health_assistant/objects/cd/0aac54270fface569a1c79fd97a7e76b1f2d20 differ diff --git a/health_assistant/objects/d2/1d83b0b90a1179e5e18be9196db8e81fc12453 b/health_assistant/objects/d2/1d83b0b90a1179e5e18be9196db8e81fc12453 new file mode 100644 index 0000000000000000000000000000000000000000..7c29192fdd46bd996487c9f934efefcda8321dbd Binary files /dev/null and b/health_assistant/objects/d2/1d83b0b90a1179e5e18be9196db8e81fc12453 differ diff --git a/health_assistant/objects/d5/a46ac3bb27dc979fbfbcc01b12c553a9d54332 b/health_assistant/objects/d5/a46ac3bb27dc979fbfbcc01b12c553a9d54332 new file mode 100644 index 0000000000000000000000000000000000000000..b23d1f2d922acc9eb9f5e713344dc322d059dd40 Binary files /dev/null and b/health_assistant/objects/d5/a46ac3bb27dc979fbfbcc01b12c553a9d54332 differ diff --git a/health_assistant/objects/d7/87d6e7a9a681b46c0be4a938a0dc9778c33456 b/health_assistant/objects/d7/87d6e7a9a681b46c0be4a938a0dc9778c33456 new file mode 100644 index 0000000000000000000000000000000000000000..cd128f99708fb2b849483252cb595b3b500217d6 --- /dev/null +++ b/health_assistant/objects/d7/87d6e7a9a681b46c0be4a938a0dc9778c33456 @@ -0,0 +1,2 @@ +xM +0 =?=mx/@$8Veoo1X ߗ\| 4"QL/窾eOTB$%3 by*Y XRe:lsS#mYo/iA1ۉJ8 \ No newline at end of file diff --git a/health_assistant/objects/dc/a8ba02d37dfd402f17d6004160b24574432251 b/health_assistant/objects/dc/a8ba02d37dfd402f17d6004160b24574432251 new file mode 100644 index 0000000000000000000000000000000000000000..1ba53206375a70e6a44017f11f9fa83cfae2666f Binary files /dev/null and b/health_assistant/objects/dc/a8ba02d37dfd402f17d6004160b24574432251 differ diff --git a/health_assistant/objects/df/f5e8305a46ebc5d4a1392b29fc84405633b8f0 b/health_assistant/objects/df/f5e8305a46ebc5d4a1392b29fc84405633b8f0 new file mode 100644 index 0000000000000000000000000000000000000000..f53233f2a3280a27706819cce7f8ae6a613c3d01 --- /dev/null +++ b/health_assistant/objects/df/f5e8305a46ebc5d4a1392b29fc84405633b8f0 @@ -0,0 +1,2 @@ +x1 @QgNA:=) +A)L壒 Dict[str, Any]: + """創建新的用餐記錄""" + meal_service = MealService(db) + try: + meal_log = meal_service.create_meal_log( + food_name=meal.food_name, + meal_type=meal.meal_type, + portion_size=meal.portion_size, + nutrition=meal.nutrition, + meal_date=meal.meal_date, + image_url=meal.image_url, + ai_analysis=meal.ai_analysis + ) + return { + "success": True, + "message": "用餐記錄已創建", + "data": { + "id": meal_log.id, + "food_name": meal_log.food_name, + "meal_type": meal_log.meal_type, + "meal_date": meal_log.meal_date + } + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/list") +async def get_meal_logs( + date_range: DateRange, + db: Session = Depends(get_db) +) -> Dict[str, Any]: + """獲取用餐記錄列表""" + meal_service = MealService(db) + try: + logs = meal_service.get_meal_logs( + start_date=date_range.start_date, + end_date=date_range.end_date, + meal_type=date_range.meal_type + ) + return { + "success": True, + "data": [{ + "id": log.id, + "food_name": log.food_name, + "meal_type": log.meal_type, + "portion_size": log.portion_size, + "calories": log.calories, + "protein": log.protein, + "carbs": log.carbs, + "fat": log.fat, + "meal_date": log.meal_date, + "image_url": log.image_url + } for log in logs] + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/nutrition-summary") +async def get_nutrition_summary( + date_range: DateRange, + db: Session = Depends(get_db) +) -> Dict[str, Any]: + """獲取營養攝入總結""" + meal_service = MealService(db) + try: + summary = meal_service.get_nutrition_summary( + start_date=date_range.start_date, + end_date=date_range.end_date + ) + return { + "success": True, + "data": summary + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000000000000000000000000000000000000..cd0aac54270fface569a1c79fd97a7e76b1f2d20 --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.11.9 \ No newline at end of file diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0f39f8b94933990e7d7ba2c458cfbab7333218ef --- /dev/null +++ b/services/__init__.py @@ -0,0 +1,5 @@ +# This file makes the services directory a Python package +# Import the HybridFoodAnalyzer class to make it available when importing from app.services +from .food_analyzer_service import HybridFoodAnalyzer + +__all__ = ['HybridFoodAnalyzer'] diff --git a/services/__pycache__/__init__.cpython-313.pyc b/services/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d5780ad7dc020f15765fd98896d5aa6b30bb16f Binary files /dev/null and b/services/__pycache__/__init__.cpython-313.pyc differ diff --git a/services/__pycache__/ai_service.cpython-313.pyc b/services/__pycache__/ai_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3181fda1f30d4bd05bdafd2ccdc32c1bbb0b8c88 Binary files /dev/null and b/services/__pycache__/ai_service.cpython-313.pyc differ diff --git a/services/__pycache__/food_analyzer_service.cpython-313.pyc b/services/__pycache__/food_analyzer_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3ad10341c18173b8d026a92bebf6b3565a4a6b28 Binary files /dev/null and b/services/__pycache__/food_analyzer_service.cpython-313.pyc differ diff --git a/services/__pycache__/meal_service.cpython-313.pyc b/services/__pycache__/meal_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8fdd0eb755cf5172a8ac8e1280180eb9cc45bdd2 Binary files /dev/null and b/services/__pycache__/meal_service.cpython-313.pyc differ diff --git a/services/__pycache__/nutrition_api_service.cpython-313.pyc b/services/__pycache__/nutrition_api_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bf1b39a555a355b1db1722735318cf4a11f220cc Binary files /dev/null and b/services/__pycache__/nutrition_api_service.cpython-313.pyc differ diff --git a/services/ai_service.py b/services/ai_service.py new file mode 100644 index 0000000000000000000000000000000000000000..2663a8ac018f2cdeb2bb5a3af69117acf5530646 --- /dev/null +++ b/services/ai_service.py @@ -0,0 +1,95 @@ +# 檔案路徑: backend/app/services/ai_service.py + +from transformers.pipelines import pipeline +from PIL import Image +import io +import logging + +# 設置日誌 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# 全局變量 +image_classifier = None + +def load_model(): + """載入模型的函數""" + global image_classifier + + try: + logger.info("正在載入食物辨識模型...") + # 載入模型 - 移除不支持的參數 + image_classifier = pipeline( + "image-classification", + model="juliensimon/autotrain-food101-1471154053", + device=-1 # 使用CPU + ) + logger.info("模型載入成功!") + return True + except Exception as e: + logger.error(f"模型載入失敗: {str(e)}") + image_classifier = None + return False + +def classify_food_image(image_bytes: bytes) -> str: + """ + 接收圖片的二進位制數據,進行分類並返回可能性最高的食物名稱。 + """ + global image_classifier + + # 如果模型未載入,嘗試重新載入 + if image_classifier is None: + logger.warning("模型未載入,嘗試重新載入...") + if not load_model(): + return "Error: Model not loaded" + + if image_classifier is None: + return "Error: Model could not be loaded" + + try: + # 驗證圖片數據 + if not image_bytes: + return "Error: Empty image data" + + # 從記憶體中的 bytes 打開圖片 + image = Image.open(io.BytesIO(image_bytes)) + + # 確保圖片是RGB格式 + if image.mode != 'RGB': + image = image.convert('RGB') + + logger.info(f"處理圖片,尺寸: {image.size}") + + # 使用模型管線進行分類 + pipeline_output = image_classifier(image) + + logger.info(f"模型輸出: {pipeline_output}") + + # 處理輸出結果 + if not pipeline_output: + return "Unknown" + + # pipeline_output 通常是一個列表 + if isinstance(pipeline_output, list) and len(pipeline_output) > 0: + result = pipeline_output[0] + if isinstance(result, dict) and 'label' in result: + label = result['label'] + confidence = result.get('score', 0) + + logger.info(f"辨識結果: {label}, 信心度: {confidence:.2f}") + + # 標籤可能包含底線,我們將其替換為空格,並讓首字母大寫 + formatted_label = str(label).replace('_', ' ').title() + return formatted_label + + return "Unknown" + + except Exception as e: + logger.error(f"圖片分類過程中發生錯誤: {str(e)}") + return f"Error: {str(e)}" + +# 在模塊載入時嘗試載入模型 +logger.info("初始化 AI 服務...") +load_model() + +__all__ = ["classify_food_image"] \ No newline at end of file diff --git a/services/food_analyzer_service.py b/services/food_analyzer_service.py new file mode 100644 index 0000000000000000000000000000000000000000..b3bb8e695b3c30bb12f7e8883b92ff6804cecd4a --- /dev/null +++ b/services/food_analyzer_service.py @@ -0,0 +1,256 @@ +import torch +import json +from typing import Dict, Any, Optional +from PIL import Image +from io import BytesIO +import base64 +import os +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +class HybridFoodAnalyzer: + def __init__(self, claude_api_key: Optional[str] = None): + """ + Initialize the HybridFoodAnalyzer with HuggingFace model and Claude API. + + Args: + claude_api_key: Optional API key for Claude. If not provided, will try to get from environment variable CLAUDE_API_KEY. + """ + # Initialize HuggingFace model + from transformers import AutoImageProcessor, AutoModelForImageClassification + + print("Loading HuggingFace food recognition model...") + self.processor = AutoImageProcessor.from_pretrained("nateraw/food") + self.model = AutoModelForImageClassification.from_pretrained("nateraw/food") + self.model.eval() # Set model to evaluation mode + + # Initialize Claude API + print("Initializing Claude API...") + import anthropic + self.claude_api_key = claude_api_key or os.getenv('CLAUDE_API_KEY') + if not self.claude_api_key: + raise ValueError("Claude API key is required. Please set CLAUDE_API_KEY environment variable or pass it as an argument.") + + self.claude_client = anthropic.Anthropic(api_key=self.claude_api_key) + + def recognize_food(self, image: Image.Image) -> Dict[str, Any]: + """ + Recognize food from an image using HuggingFace model. + + Args: + image: PIL Image object containing the food image + + Returns: + Dictionary containing food name and confidence score + """ + try: + print("Processing image for food recognition...") + inputs = self.processor(images=image, return_tensors="pt") + + with torch.no_grad(): + outputs = self.model(**inputs) + predictions = torch.nn.functional.softmax(outputs.logits, dim=-1) + + predicted_class_id = int(predictions.argmax().item()) + confidence = predictions[0][predicted_class_id].item() + food_name = self.model.config.id2label[predicted_class_id] + + # Map common food names to Chinese + food_name_mapping = { + "hamburger": "漢堡", + "pizza": "披薩", + "sushi": "壽司", + "fried rice": "炒飯", + "chicken wings": "雞翅", + "salad": "沙拉", + "apple": "蘋果", + "banana": "香蕉", + "orange": "橙子", + "noodles": "麵條" + } + + chinese_name = food_name_mapping.get(food_name.lower(), food_name) + + return { + "food_name": food_name, + "chinese_name": chinese_name, + "confidence": confidence, + "success": True + } + + except Exception as e: + print(f"Error in food recognition: {str(e)}") + return { + "success": False, + "error": str(e) + } + + def analyze_nutrition(self, food_name: str) -> Dict[str, Any]: + """ + Analyze nutrition information for a given food using Claude API. + + Args: + food_name: Name of the food to analyze + + Returns: + Dictionary containing nutrition information + """ + try: + print(f"Analyzing nutrition for {food_name}...") + prompt = f""" + 請分析 {food_name} 的營養成分(每100g),並以JSON格式回覆: + {{ + "calories": 數值, + "protein": 數值, + "fat": 數值, + "carbs": 數值, + "fiber": 數值, + "sugar": 數值, + "sodium": 數值 + }} + """ + + message = self.claude_client.messages.create( + model="claude-3-sonnet-20240229", + max_tokens=500, + messages=[{"role": "user", "content": prompt}] + ) + + # Extract and parse the JSON response + response_text = message.content[0].get("text", "") if isinstance(message.content[0], dict) else str(message.content[0]) + try: + nutrition_data = json.loads(response_text.strip()) + return { + "success": True, + "nutrition": nutrition_data + } + except json.JSONDecodeError as e: + print(f"Error parsing Claude response: {e}") + return { + "success": False, + "error": f"Failed to parse nutrition data: {e}", + "raw_response": response_text + } + + except Exception as e: + print(f"Error in nutrition analysis: {str(e)}") + return { + "success": False, + "error": str(e) + } + + def process_image(self, image_data: bytes) -> Dict[str, Any]: + """ + Process an image to recognize food and analyze its nutrition. + + Args: + image_data: Binary image data + + Returns: + Dictionary containing recognition and analysis results + """ + try: + # Convert bytes to PIL Image + image = Image.open(BytesIO(image_data)) + + # Step 1: Recognize food + recognition_result = self.recognize_food(image) + if not recognition_result.get("success"): + return recognition_result + + # Step 2: Analyze nutrition + nutrition_result = self.analyze_nutrition(recognition_result["food_name"]) + if not nutrition_result.get("success"): + return nutrition_result + + # Calculate health score + nutrition = nutrition_result["nutrition"] + health_score = self.calculate_health_score(nutrition) + + # Generate recommendations and warnings + recommendations = self.generate_recommendations(nutrition) + warnings = self.generate_warnings(nutrition) + + return { + "success": True, + "food_name": recognition_result["food_name"], + "chinese_name": recognition_result["chinese_name"], + "confidence": recognition_result["confidence"], + "nutrition": nutrition, + "analysis": { + "healthScore": health_score, + "recommendations": recommendations, + "warnings": warnings + } + } + + except Exception as e: + return { + "success": False, + "error": f"Failed to process image: {str(e)}" + } + + def calculate_health_score(self, nutrition: Dict[str, float]) -> int: + """Calculate a health score based on nutritional values""" + score = 100 + + # 熱量評分 + if nutrition["calories"] > 400: + score -= 20 + elif nutrition["calories"] > 300: + score -= 10 + + # 脂肪評分 + if nutrition["fat"] > 20: + score -= 15 + elif nutrition["fat"] > 15: + score -= 8 + + # 蛋白質評分 + if nutrition["protein"] > 15: + score += 10 + elif nutrition["protein"] < 5: + score -= 10 + + # 鈉含量評分 + if "sodium" in nutrition and nutrition["sodium"] > 800: + score -= 15 + elif "sodium" in nutrition and nutrition["sodium"] > 600: + score -= 8 + + return max(0, min(100, score)) + + def generate_recommendations(self, nutrition: Dict[str, float]) -> list: + """Generate dietary recommendations based on nutrition data""" + recommendations = [] + + if nutrition["protein"] < 10: + recommendations.append("建議增加蛋白質攝取,可搭配雞蛋或豆腐") + + if nutrition["fat"] > 20: + recommendations.append("脂肪含量較高,建議適量食用") + + if "fiber" in nutrition and nutrition["fiber"] < 3: + recommendations.append("纖維含量不足,建議搭配蔬菜沙拉") + + if "sodium" in nutrition and nutrition["sodium"] > 600: + recommendations.append("鈉含量偏高,建議多喝水並減少其他鹽分攝取") + + return recommendations + + def generate_warnings(self, nutrition: Dict[str, float]) -> list: + """Generate dietary warnings based on nutrition data""" + warnings = [] + + if nutrition["calories"] > 500: + warnings.append("高熱量食物") + + if nutrition["fat"] > 25: + warnings.append("高脂肪食物") + + if "sodium" in nutrition and nutrition["sodium"] > 1000: + warnings.append("高鈉食物") + + return warnings diff --git a/services/meal_service.py b/services/meal_service.py new file mode 100644 index 0000000000000000000000000000000000000000..b0188b498ea687fbc125113b42f0a138eb2d1375 --- /dev/null +++ b/services/meal_service.py @@ -0,0 +1,87 @@ +from datetime import datetime +from typing import List, Dict, Any, Optional +from sqlalchemy.orm import Session +from models.meal_log import MealLog + +class MealService: + def __init__(self, db: Session): + self.db = db + + def create_meal_log( + self, + food_name: str, + meal_type: str, + portion_size: str, + nutrition: Dict[str, float], + meal_date: datetime, + image_url: Optional[str] = None, + ai_analysis: Optional[Dict[str, Any]] = None + ) -> MealLog: + """創建新的用餐記錄""" + meal_log = MealLog( + food_name=food_name, + meal_type=meal_type, + portion_size=portion_size, + calories=float(nutrition.get('calories', 0)), + protein=float(nutrition.get('protein', 0)), + carbs=float(nutrition.get('carbs', 0)), + fat=float(nutrition.get('fat', 0)), + fiber=float(nutrition.get('fiber', 0)), + meal_date=meal_date, + image_url=image_url, + ai_analysis=ai_analysis, + created_at=datetime.utcnow() + ) + self.db.add(meal_log) + self.db.commit() + self.db.refresh(meal_log) + return meal_log + + def get_meal_logs( + self, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + meal_type: Optional[str] = None + ) -> List[MealLog]: + """獲取用餐記錄""" + query = self.db.query(MealLog) + if start_date: + query = query.filter(MealLog.meal_date >= start_date) + if end_date: + query = query.filter(MealLog.meal_date <= end_date) + if meal_type: + query = query.filter(MealLog.meal_type == meal_type) + return query.order_by(MealLog.meal_date.desc()).all() + + def get_nutrition_summary( + self, + start_date: datetime, + end_date: datetime + ) -> Dict[str, float]: + """獲取指定時間範圍內的營養攝入總結""" + meals = self.get_meal_logs(start_date, end_date) + summary: Dict[str, float] = { + 'total_calories': 0.0, + 'total_protein': 0.0, + 'total_carbs': 0.0, + 'total_fat': 0.0, + 'total_fiber': 0.0 + } + for meal in meals: + portion_size = getattr(meal, 'portion_size', 'medium') + calories = getattr(meal, 'calories', 0.0) + protein = getattr(meal, 'protein', 0.0) + carbs = getattr(meal, 'carbs', 0.0) + fat = getattr(meal, 'fat', 0.0) + fiber = getattr(meal, 'fiber', 0.0) + multiplier = { + 'small': 0.7, + 'medium': 1.0, + 'large': 1.3 + }.get(portion_size, 1.0) + summary['total_calories'] += float(calories) * multiplier + summary['total_protein'] += float(protein) * multiplier + summary['total_carbs'] += float(carbs) * multiplier + summary['total_fat'] += float(fat) * multiplier + summary['total_fiber'] += float(fiber) * multiplier + return summary diff --git a/services/nutrition_api_service.py b/services/nutrition_api_service.py new file mode 100644 index 0000000000000000000000000000000000000000..c7d40c51eedb24b81645af232144a0653af38b02 --- /dev/null +++ b/services/nutrition_api_service.py @@ -0,0 +1,101 @@ +# backend/app/services/nutrition_api_service.py +import os +import requests +from dotenv import load_dotenv +import logging + +# 設置日誌 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# 載入環境變數 +load_dotenv() + +# 從環境變數中獲取 API 金鑰 +USDA_API_KEY = os.getenv("USDA_API_KEY") +USDA_API_URL = "https://api.nal.usda.gov/fdc/v1/foods/search" + +# 我們關心的主要營養素及其在 USDA API 中的名稱或編號 +# 我們可以透過 nutrient.nutrientNumber 或 nutrient.name 來匹配 +NUTRIENT_MAP = { + 'calories': 'Energy', + 'protein': 'Protein', + 'fat': 'Total lipid (fat)', + 'carbs': 'Carbohydrate, by difference', + 'fiber': 'Fiber, total dietary', + 'sugar': 'Total sugars', + 'sodium': 'Sodium, Na' +} + +def fetch_nutrition_data(food_name: str): + """ + 從 USDA FoodData Central API 獲取食物的營養資訊。 + + :param food_name: 要查詢的食物名稱 (例如 "Donuts")。 + :return: 包含營養資訊的字典,如果找不到則返回 None。 + """ + if not USDA_API_KEY: + logger.error("USDA_API_KEY 未設定,無法查詢營養資訊。請在環境變數中設定 USDA_API_KEY") + return None + + params = { + 'query': food_name, + 'api_key': USDA_API_KEY, + 'dataType': 'Branded', # 優先搜尋品牌食品,結果通常更符合預期 + 'pageSize': 1 # 我們只需要最相關的一筆結果 + } + + try: + logger.info(f"正在向 USDA API 查詢食物:{food_name}") + response = requests.get(USDA_API_URL, params=params) + response.raise_for_status() # 如果請求失敗 (例如 4xx 或 5xx),則會拋出異常 + + data = response.json() + + # 檢查是否有找到食物 + if data.get('foods') and len(data['foods']) > 0: + food_data = data['foods'][0] # 取第一個最相關的結果 + logger.info(f"從 API 成功獲取到食物 '{food_data.get('description')}' 的資料") + + nutrition_info = { + "food_name": food_data.get('description', food_name).capitalize(), + "chinese_name": None, # USDA API 不提供中文名 + } + + # 遍歷我們需要的營養素 + extracted_nutrients = {key: 0.0 for key in NUTRIENT_MAP.keys()} # 初始化 + + for nutrient in food_data.get('foodNutrients', []): + for key, name in NUTRIENT_MAP.items(): + if nutrient.get('nutrientName').strip().lower() == name.strip().lower(): + # 將值存入我們的格式 + extracted_nutrients[key] = float(nutrient.get('value', 0.0)) + break # 找到後就跳出內層迴圈 + + nutrition_info.update(extracted_nutrients) + + # 由於 USDA 不直接提供健康建議,我們先回傳原始數據 + # 後續可以在 main.py 中根據這些數據生成我們自己的建議 + return nutrition_info + + else: + logger.warning(f"在 USDA API 中找不到食物:{food_name}") + return None + + except requests.exceptions.RequestException as e: + logger.error(f"請求 USDA API 時發生錯誤: {e}") + return None + except Exception as e: + logger.error(f"處理 API 回應時發生未知錯誤: {e}") + return None + +if __name__ == '__main__': + # 測試此模組的功能 + test_food = "donuts" + nutrition = fetch_nutrition_data(test_food) + if nutrition: + import json + print(f"成功獲取 '{test_food}' 的營養資訊:") + print(json.dumps(nutrition, indent=2)) + else: + print(f"無法獲取 '{test_food}' 的營養資訊。") \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..b3050f4fa9a5a96587877757640152f5bf03b143 --- /dev/null +++ b/setup.py @@ -0,0 +1,30 @@ +from setuptools import setup, find_packages + +setup( + name="health_assistant", + version="0.1.0", + packages=find_packages(where="backend"), + package_dir={"": "backend"}, + install_requires=[ + "fastapi>=0.68.0", + "uvicorn>=0.15.0", + "python-multipart>=0.0.5", + "Pillow>=8.3.1", + "transformers>=4.11.3", + "torch>=1.9.0", + "python-dotenv>=0.19.0", + "httpx>=0.19.0", + "pydantic>=1.8.0", + ], + extras_require={ + "dev": [ + "pytest>=6.2.5", + "pytest-cov>=2.12.1", + "pytest-asyncio>=0.15.1", + "black>=21.7b0", + "isort>=5.9.3", + "mypy>=0.910", + ], + }, + python_requires=">=3.8", +) diff --git a/test_app.py b/test_app.py new file mode 100644 index 0000000000000000000000000000000000000000..928f4ad1fc2c38db88023afb43ead6d4f10c2471 --- /dev/null +++ b/test_app.py @@ -0,0 +1,76 @@ +from fastapi import FastAPI, File, UploadFile, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from typing import Dict, Any, List, Optional + +app = FastAPI(title="Health Assistant API - Test Version") + +# 配置 CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 直接在這個檔案中定義 router 和端點 +class WeightEstimationResponse(BaseModel): + food_type: str + estimated_weight: float + weight_confidence: float + weight_error_range: List[float] + nutrition: Dict[str, Any] + reference_object: Optional[str] = None + note: str + +@app.get("/") +async def root(): + return {"message": "Health Assistant API - Test Version is running"} + +@app.get("/health") +async def health_check(): + return { + "status": "healthy", + "version": "test", + "endpoints": [ + "/ai/analyze-food-image/", + "/ai/analyze-food-image-with-weight/", + "/ai/health" + ] + } + +@app.post("/ai/analyze-food-image/") +async def analyze_food_image_endpoint(file: UploadFile = File(...)): + """測試食物圖片分析端點""" + if not file.content_type or not file.content_type.startswith("image/"): + raise HTTPException(status_code=400, detail="上傳的檔案不是圖片格式。") + + return {"food_name": "測試食物", "nutrition_info": {"calories": 100}} + +@app.post("/ai/analyze-food-image-with-weight/", response_model=WeightEstimationResponse) +async def analyze_food_image_with_weight_endpoint(file: UploadFile = File(...)): + """測試重量估算端點""" + if not file.content_type or not file.content_type.startswith("image/"): + raise HTTPException(status_code=400, detail="上傳的檔案不是圖片格式。") + + return WeightEstimationResponse( + food_type="測試食物", + estimated_weight=150.0, + weight_confidence=0.85, + weight_error_range=[130.0, 170.0], + nutrition={"calories": 100, "protein": 5, "fat": 2, "carbs": 15}, + reference_object="硬幣", + note="測試重量估算結果" + ) + +@app.get("/ai/health") +async def ai_health_check(): + """AI 服務健康檢查""" + return { + "status": "healthy", + "services": { + "food_classification": "available", + "weight_estimation": "available" + } + } \ No newline at end of file diff --git a/test_pytest.py b/test_pytest.py new file mode 100644 index 0000000000000000000000000000000000000000..95c5ab74551bf74921d0dddabb2907833f6791ff --- /dev/null +++ b/test_pytest.py @@ -0,0 +1,2 @@ +def test_example(): + assert 1 + 1 == 2 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..f26a2adc77d65196373b092598547d3d2ee90119 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,45 @@ +import pytest +from fastapi.testclient import TestClient +from backend.app.main import app +from unittest.mock import patch, MagicMock + +@pytest.fixture +def test_app(): + with TestClient(app) as test_client: + yield test_client + +@pytest.fixture +def sample_image(): + """Create a sample in-memory image for testing""" + from PIL import Image + from io import BytesIO + + # Create a simple red image + img = Image.new('RGB', (100, 100), color='red') + img_byte_arr = BytesIO() + img.save(img_byte_arr, format='JPEG') + img_byte_arr.seek(0) + + # Return the image data as bytes + return img_byte_arr.getvalue() + +@pytest.fixture +def mock_analyzer(): + with patch('transformers.AutoImageProcessor') as mock_processor, \ + patch('transformers.AutoModelForImageClassification') as mock_model, \ + patch('anthropic.Anthropic') as mock_anthropic: + + # Setup mock processor + mock_processor.from_pretrained.return_value = MagicMock() + + # Setup mock model + mock_model.from_pretrained.return_value = MagicMock() + + # Setup mock anthropic + mock_anthropic.return_value = MagicMock() + + yield ( + mock_processor.from_pretrained.return_value, + mock_model.from_pretrained.return_value, + mock_anthropic.return_value + ) diff --git a/tests/test_api/__init__.py b/tests/test_api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/test_api/test_main.py b/tests/test_api/test_main.py new file mode 100644 index 0000000000000000000000000000000000000000..48de39c526a6766ebb4ff4df91c4a9f2b849d573 --- /dev/null +++ b/tests/test_api/test_main.py @@ -0,0 +1,100 @@ +import pytest +import sys +import os +from pathlib import Path + +# Add the project root to the Python path +project_root = Path(__file__).parent.parent +sys.path.append(str(project_root)) + +from unittest.mock import patch, MagicMock +from fastapi import HTTPException +from fastapi.testclient import TestClient + +# Test the root endpoint +def test_read_root(test_app): + response = test_app.get("/") + assert response.status_code == 200 + assert response.json() == {"message": "Health Assistant API is running"} + +# Test food recognition endpoint +@patch('backend.app.services.food_analyzer_service.HybridFoodAnalyzer.analyze_food') +def test_recognize_food(mock_analyze_food, test_app, sample_image): + # Mock the response from the analyzer + mock_analyze_food.return_value = { + "food_name": "apple", + "chinese_name": "蘋果", + "confidence": 0.95, + "success": True + } + + # Create a test file + files = {"file": ("test_image.jpg", sample_image, "image/jpeg")} + + # Make the request + response = test_app.post("/recognize-food", files=files) + + # Check the response + assert response.status_code == 200 + data = response.json() + assert data["success"] is True + assert "food_name" in data + assert "chinese_name" in data + assert "confidence" in data + +# Test nutrition analysis endpoint +@patch('app.services.food_analyzer_service.HybridFoodAnalyzer.analyze_nutrition') +def test_analyze_nutrition(mock_analyze_nutrition, test_app): + # Mock the response from the analyzer + mock_analyze_nutrition.return_value = { + "nutrition": { + "calories": 95, + "protein": 0.5, + "fat": 0.3, + "carbohydrates": 25.0 + }, + "success": True + } + + # Test data + test_data = {"food_name": "apple"} + + # Make the request + response = test_app.post("/analyze-nutrition", json=test_data) + + # Check the response + assert response.status_code == 200 + data = response.json() + assert data["success"] is True + assert "nutrition" in data + assert "analysis" in data + +# Test error handling for invalid image +def test_recognize_food_invalid_file(test_app): + # Create an invalid file + files = {"file": ("test.txt", b"not an image", "text/plain")} + + # Make the request + response = test_app.post("/recognize-food", files=files) + + # Check the response + assert response.status_code == 400 + data = response.json() + assert data["detail"] == "Invalid image file" + +# Test error handling for missing food name +@patch('app.services.food_analyzer_service.HybridFoodAnalyzer.analyze_nutrition') +def test_analyze_nutrition_missing_food(mock_analyze_nutrition, test_app): + # Configure the mock to raise an exception + mock_analyze_nutrition.side_effect = ValueError("Food name is required") + + # Test data with missing food name + test_data = {} + + # Make the request + response = test_app.post("/analyze-nutrition", json=test_data) + + # Check the response + assert response.status_code == 400 + data = response.json() + assert "detail" in data diff --git a/tests/test_services/__init__.py b/tests/test_services/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/test_services/test_food_analyzer_service.py b/tests/test_services/test_food_analyzer_service.py new file mode 100644 index 0000000000000000000000000000000000000000..7fafef93a79547a5582a9a9a7184aa756db6d19c --- /dev/null +++ b/tests/test_services/test_food_analyzer_service.py @@ -0,0 +1,179 @@ +import pytest +from unittest.mock import patch, MagicMock, ANY +from PIL import Image +from io import BytesIO +import json +import sys +import os +import torch + +# Add project root to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) + +from backend.app.services.food_analyzer_service import HybridFoodAnalyzer + +# Test data +SAMPLE_NUTRITION_DATA = { + "calories": 95, + "protein": 0.5, + "fat": 0.3, + "carbohydrates": 25.0, + "fiber": 4.4, + "sugar": 19.0, + "sodium": 2, + "cholesterol": 0, + "saturated_fat": 0.1, + "calcium": 1, + "iron": 1, + "potassium": 195, + "vitamin_a": 2, + "vitamin_c": 14, + "vitamin_d": 0 +} + +SAMPLE_IMAGE = None + +def create_test_image(): + """Helper function to create a test image""" + img = Image.new('RGB', (100, 100), color='red') + img_byte_arr = BytesIO() + img.save(img_byte_arr, format='JPEG') + return img_byte_arr.getvalue() + +@pytest.fixture +def mock_analyzer(): + with patch('transformers.AutoImageProcessor.from_pretrained') as mock_processor, \ + patch('transformers.AutoModelForImageClassification.from_pretrained') as mock_model, \ + patch('anthropic.Anthropic') as mock_anthropic: + + # Setup mock processor + mock_processor.return_value = MagicMock() + + # Setup mock model + mock_model.return_value = MagicMock() + mock_model.return_value.config.id2label = {0: "apple"} + mock_model.return_value.return_value = MagicMock(logits=torch.tensor([[0.9, 0.1]])) + + # Setup mock anthropic + mock_anthropic.return_value = MagicMock() + mock_anthropic.return_value.messages.create.return_value.content = [ + MagicMock(text=json.dumps(SAMPLE_NUTRITION_DATA)) + ] + + analyzer = HybridFoodAnalyzer("test_api_key") + analyzer.processor = mock_processor.return_value + analyzer.model = mock_model.return_value + analyzer.claude_client = mock_anthropic.return_value + + yield analyzer + +def test_hybrid_food_analyzer_init(mock_analyzer): + """Test initialization of HybridFoodAnalyzer""" + assert mock_analyzer is not None + assert hasattr(mock_analyzer, 'processor') + assert hasattr(mock_analyzer, 'model') + assert hasattr(mock_analyzer, 'claude_client') + +def test_recognize_food_success(mock_analyzer): + """Test successful food recognition""" + # Create test image + img = Image.new('RGB', (100, 100), color='red') + img_byte_arr = BytesIO() + img.save(img_byte_arr, format='JPEG') + + # Call method + result = mock_analyzer.recognize_food(img_byte_arr.getvalue()) + + # Assertions + assert result["success"] is True + assert "food_name" in result + assert "chinese_name" in result + assert "confidence" in result + assert result["food_name"] == "apple" + assert result["chinese_name"] == "蘋果" + +def test_recognize_food_error(mock_analyzer): + """Test error handling in food recognition""" + # Setup mock to raise exception + mock_analyzer.model.side_effect = Exception("Test error") + + # Call method with invalid image + result = mock_analyzer.recognize_food(b"invalid_image") + + # Assertions + assert result["success"] is False + assert "error" in result + +def test_analyze_nutrition_success(mock_analyzer): + """Test successful nutrition analysis""" + # Call method + result = mock_analyzer.analyze_nutrition("apple") + + # Assertions + assert result["success"] is True + assert "nutrition" in result + assert result["nutrition"] == SAMPLE_NUTRITION_DATA + mock_analyzer.claude_client.messages.create.assert_called_once() + +def test_analyze_nutrition_error(mock_analyzer): + """Test error handling in nutrition analysis""" + # Setup mock to raise exception + mock_analyzer.claude_client.messages.create.side_effect = Exception("API error") + + # Call method + result = mock_analyzer.analyze_nutrition("invalid_food") + + # Assertions + assert result["success"] is False + assert "error" in result + +def test_process_image_success(mock_analyzer): + """Test successful image processing""" + # Setup + test_image = create_test_image() + + # Call method + result = mock_analyzer.process_image(test_image) + + # Assertions + assert result["success"] is True + assert "food_name" in result + assert "nutrition" in result + assert "analysis" in result + assert "healthScore" in result["analysis"] + assert "recommendations" in result["analysis"] + assert "warnings" in result["analysis"] + +def test_calculate_health_score(mock_analyzer): + """Test health score calculation""" + # Test with sample nutrition data + score = mock_analyzer.calculate_health_score(SAMPLE_NUTRITION_DATA) + + # Assert score is within expected range + assert isinstance(score, (int, float)) + assert 0 <= score <= 100 + +def test_generate_recommendations(mock_analyzer): + """Test generation of dietary recommendations""" + # Call method + recommendations = mock_analyzer.generate_recommendations(SAMPLE_NUTRITION_DATA) + + # Assertions + assert isinstance(recommendations, list) + assert all(isinstance(rec, str) for rec in recommendations) + +def test_generate_warnings(mock_analyzer): + """Test generation of dietary warnings""" + # Call method + warnings = mock_analyzer.generate_warnings(SAMPLE_NUTRITION_DATA) + + # Assertions + assert isinstance(warnings, list) + assert all(isinstance(warning, str) for warning in warnings) + +def test_calculate_health_score_incomplete_data(mock_analyzer): + """Test health score calculation with incomplete nutrition data""" + # Test with incomplete nutrition data + incomplete_nutrition = {"calories": 100} + health_score = mock_analyzer.calculate_health_score(incomplete_nutrition) + assert 0 <= health_score <= 100 diff --git a/tests/test_simple.py b/tests/test_simple.py new file mode 100644 index 0000000000000000000000000000000000000000..82c56be3d0801144b689b585331c55250878ffb2 --- /dev/null +++ b/tests/test_simple.py @@ -0,0 +1,2 @@ +def test_simple(): + assert 1 + 1 == 2 diff --git a/tests/test_verify.py b/tests/test_verify.py new file mode 100644 index 0000000000000000000000000000000000000000..d787d6e7a9a681b46c0be4a938a0dc9778c33456 --- /dev/null +++ b/tests/test_verify.py @@ -0,0 +1,5 @@ +def test_verify(): + """Simple test to verify pytest is working""" + print("\n=== Running verify test ===") + assert 1 + 1 == 2 + print("=== Verify test passed ===\n") diff --git a/updated_addToDiary.js b/updated_addToDiary.js new file mode 100644 index 0000000000000000000000000000000000000000..43f1e426257de200947d3dd1fd49bc33c44d9c83 --- /dev/null +++ b/updated_addToDiary.js @@ -0,0 +1,123 @@ +// 將食物加入飲食記錄 +function addToDiary(foodName) { + if (!foodName) { + console.warn('無法加入空的食物名稱'); + return false; + } + + try { + // 確保 dailyMeals 是陣列 + if (!Array.isArray(dailyMeals)) { + dailyMeals = []; + } + + const today = new Date().toISOString().split('T')[0]; + const now = new Date(); + + // 從營養資料庫獲取食物資訊,如果沒有則使用預設值 + const foodInfo = nutritionDatabase[foodName] || { + calories: 200, + protein: 10, + carbs: 30, + fat: 5, + fiber: 2, + sugar: 5 + }; + + const meal = { + id: Date.now(), + name: foodName, + ...foodInfo, // 展開營養資訊 + date: today, + time: now.toLocaleTimeString('zh-TW', {hour: '2-digit', minute:'2-digit'}), + timestamp: now.toISOString() + }; + + // 加入飲食記錄 + dailyMeals.push(meal); + + // 儲存到 localStorage + localStorage.setItem('dailyMeals', JSON.stringify(dailyMeals)); + + // 顯示成功訊息 + showNotification(`已將「${foodName}」加入飲食記錄`, 'success'); + + // 更新 UI + if (typeof updateMealsList === 'function') { + updateMealsList(); + } + + if (typeof updateTrackingStats === 'function') { + updateTrackingStats(); + } + + // 觸發自定義事件,通知其他組件 + document.dispatchEvent(new CustomEvent('mealAdded', { detail: meal })); + + return true; + + } catch (error) { + console.error('加入飲食記錄失敗:', error); + showNotification('加入飲食記錄時發生錯誤', 'error'); + return false; + } +} + +// 顯示通知訊息 +function showNotification(message, type = 'info') { + // 檢查是否已經存在通知容器 + let notificationContainer = document.getElementById('notification-container'); + + if (!notificationContainer) { + // 創建通知容器 + notificationContainer = document.createElement('div'); + notificationContainer.id = 'notification-container'; + notificationContainer.style.position = 'fixed'; + notificationContainer.style.top = '20px'; + notificationContainer.style.right = '20px'; + notificationContainer.style.zIndex = '1000'; + document.body.appendChild(notificationContainer); + } + + // 創建通知元素 + const notification = document.createElement('div'); + notification.className = `notification ${type}`; + notification.style.padding = '12px 20px'; + notification.style.marginBottom = '10px'; + notification.style.borderRadius = '4px'; + notification.style.color = 'white'; + notification.style.opacity = '0'; + notification.style.transition = 'opacity 0.3s ease-in-out'; + notification.style.boxShadow = '0 2px 10px rgba(0,0,0,0.1)'; + + // 根據類型設置背景色 + const colors = { + success: '#4CAF50', + error: '#F44336', + warning: '#FF9800', + info: '#2196F3' + }; + + notification.style.backgroundColor = colors[type] || colors.info; + notification.textContent = message; + + // 添加到容器 + notificationContainer.appendChild(notification); + + // 觸發動畫 + setTimeout(() => { + notification.style.opacity = '1'; + }, 10); + + // 3秒後自動移除 + setTimeout(() => { + notification.style.opacity = '0'; + setTimeout(() => { + notification.remove(); + // 如果容器為空,則移除容器 + if (notificationContainer && notificationContainer.children.length === 0) { + notificationContainer.remove(); + } + }, 300); + }, 3000); +}