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食物營養分析器 - 個人化版本
+
+
+
+
+
+
+
+
+ 分析食物
+ 個人資料
+ 營養追蹤
+
+
+
+
+
+
請先到「個人資料」頁面完成設定,以獲得個人化營養建議和追蹤功能 👆
+
+
+
+
+
+
+
+
+ 📷 開啟相機
+ 📸 拍照分析
+ 📁 上傳圖片
+
+
+
+
+
+
+
+
識別的食物
+ 加入飲食記錄
+
+
+
+
+
這裡會顯示食物的詳細描述和營養價值。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
請先到「個人資料」頁面完成設定,以獲得個人化營養建議和追蹤功能 👆
+
+
+
+
+
+
+
\ 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食物營養分析器
+
智能分析您的飲食營養
+
+
+
+ switchTab('analyzer')}>分析食物
+ switchTab('profile')}>個人資料
+ switchTab('tracking')}>營養追蹤
+
+
+ {tab === 'analyzer' && (
+
+
+ {/* 預覽圖片優先顯示,否則顯示相機畫面 */}
+ {previewImage ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ 📷 開啟相機
+ 📸 拍照分析
+ fileInputRef.current && fileInputRef.current.click()}>📁 上傳圖片
+
+
+
+ {loading && (
+
+ )}
+
+ {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])=>(
+
+ ))}
+
+
+
+
+
維生素
+
+ {result.vitamins && Object.entries(result.vitamins).map(([k,v])=>(
+
+ ))}
+
+
+
+
+
礦物質
+
+ {result.minerals && Object.entries(result.minerals).map(([k,v])=>(
+
+ ))}
+
+
+
+
+ {profile && (
+
+
💡 個人化建議
+
+ 這份食物約佔您每日熱量建議的 {Math.round((result.nutrition?.calories / profile.dailyCalories) * 100)}%。
+
+
+ )}
+
+ )}
+
+ )}
+
+ {tab === 'profile' && (
+
+ )}
+
+ {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 (
+
+
+ {/* 上方切換選單間隔 */}
+
+
+ {/* 用戶卡片 */}
+
+
+
+ 用戶資料
+
+
+ {userCreated && (
+ <>
+ 用戶ID:6
+ setUserCreated(false)}>變更用戶資料
+
+
姓名:{user.name}
+
年齡:{user.age}
+
身高:{user.height} cm
+
體重:{user.weight} kg
+
+ >
+ )}
+
+ {/* 飲食紀錄卡片 */}
+
+
+
+ 飲食紀錄
+
+
+ {foodMsg && {foodMsg}
}
+
+ {/* BMI 卡片 */}
+
+
+
+ BMI 與建議
+
+ 查詢 BMI
+ {bmi && BMI:{bmi}
}
+ {bmiMsg && {bmiMsg}
}
+
+ {/* 水分卡片 */}
+
+
+
+ 水分攝取
+
+
+ {waterMsg && {waterMsg}
}
+
+ {/* AI 卡片 */}
+
+ {/* 歷史紀錄卡片 */}
+
+
+ 查詢所有歷史紀錄
+
+
+
+
+ 日期
+ 熱量
+ 碳水
+ 蛋白質
+
+
+
+ {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 (
+
+
+
記錄運動
+
+
+
+
+
+ 運動類型
+
+ setSelectedExercise(e.target.value)}
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
+ >
+ 選擇運動類型
+ {exerciseTypes.map(type => (
+ {type.name}
+ ))}
+
+
+
+
+
+ 運動時間 (分鐘)
+
+ 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
+
+ )}
+
+
+
+ 新增記錄
+
+
+
+
+
本週運動統計
+
+
+
+
+ 消耗卡路里
+ 1,200 kcal
+
+
+
+
+
+
+
+
+ {/* 運動記錄 */}
+
+
最近的運動記錄
+
+
+
+
+ 日期
+ 運動類型
+ 時間 (分鐘)
+ 消耗卡路里
+
+
+
+
+ 2025-04-21
+ 跑步
+ 30
+ 300
+
+
+ 2025-04-20
+ 重訓
+ 45
+ 270
+
+
+
+
+
+
+ );
+}
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食物營養分析器
+
智能分析您的飲食營養
+
+
+ {/* 標籤導航 */}
+
+
+ setActiveTab('analyzer')}
+ >
+ 分析食物
+
+ setActiveTab('profile')}
+ >
+ 個人資料
+
+ setActiveTab('tracking')}
+ >
+ 營養追蹤
+
+
+
+
+ {/* 內容區域 */}
+
+ {/* 食物分析頁面 */}
+ {activeTab === 'analyzer' && (
+
+ {!userProfile.name && (
+
+
+ 請先到「個人資料」頁面完成設定,以獲得個人化營養建議和追蹤功能 👆
+
+
+ )}
+
+
+ {/* 相機區域 */}
+
+
+
+
+
+ {selectedImage && (
+
+
+
{
+ setSelectedImage(null);
+ setAnalysisResults(null);
+ }}
+ className="absolute top-2 right-2 bg-red-500 text-white rounded-full p-2 hover:bg-red-600"
+ >
+
+
+
+ )}
+
+ {!selectedImage && (
+
+
📷
+
上傳食物照片
+
支援 JPG、PNG 格式,最大 5MB
+
+ )}
+
+
+ {/* 控制按鈕 */}
+
+
+
+ 開啟相機
+
+
+
+ 拍照分析
+
+
+
+ 上傳圖片
+
+
+
+
+
+ {/* 載入中 */}
+ {isAnalyzing && (
+
+ )}
+
+ {/* 分析結果 */}
+ {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]) => (
+
+ ))}
+
+
+ )}
+ {analysisResults.minerals && (
+
+
礦物質
+
+ {Object.entries(analysisResults.minerals).map(([key, value]) => (
+
+ ))}
+
+
+ )}
+
+ )}
+
+ {/* 個人化建議 */}
+ {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('gender', 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"
+ >
+ 請選擇
+ 男性
+ 女性
+
+
+
+
+
+
+
+
+ 活動量
+ handleProfileChange('activityLevel', 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"
+ >
+ 久坐少動
+ 輕度活動
+ 中度活動
+ 高度活動
+ 超高活動
+
+
+
+ 健康目標
+ handleProfileChange('goal', 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"
+ >
+ 減重
+ 維持體重
+ 增重
+
+
+
+
+
+ 💾 儲存個人資料
+
+
+
+
+ )}
+
+ {/* 營養追蹤頁面 */}
+ {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
+
+
+
+
+ {/* 快速添加按鈕 */}
+
+
addWater(200)}
+ className="flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
+ >
+
+ 200ml
+
+
addWater(300)}
+ className="flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
+ >
+
+ 300ml
+
+
addWater(500)}
+ className="flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
+ >
+
+ 500ml
+
+
removeWater(200)}
+ className="flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700"
+ >
+
+ 取消
+
+
+
+ {/* 自定義輸入 */}
+
+
+
+ 自定義添加量 (ml)
+
+
+
+
+ 添加
+
+
+
+
+ {/* 今日記錄 */}
+
+
今日記錄
+
+
+
+
+ 時間
+ 數量 (ml)
+
+
+
+
+ 14:30
+ 300
+
+
+ 12:00
+ 500
+
+
+ 09:15
+ 200
+
+
+
+
+
+
+ );
+}
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ԑú~H2FNFX]-mĖΒԜbXT
+qe!ZΕ2Hb֢%`B-n4\.nl.6[Qra4̛_,A'1&ZFERo9.?%Bm&RS
+~8q*>warn)tw&:O$t_WPJc3yS E!Pj![b⎽gJKcȣd[&@+8㣝;,o_ߟ}3$ia{K-"ȢT2Cy}q(IU_
SHdmov
\ 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#;0GsSki:Ӊ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['wOI
\ 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 @@
+x10E9EHB%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 @@
+xmOO09SX=p1iIHm*
m4 I:mT@|+-a
clY-<àUX"62Bj%f'LBk#|0WJ(C&*'U9v2la;@^Rr?Đ3:nx+/;R<"1SNQwlJTZy2#99qfw2f
IW&N(f[e|_᠒|bao
\ No newline at end of file
diff --git a/health_assistant/objects/c6/24114db74317ff5127d25f81440ac813888b19 b/health_assistant/objects/c6/24114db74317ff5127d25f81440ac813888b19
new file mode 100644
index 0000000000000000000000000000000000000000..b3708a7a545d5deb1fdc2c81636607087dc533b1
--- /dev/null
+++ b/health_assistant/objects/c6/24114db74317ff5127d25f81440ac813888b19
@@ -0,0 +1 @@
+xUPN@_aD"E8phuUyMZŚyvbO/75M)N*(E2&Nf/(Е$0*}R.N&#q+Sɠt6`$v"8#{2+a{H4MJ ވy,|D~Cq)ePvӖ~iz:4^uIih=Gj0y}
i?
\ No newline at end of file
diff --git a/health_assistant/objects/c6/7360d78ba1f1bfb12f070c014df7618f6e88bb b/health_assistant/objects/c6/7360d78ba1f1bfb12f070c014df7618f6e88bb
new file mode 100644
index 0000000000000000000000000000000000000000..3a47ab392eae6bedfc15b31d6d2056baf37c9ce6
--- /dev/null
+++ b/health_assistant/objects/c6/7360d78ba1f1bfb12f070c014df7618f6e88bb
@@ -0,0 +1,2 @@
+xUn0D;+d N~@,1PY$ƭxstj^1C At"nI~lՋTy9~8:g]gޤ'.w~Ocd6&r^.Ө>m,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*YXRe: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);
+}