Deploy Medical Diagnosis AI - Full Stack Application
Browse filesFeatures:
- React frontend with Vite
- FastAPI backend with smart predictions
- ML model (Gradient Boosting) for disease diagnosis
- 16-feature medical assessment
- Professional UI with glassmorphism design
- Patient Data Report with all metrics
- Docker container with non-root user (UID 1000)
- Serves both API and static frontend on port 7860
Note: node_modules will be built during Docker build via npm install
This keeps the repository size manageable (~20MB vs 500MB+)
- .dockerignore +81 -0
- .gitignore +43 -0
- Dockerfile +78 -0
- PUSH_TO_HF_SPACES.md +178 -0
- app/__init__.py +4 -0
- app/api.py +266 -0
- app/config.py +93 -0
- app/main.py +923 -0
- app/memory.py +97 -0
- app/schemas.py +146 -0
- app/services/__init__.py +12 -0
- app/services/feature_builder.py +122 -0
- app/services/llm_extractor.py +508 -0
- app/services/predictor.py +146 -0
- app/services/session_manager.py +178 -0
- app/utils/__init__.py +5 -0
- app/utils/helpers.py +56 -0
- frontend/index.html +13 -0
- frontend/package-lock.json +2720 -0
- frontend/package.json +26 -0
- frontend/postcss.config.js +6 -0
- frontend/src/App.jsx +67 -0
- frontend/src/components/ChatWindow.jsx +46 -0
- frontend/src/components/DiagnosisCard.jsx +123 -0
- frontend/src/components/FeatureSidebar.jsx +105 -0
- frontend/src/components/FeatureTooltip.jsx +138 -0
- frontend/src/components/InputBar.jsx +79 -0
- frontend/src/components/MessageBubble.jsx +25 -0
- frontend/src/components/MetricItem.jsx +130 -0
- frontend/src/hooks/useChat.js +84 -0
- frontend/src/index.css +41 -0
- frontend/src/main.jsx +10 -0
- frontend/tailwind.config.js +18 -0
- frontend/vite.config.js +14 -0
- models/GradientBoosting_model.pkl +3 -0
- requirements.txt +11 -0
- server.py +74 -0
.dockerignore
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Git
|
| 2 |
+
.git
|
| 3 |
+
.gitignore
|
| 4 |
+
.gitattributes
|
| 5 |
+
|
| 6 |
+
# Documentation
|
| 7 |
+
*.md
|
| 8 |
+
README.md
|
| 9 |
+
LICENSE
|
| 10 |
+
|
| 11 |
+
# Development
|
| 12 |
+
.env
|
| 13 |
+
.env.local
|
| 14 |
+
.env.*.local
|
| 15 |
+
|
| 16 |
+
# Python
|
| 17 |
+
__pycache__
|
| 18 |
+
*.pyc
|
| 19 |
+
*.pyo
|
| 20 |
+
*.pyd
|
| 21 |
+
.Python
|
| 22 |
+
env/
|
| 23 |
+
venv/
|
| 24 |
+
.venv
|
| 25 |
+
pip-log.txt
|
| 26 |
+
pip-delete-this-directory.txt
|
| 27 |
+
.tox/
|
| 28 |
+
.coverage
|
| 29 |
+
.coverage.*
|
| 30 |
+
.cache
|
| 31 |
+
nosetests.xml
|
| 32 |
+
coverage.xml
|
| 33 |
+
*.cover
|
| 34 |
+
.hypothesis/
|
| 35 |
+
.pytest_cache/
|
| 36 |
+
*.egg-info/
|
| 37 |
+
dist/
|
| 38 |
+
build/
|
| 39 |
+
|
| 40 |
+
# Node
|
| 41 |
+
node_modules/
|
| 42 |
+
npm-debug.log*
|
| 43 |
+
yarn-debug.log*
|
| 44 |
+
yarn-error.log*
|
| 45 |
+
.npm
|
| 46 |
+
.eslintcache
|
| 47 |
+
|
| 48 |
+
# IDE
|
| 49 |
+
.vscode
|
| 50 |
+
.idea
|
| 51 |
+
*.swp
|
| 52 |
+
*.swo
|
| 53 |
+
*~
|
| 54 |
+
.DS_Store
|
| 55 |
+
|
| 56 |
+
# Frontend build artifacts (will be rebuilt)
|
| 57 |
+
frontend/dist
|
| 58 |
+
frontend/.vite
|
| 59 |
+
|
| 60 |
+
# Tests
|
| 61 |
+
test_*.py
|
| 62 |
+
tests/
|
| 63 |
+
*.test.js
|
| 64 |
+
|
| 65 |
+
# Sessions and cache
|
| 66 |
+
/tmp
|
| 67 |
+
sessions/
|
| 68 |
+
*.json
|
| 69 |
+
.claude*
|
| 70 |
+
|
| 71 |
+
# Docker
|
| 72 |
+
.dockerignore
|
| 73 |
+
Dockerfile
|
| 74 |
+
|
| 75 |
+
# CI/CD
|
| 76 |
+
.github
|
| 77 |
+
.gitlab-ci.yml
|
| 78 |
+
|
| 79 |
+
# Misc
|
| 80 |
+
*.log
|
| 81 |
+
.next
|
.gitignore
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Node
|
| 2 |
+
node_modules/
|
| 3 |
+
npm-debug.log*
|
| 4 |
+
yarn-debug.log*
|
| 5 |
+
yarn-error.log*
|
| 6 |
+
.npm
|
| 7 |
+
.eslintcache
|
| 8 |
+
|
| 9 |
+
# Python
|
| 10 |
+
__pycache__/
|
| 11 |
+
*.pyc
|
| 12 |
+
*.pyo
|
| 13 |
+
*.pyd
|
| 14 |
+
.Python
|
| 15 |
+
env/
|
| 16 |
+
venv/
|
| 17 |
+
.venv
|
| 18 |
+
pip-log.txt
|
| 19 |
+
.tox/
|
| 20 |
+
.coverage
|
| 21 |
+
.pytest_cache/
|
| 22 |
+
*.egg-info/
|
| 23 |
+
|
| 24 |
+
# Build artifacts
|
| 25 |
+
frontend/dist/
|
| 26 |
+
frontend/.vite/
|
| 27 |
+
|
| 28 |
+
# IDE
|
| 29 |
+
.vscode
|
| 30 |
+
.idea
|
| 31 |
+
*.swp
|
| 32 |
+
*.swo
|
| 33 |
+
|
| 34 |
+
# Environment
|
| 35 |
+
.env
|
| 36 |
+
.env.local
|
| 37 |
+
|
| 38 |
+
# Sessions
|
| 39 |
+
/tmp
|
| 40 |
+
sessions/
|
| 41 |
+
|
| 42 |
+
# OS
|
| 43 |
+
.DS_Store
|
Dockerfile
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Multi-stage build for Medical Diagnosis AI
|
| 2 |
+
# Stage 1: Build React frontend with Node.js
|
| 3 |
+
# Stage 2: Python backend serving both API and static files
|
| 4 |
+
|
| 5 |
+
# ============================================================
|
| 6 |
+
# Stage 1: Build React Frontend
|
| 7 |
+
# ============================================================
|
| 8 |
+
FROM node:18-slim AS frontend-builder
|
| 9 |
+
|
| 10 |
+
WORKDIR /build/frontend
|
| 11 |
+
|
| 12 |
+
# Copy frontend source code
|
| 13 |
+
COPY frontend/package*.json ./
|
| 14 |
+
RUN npm install --frozen-lockfile
|
| 15 |
+
|
| 16 |
+
# Copy frontend source
|
| 17 |
+
COPY frontend/ .
|
| 18 |
+
|
| 19 |
+
# Build the frontend (creates dist/ folder)
|
| 20 |
+
RUN npm run build
|
| 21 |
+
|
| 22 |
+
# ============================================================
|
| 23 |
+
# Stage 2: Python Backend with Static File Serving
|
| 24 |
+
# ============================================================
|
| 25 |
+
FROM python:3.10-slim
|
| 26 |
+
|
| 27 |
+
# Set metadata
|
| 28 |
+
LABEL maintainer="Medical Diagnosis AI Team"
|
| 29 |
+
LABEL description="Medical Diagnosis AI - Full Stack Application"
|
| 30 |
+
|
| 31 |
+
# Set environment variables
|
| 32 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 33 |
+
PYTHONDONTWRITEBYTECODE=1 \
|
| 34 |
+
PIP_NO_CACHE_DIR=1 \
|
| 35 |
+
PIP_DISABLE_PIP_VERSION_CHECK=1
|
| 36 |
+
|
| 37 |
+
# Create non-root user with UID 1000 (Hugging Face Spaces requirement)
|
| 38 |
+
RUN groupadd -r user && useradd -r -u 1000 -g user user
|
| 39 |
+
|
| 40 |
+
# Set working directory
|
| 41 |
+
WORKDIR /home/user/app
|
| 42 |
+
|
| 43 |
+
# Copy requirements and install Python dependencies
|
| 44 |
+
COPY requirements.txt .
|
| 45 |
+
RUN pip install --upgrade pip && \
|
| 46 |
+
pip install -r requirements.txt
|
| 47 |
+
|
| 48 |
+
# Install curl for health checks
|
| 49 |
+
RUN apt-get update && \
|
| 50 |
+
apt-get install -y --no-install-recommends curl && \
|
| 51 |
+
rm -rf /var/lib/apt/lists/*
|
| 52 |
+
|
| 53 |
+
# Copy backend application code
|
| 54 |
+
COPY app/ ./app/
|
| 55 |
+
COPY server.py .
|
| 56 |
+
|
| 57 |
+
# Copy the ML model (CRITICAL - needed for disease predictions)
|
| 58 |
+
COPY models/ ./models/
|
| 59 |
+
|
| 60 |
+
# Create frontend dist directory and copy built frontend from Stage 1
|
| 61 |
+
RUN mkdir -p frontend
|
| 62 |
+
COPY --from=frontend-builder /build/frontend/dist ./frontend/dist
|
| 63 |
+
|
| 64 |
+
# Change ownership of all files to the non-root user
|
| 65 |
+
RUN chown -R user:user /home/user/app
|
| 66 |
+
|
| 67 |
+
# Switch to non-root user
|
| 68 |
+
USER user
|
| 69 |
+
|
| 70 |
+
# Expose port 7860 (Hugging Face Spaces standard)
|
| 71 |
+
EXPOSE 7860
|
| 72 |
+
|
| 73 |
+
# Health check
|
| 74 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
| 75 |
+
CMD curl -f http://localhost:7860/health || exit 1
|
| 76 |
+
|
| 77 |
+
# Run the server
|
| 78 |
+
CMD ["python", "server.py"]
|
PUSH_TO_HF_SPACES.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Push to Hugging Face Spaces - Final Step
|
| 2 |
+
|
| 3 |
+
You're almost done! Your files are committed and ready to push. Follow these steps:
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## π Step 1: Authenticate with Hugging Face
|
| 8 |
+
|
| 9 |
+
### Option A: Using HF Token (Recommended)
|
| 10 |
+
|
| 11 |
+
**Get your token:**
|
| 12 |
+
1. Go to: https://huggingface.co/settings/tokens
|
| 13 |
+
2. Create a new token if you don't have one
|
| 14 |
+
3. Copy the token
|
| 15 |
+
|
| 16 |
+
**Set it up in your terminal:**
|
| 17 |
+
|
| 18 |
+
```bash
|
| 19 |
+
# Option 1: Set as environment variable (temporary)
|
| 20 |
+
export HF_TOKEN=your_actual_token_here
|
| 21 |
+
git push
|
| 22 |
+
|
| 23 |
+
# Option 2: Use git credential helper (persistent)
|
| 24 |
+
git config --global credential.helper store
|
| 25 |
+
# When prompted, enter:
|
| 26 |
+
# Username: your_huggingface_username
|
| 27 |
+
# Password: your_HF_TOKEN
|
| 28 |
+
git push
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
### Option B: Using SSH (Alternative)
|
| 32 |
+
|
| 33 |
+
If you have SSH keys set up on HF:
|
| 34 |
+
|
| 35 |
+
```bash
|
| 36 |
+
git remote set-url origin git@huggingface.co:spaces/zunayed02/MediHelp.git
|
| 37 |
+
git push
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## π Step 2: Push to Your Space
|
| 43 |
+
|
| 44 |
+
Once authenticated:
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
cd /media/zunayed/HDD_code/chatbot\ with\ llm/MediHelp
|
| 48 |
+
git push
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
**Expected output:**
|
| 52 |
+
```
|
| 53 |
+
Counting objects: ...
|
| 54 |
+
Writing objects: ...
|
| 55 |
+
Total ... (delta ...)
|
| 56 |
+
remote: Scanning for {content}...
|
| 57 |
+
To https://huggingface.co/spaces/zunayed02/MediHelp
|
| 58 |
+
31515be..0917c28 main -> main
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
## β³ Step 3: Wait for Build
|
| 64 |
+
|
| 65 |
+
After pushing:
|
| 66 |
+
|
| 67 |
+
1. **Go to your Space:** https://huggingface.co/spaces/zunayed02/MediHelp
|
| 68 |
+
2. **Click "Build" tab** to watch the build progress
|
| 69 |
+
3. **Build takes:** 5-15 minutes
|
| 70 |
+
4. **Watch for:**
|
| 71 |
+
- β
Stage 1: Frontend build
|
| 72 |
+
- β
Stage 2: Backend setup
|
| 73 |
+
- β
Health check: Pass
|
| 74 |
+
|
| 75 |
+
---
|
| 76 |
+
|
| 77 |
+
## βοΈ Step 4: Configure GROQ_API_KEY (CRITICAL!)
|
| 78 |
+
|
| 79 |
+
**After build completes:**
|
| 80 |
+
|
| 81 |
+
1. Go to your Space: https://huggingface.co/spaces/zunayed02/MediHelp
|
| 82 |
+
2. Click **Settings** (gear icon)
|
| 83 |
+
3. Click **Secrets**
|
| 84 |
+
4. Add new secret:
|
| 85 |
+
- **Key:** `GROQ_API_KEY`
|
| 86 |
+
- **Value:** [Your actual Groq API key](https://console.groq.com)
|
| 87 |
+
5. Save
|
| 88 |
+
|
| 89 |
+
**Without this, the app won't work!**
|
| 90 |
+
|
| 91 |
+
---
|
| 92 |
+
|
| 93 |
+
## β
Step 5: Test Your Live App
|
| 94 |
+
|
| 95 |
+
Once build is complete and secrets are set:
|
| 96 |
+
|
| 97 |
+
1. **Reload the Space:** F5 or refresh
|
| 98 |
+
2. **Test the app:**
|
| 99 |
+
- β
Frontend loads
|
| 100 |
+
- β
Can enter health data
|
| 101 |
+
- β
Gets diagnosis results
|
| 102 |
+
- β
Shows Patient Data Report
|
| 103 |
+
|
| 104 |
+
---
|
| 105 |
+
|
| 106 |
+
## π Current Status
|
| 107 |
+
|
| 108 |
+
| Item | Status |
|
| 109 |
+
|------|--------|
|
| 110 |
+
| **Files committed** | β
Done |
|
| 111 |
+
| **Need to push** | β³ Next step |
|
| 112 |
+
| **Get HF token** | β³ Next step |
|
| 113 |
+
| **Push to Space** | β³ Next step |
|
| 114 |
+
| **Wait for build** | β³ Then |
|
| 115 |
+
| **Add GROQ_API_KEY** | β³ Then |
|
| 116 |
+
| **Test app** | β³ Final |
|
| 117 |
+
|
| 118 |
+
---
|
| 119 |
+
|
| 120 |
+
## π― Quick Commands
|
| 121 |
+
|
| 122 |
+
```bash
|
| 123 |
+
# Navigate to MediHelp
|
| 124 |
+
cd /media/zunayed/HDD_code/chatbot\ with\ llm/MediHelp
|
| 125 |
+
|
| 126 |
+
# Check what's committed
|
| 127 |
+
git log --oneline -3
|
| 128 |
+
|
| 129 |
+
# Set HF token (temporary)
|
| 130 |
+
export HF_TOKEN=your_token_here
|
| 131 |
+
|
| 132 |
+
# Push
|
| 133 |
+
git push
|
| 134 |
+
|
| 135 |
+
# Verify
|
| 136 |
+
git status
|
| 137 |
+
# Should show: "Your branch is up to date with 'origin/main'"
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
---
|
| 141 |
+
|
| 142 |
+
## β οΈ Troubleshooting
|
| 143 |
+
|
| 144 |
+
### "No such device or address"
|
| 145 |
+
- No internet connection
|
| 146 |
+
- Network issue
|
| 147 |
+
- Token not set
|
| 148 |
+
|
| 149 |
+
**Solution:** Set token and try again
|
| 150 |
+
|
| 151 |
+
### "Permission denied"
|
| 152 |
+
- Wrong token
|
| 153 |
+
- Wrong username
|
| 154 |
+
- Invalid credentials
|
| 155 |
+
|
| 156 |
+
**Solution:** Verify token at https://huggingface.co/settings/tokens
|
| 157 |
+
|
| 158 |
+
### Build fails in HF Spaces
|
| 159 |
+
- Check the build logs
|
| 160 |
+
- Most likely causes:
|
| 161 |
+
- Docker issue (shouldn't happen - we tested)
|
| 162 |
+
- Missing environment variable (GROQ_API_KEY)
|
| 163 |
+
|
| 164 |
+
---
|
| 165 |
+
|
| 166 |
+
## π Next Steps After Push
|
| 167 |
+
|
| 168 |
+
1. **Watch build logs** (should succeed)
|
| 169 |
+
2. **Add GROQ_API_KEY** to Space secrets
|
| 170 |
+
3. **Test the app** at your Space URL
|
| 171 |
+
4. **Share with friends!**
|
| 172 |
+
|
| 173 |
+
---
|
| 174 |
+
|
| 175 |
+
**Questions?** Check the deployment guides in the medical-predictor-chatbot directory:
|
| 176 |
+
- `DEPLOYMENT_GO_NO_GO.md` - Overview
|
| 177 |
+
- `HUGGING_FACE_DEPLOYMENT.md` - Full guide
|
| 178 |
+
- `DEPLOYMENT_READINESS_AUDIT.md` - Technical details
|
app/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Medical Predictor Chatbot Application"""
|
| 2 |
+
|
| 3 |
+
__version__ = "1.0.0"
|
| 4 |
+
__author__ = "Medical AI Team"
|
app/api.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FastAPI REST API layer wrapping the existing medical chatbot logic.
|
| 3 |
+
Converts the Gradio interface to a REST API for React frontend integration.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import hashlib
|
| 7 |
+
import logging
|
| 8 |
+
from fastapi import FastAPI, HTTPException
|
| 9 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 10 |
+
from pydantic import BaseModel
|
| 11 |
+
from typing import Optional, Dict, Any
|
| 12 |
+
import json
|
| 13 |
+
|
| 14 |
+
from app.main import (
|
| 15 |
+
chat_fn,
|
| 16 |
+
_load_state,
|
| 17 |
+
_save_state,
|
| 18 |
+
_parse_response,
|
| 19 |
+
_get_question_hint,
|
| 20 |
+
)
|
| 21 |
+
from app.services.llm_extractor import extract_features_from_text
|
| 22 |
+
from app.services.feature_builder import count_collected_features, is_ready_for_prediction, prepare_feature_vector
|
| 23 |
+
from app.services.predictor import get_predictor
|
| 24 |
+
from app.memory import initialize_state, update_state, get_missing_features
|
| 25 |
+
from app.config import DEFAULT_MODEL_FEATURES, MIN_FEATURES_FOR_PREDICTION, CLASS_NAMES
|
| 26 |
+
from app.utils.helpers import generate_question, prioritize_features
|
| 27 |
+
from app.services.session_manager import get_session_manager
|
| 28 |
+
|
| 29 |
+
logger = logging.getLogger(__name__)
|
| 30 |
+
|
| 31 |
+
# Initialize FastAPI
|
| 32 |
+
app = FastAPI(title="Medical Diagnosis AI", description="REST API for medical health prediction")
|
| 33 |
+
|
| 34 |
+
# CORS middleware
|
| 35 |
+
app.add_middleware(
|
| 36 |
+
CORSMiddleware,
|
| 37 |
+
allow_origins=["*"],
|
| 38 |
+
allow_credentials=True,
|
| 39 |
+
allow_methods=["*"],
|
| 40 |
+
allow_headers=["*"],
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
# Session manager
|
| 44 |
+
session_manager = get_session_manager()
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# ===== Request/Response Models =====
|
| 48 |
+
|
| 49 |
+
class ChatRequest(BaseModel):
|
| 50 |
+
"""Chat message request"""
|
| 51 |
+
session_id: Optional[str] = None
|
| 52 |
+
message: str
|
| 53 |
+
history: list = []
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class ChatResponse(BaseModel):
|
| 57 |
+
"""Chat response with features and state"""
|
| 58 |
+
session_id: str
|
| 59 |
+
message: str
|
| 60 |
+
features: Dict[str, Any]
|
| 61 |
+
collected_count: int
|
| 62 |
+
total_features: int = 16
|
| 63 |
+
is_complete: bool
|
| 64 |
+
prediction: Optional[Dict[str, Any]] = None
|
| 65 |
+
hint: str = ""
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class ResetRequest(BaseModel):
|
| 69 |
+
"""Reset session request"""
|
| 70 |
+
session_id: str
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class SessionStateResponse(BaseModel):
|
| 74 |
+
"""Session state response"""
|
| 75 |
+
session_id: str
|
| 76 |
+
features: Dict[str, Any]
|
| 77 |
+
collected_count: int
|
| 78 |
+
total_features: int = 16
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# ===== Helper Functions =====
|
| 82 |
+
|
| 83 |
+
def _build_acknowledgment(extracted: dict) -> str:
|
| 84 |
+
"""Build acknowledgment message from extracted features"""
|
| 85 |
+
extracted_items = []
|
| 86 |
+
for feature, value in extracted.items():
|
| 87 |
+
if value is not None and feature in DEFAULT_MODEL_FEATURES:
|
| 88 |
+
extracted_items.append(f"{feature}: {value}")
|
| 89 |
+
|
| 90 |
+
if extracted_items:
|
| 91 |
+
return f"β Got your {', '.join(extracted_items[:2])}"
|
| 92 |
+
return ""
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
# ===== API Endpoints =====
|
| 96 |
+
|
| 97 |
+
@app.get("/health")
|
| 98 |
+
def health_check():
|
| 99 |
+
"""Health check endpoint"""
|
| 100 |
+
return {"status": "ok", "service": "Medical Diagnosis AI"}
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
@app.post("/api/chat", response_model=ChatResponse)
|
| 104 |
+
def chat_endpoint(req: ChatRequest):
|
| 105 |
+
"""
|
| 106 |
+
Send a message and get AI response with updated features.
|
| 107 |
+
|
| 108 |
+
Handles:
|
| 109 |
+
- Session ID generation if not provided
|
| 110 |
+
- Feature extraction from user message
|
| 111 |
+
- State persistence
|
| 112 |
+
- Prediction when all 16 features collected
|
| 113 |
+
"""
|
| 114 |
+
try:
|
| 115 |
+
# Generate or use session ID
|
| 116 |
+
if req.session_id:
|
| 117 |
+
session_id = req.session_id
|
| 118 |
+
else:
|
| 119 |
+
# Generate from first message hash
|
| 120 |
+
session_id = "sess_" + hashlib.md5(req.message.encode()).hexdigest()[:8]
|
| 121 |
+
logger.info(f"π Created new session: {session_id}")
|
| 122 |
+
|
| 123 |
+
# Load persisted state
|
| 124 |
+
state = _load_state(session_id)
|
| 125 |
+
|
| 126 |
+
# Extract features from user message
|
| 127 |
+
extracted = extract_features_from_text(req.message)
|
| 128 |
+
|
| 129 |
+
# Update state with extracted features
|
| 130 |
+
state = update_state(state, extracted)
|
| 131 |
+
|
| 132 |
+
# Count collected features
|
| 133 |
+
collected = count_collected_features(state)
|
| 134 |
+
missing = get_missing_features(state)
|
| 135 |
+
|
| 136 |
+
# Check if ready for prediction
|
| 137 |
+
if is_ready_for_prediction(state, MIN_FEATURES_FOR_PREDICTION):
|
| 138 |
+
# All 16 features collected - make prediction
|
| 139 |
+
feature_vector = prepare_feature_vector(state)
|
| 140 |
+
predictor = get_predictor()
|
| 141 |
+
pred_result = predictor.predict(feature_vector)
|
| 142 |
+
|
| 143 |
+
pred_data = {
|
| 144 |
+
"prediction_class": int(pred_result.prediction),
|
| 145 |
+
"prediction_name": CLASS_NAMES[int(pred_result.prediction)],
|
| 146 |
+
"confidence": float(pred_result.probability),
|
| 147 |
+
"risk_level": pred_result.risk_level,
|
| 148 |
+
"explanation": pred_result.explanation,
|
| 149 |
+
"features": state
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
# Simple completion message - full details now in DiagnosisCard component
|
| 153 |
+
response_msg = "β
Assessment Complete! Your diagnosis is ready below."
|
| 154 |
+
|
| 155 |
+
_save_state(session_id, state)
|
| 156 |
+
|
| 157 |
+
return ChatResponse(
|
| 158 |
+
session_id=session_id,
|
| 159 |
+
message=response_msg,
|
| 160 |
+
features=state,
|
| 161 |
+
collected_count=collected,
|
| 162 |
+
is_complete=True,
|
| 163 |
+
prediction=pred_data,
|
| 164 |
+
hint=""
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
# Not complete yet - ask for next missing feature
|
| 168 |
+
prioritized_missing = prioritize_features(missing)
|
| 169 |
+
next_question = generate_question(prioritized_missing[:1])
|
| 170 |
+
ack = _build_acknowledgment(extracted)
|
| 171 |
+
remaining = 16 - collected
|
| 172 |
+
|
| 173 |
+
response_msg = f"""{ack}
|
| 174 |
+
|
| 175 |
+
{next_question}
|
| 176 |
+
|
| 177 |
+
**{remaining} more pieces of information needed.**""" if ack else f"""{next_question}
|
| 178 |
+
|
| 179 |
+
**{remaining} more pieces of information needed.**"""
|
| 180 |
+
|
| 181 |
+
# Get hint for the next question
|
| 182 |
+
hint = _get_question_hint(next_question)
|
| 183 |
+
|
| 184 |
+
_save_state(session_id, state)
|
| 185 |
+
|
| 186 |
+
return ChatResponse(
|
| 187 |
+
session_id=session_id,
|
| 188 |
+
message=response_msg,
|
| 189 |
+
features=state,
|
| 190 |
+
collected_count=collected,
|
| 191 |
+
is_complete=False,
|
| 192 |
+
hint=hint
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
except Exception as e:
|
| 196 |
+
logger.error(f"β Error in chat endpoint: {e}", exc_info=True)
|
| 197 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
@app.post("/api/reset")
|
| 201 |
+
def reset_endpoint(req: ResetRequest):
|
| 202 |
+
"""Reset a session - clear all features and start fresh"""
|
| 203 |
+
try:
|
| 204 |
+
session_manager = get_session_manager()
|
| 205 |
+
success = session_manager.reset_session(req.session_id)
|
| 206 |
+
|
| 207 |
+
if success:
|
| 208 |
+
logger.info(f"β
Reset session {req.session_id}")
|
| 209 |
+
return {
|
| 210 |
+
"success": True,
|
| 211 |
+
"message": "Session reset successfully",
|
| 212 |
+
"session_id": req.session_id
|
| 213 |
+
}
|
| 214 |
+
else:
|
| 215 |
+
raise HTTPException(status_code=404, detail="Session not found")
|
| 216 |
+
|
| 217 |
+
except Exception as e:
|
| 218 |
+
logger.error(f"β Error resetting session: {e}")
|
| 219 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
@app.get("/api/session/{session_id}", response_model=SessionStateResponse)
|
| 223 |
+
def get_session_endpoint(session_id: str):
|
| 224 |
+
"""Get current session state"""
|
| 225 |
+
try:
|
| 226 |
+
state = _load_state(session_id)
|
| 227 |
+
collected = count_collected_features(state)
|
| 228 |
+
|
| 229 |
+
return SessionStateResponse(
|
| 230 |
+
session_id=session_id,
|
| 231 |
+
features=state,
|
| 232 |
+
collected_count=collected
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
except Exception as e:
|
| 236 |
+
logger.error(f"β Error getting session: {e}")
|
| 237 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
@app.get("/api/features")
|
| 241 |
+
def get_features_list():
|
| 242 |
+
"""Get list of all 16 features with their metadata"""
|
| 243 |
+
from app.config import FEATURE_RANGES
|
| 244 |
+
|
| 245 |
+
features_info = {}
|
| 246 |
+
for feature in DEFAULT_MODEL_FEATURES:
|
| 247 |
+
if feature in FEATURE_RANGES:
|
| 248 |
+
min_val, max_val, _ = FEATURE_RANGES[feature]
|
| 249 |
+
features_info[feature] = {
|
| 250 |
+
"min": min_val,
|
| 251 |
+
"max": max_val,
|
| 252 |
+
"type": "numeric" if feature not in ["Smoking", "Alcohol", "Family History"] else "binary"
|
| 253 |
+
}
|
| 254 |
+
else:
|
| 255 |
+
features_info[feature] = {"min": None, "max": None, "type": "unknown"}
|
| 256 |
+
|
| 257 |
+
return {
|
| 258 |
+
"total": len(DEFAULT_MODEL_FEATURES),
|
| 259 |
+
"features": DEFAULT_MODEL_FEATURES,
|
| 260 |
+
"metadata": features_info
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
if __name__ == "__main__":
|
| 265 |
+
import uvicorn
|
| 266 |
+
uvicorn.run(app, host="0.0.0.0", port=8000, reload=True)
|
app/config.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
+
# Load environment
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
# Paths - robust for both local and HF Spaces deployment
|
| 9 |
+
BASE_DIR = Path(__file__).parent.parent
|
| 10 |
+
|
| 11 |
+
# Try multiple possible model locations
|
| 12 |
+
_possible_paths = [
|
| 13 |
+
BASE_DIR / "models" / "GradientBoosting_model.pkl", # Local development
|
| 14 |
+
BASE_DIR / "model" / "GradientBoosting_model.pkl", # If named 'model' instead
|
| 15 |
+
Path("/app/models/GradientBoosting_model.pkl"), # HF Spaces absolute path
|
| 16 |
+
Path.cwd() / "models" / "GradientBoosting_model.pkl", # Current working directory
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
MODEL_PATH = None
|
| 20 |
+
for path in _possible_paths:
|
| 21 |
+
if path.exists():
|
| 22 |
+
MODEL_PATH = path
|
| 23 |
+
break
|
| 24 |
+
|
| 25 |
+
# Default to first path if none found (will error gracefully)
|
| 26 |
+
if MODEL_PATH is None:
|
| 27 |
+
MODEL_PATH = _possible_paths[0]
|
| 28 |
+
|
| 29 |
+
# Groq API configuration
|
| 30 |
+
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
|
| 31 |
+
GROQ_MODEL = "llama-3.1-70b-versatile"
|
| 32 |
+
GROQ_TEMPERATURE = 0
|
| 33 |
+
GROQ_TIMEOUT = 30
|
| 34 |
+
|
| 35 |
+
# Medical features (all 16)
|
| 36 |
+
DEFAULT_MODEL_FEATURES = [
|
| 37 |
+
"LengthOfStay",
|
| 38 |
+
"Smoking",
|
| 39 |
+
"Family History",
|
| 40 |
+
"HbA1c",
|
| 41 |
+
"Glucose",
|
| 42 |
+
"Age",
|
| 43 |
+
"Diet Score",
|
| 44 |
+
"Alcohol",
|
| 45 |
+
"Physical Activity",
|
| 46 |
+
"Blood Pressure",
|
| 47 |
+
"BMI",
|
| 48 |
+
"Cholesterol",
|
| 49 |
+
"Sleep Hours",
|
| 50 |
+
"Stress Level",
|
| 51 |
+
"Triglycerides",
|
| 52 |
+
"Oxygen Saturation"
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
# Feature validation ranges (min, max, expected type)
|
| 56 |
+
FEATURE_RANGES = {
|
| 57 |
+
"Age": (0, 150, float),
|
| 58 |
+
"Glucose": (70, 400, float),
|
| 59 |
+
"HbA1c": (3, 15, float),
|
| 60 |
+
"BMI": (10, 60, float),
|
| 61 |
+
"Cholesterol": (100, 400, float),
|
| 62 |
+
"Triglycerides": (20, 500, float),
|
| 63 |
+
"Blood Pressure": (60, 200, float),
|
| 64 |
+
"Physical Activity": (0, 24, float),
|
| 65 |
+
"Sleep Hours": (0, 24, float),
|
| 66 |
+
"Stress Level": (1, 10, float),
|
| 67 |
+
"Diet Score": (1, 10, float),
|
| 68 |
+
"Smoking": (0, 1, int),
|
| 69 |
+
"Alcohol": (0, 1, int),
|
| 70 |
+
"Family History": (0, 1, int),
|
| 71 |
+
"LengthOfStay": (0, 365, int),
|
| 72 |
+
"Oxygen Saturation": (80, 100, float),
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
# Model configuration
|
| 76 |
+
MIN_FEATURES_FOR_PREDICTION = 16 # Ask for all 16 features
|
| 77 |
+
MAX_RETRIES = 3
|
| 78 |
+
|
| 79 |
+
# Prediction classes
|
| 80 |
+
CLASS_NAMES = [
|
| 81 |
+
"Arthritis",
|
| 82 |
+
"Asthma",
|
| 83 |
+
"Cancer",
|
| 84 |
+
"Diabetes",
|
| 85 |
+
"Healthy",
|
| 86 |
+
"Hypertension",
|
| 87 |
+
"Obesity",
|
| 88 |
+
"Other/Unknown",
|
| 89 |
+
]
|
| 90 |
+
|
| 91 |
+
# App configuration
|
| 92 |
+
DEBUG_MODE = True
|
| 93 |
+
CONVERSATION_MAX_TURNS = 20
|
app/main.py
ADDED
|
@@ -0,0 +1,923 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import logging
|
| 3 |
+
import json
|
| 4 |
+
import re
|
| 5 |
+
import hashlib
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import List, Tuple
|
| 8 |
+
|
| 9 |
+
from .services.llm_extractor import extract_features_from_text
|
| 10 |
+
from .services.feature_builder import (
|
| 11 |
+
prepare_feature_vector,
|
| 12 |
+
count_collected_features,
|
| 13 |
+
is_ready_for_prediction
|
| 14 |
+
)
|
| 15 |
+
from .services.predictor import get_predictor
|
| 16 |
+
from .services.session_manager import get_session_manager
|
| 17 |
+
from .memory import initialize_state, update_state, get_missing_features
|
| 18 |
+
from .utils.helpers import generate_question, prioritize_features
|
| 19 |
+
from .config import DEFAULT_MODEL_FEATURES, MIN_FEATURES_FOR_PREDICTION, CLASS_NAMES
|
| 20 |
+
|
| 21 |
+
# Setup logging
|
| 22 |
+
logging.basicConfig(
|
| 23 |
+
level=logging.INFO,
|
| 24 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 25 |
+
)
|
| 26 |
+
logger = logging.getLogger(__name__)
|
| 27 |
+
|
| 28 |
+
# Session state storage
|
| 29 |
+
SESSION_DIR = Path("/tmp/deepsense_sessions")
|
| 30 |
+
SESSION_DIR.mkdir(exist_ok=True)
|
| 31 |
+
|
| 32 |
+
# Session manager
|
| 33 |
+
session_manager = get_session_manager()
|
| 34 |
+
|
| 35 |
+
def _get_session_id(history: List) -> str:
|
| 36 |
+
"""Generate session ID consistently across all turns
|
| 37 |
+
|
| 38 |
+
Key insight: In Gradio ChatInterface:
|
| 39 |
+
- Turn 1: history is empty
|
| 40 |
+
- Turn 2+: history contains all prior messages, starting with first user message
|
| 41 |
+
|
| 42 |
+
Strategy: When history is empty, use a temporary default and let it be overridden
|
| 43 |
+
on the next turn when we have the actual first message.
|
| 44 |
+
"""
|
| 45 |
+
try:
|
| 46 |
+
first_msg = None
|
| 47 |
+
|
| 48 |
+
# If we have history, always use the first message (most reliable)
|
| 49 |
+
if history:
|
| 50 |
+
# Gradio 6 format: list of dicts with 'role' and 'content'
|
| 51 |
+
if isinstance(history[0], dict):
|
| 52 |
+
first_msg = history[0].get("content", "")
|
| 53 |
+
# Fallback for tuple format (Gradio 4 or mixed formats)
|
| 54 |
+
elif isinstance(history[0], (tuple, list)):
|
| 55 |
+
first_msg = history[0][0] if len(history[0]) > 0 else ""
|
| 56 |
+
else:
|
| 57 |
+
first_msg = str(history[0])
|
| 58 |
+
|
| 59 |
+
# Handle case where first_msg is a list (Gradio 6 content can be list)
|
| 60 |
+
if first_msg and isinstance(first_msg, list):
|
| 61 |
+
first_msg = str(first_msg[0]) if first_msg else ""
|
| 62 |
+
|
| 63 |
+
first_msg = str(first_msg) if first_msg else "default"
|
| 64 |
+
|
| 65 |
+
# Generate session ID from first message hash
|
| 66 |
+
session_id = "sess_" + hashlib.md5(first_msg.encode()).hexdigest()[:8]
|
| 67 |
+
logger.debug(f"π Session ID: {session_id} (from message: {first_msg[:50]}...)")
|
| 68 |
+
return session_id
|
| 69 |
+
|
| 70 |
+
except Exception as e:
|
| 71 |
+
logger.warning(f"β οΈ Error generating session ID: {e}")
|
| 72 |
+
return "sess_error"
|
| 73 |
+
|
| 74 |
+
def _load_state(session_id: str) -> dict:
|
| 75 |
+
"""Load state from file with detailed logging and error handling"""
|
| 76 |
+
try:
|
| 77 |
+
path = SESSION_DIR / f"{session_id}.json"
|
| 78 |
+
if path.exists():
|
| 79 |
+
with open(path, 'r') as f:
|
| 80 |
+
state_data = json.load(f)
|
| 81 |
+
collected = sum(1 for v in state_data.values() if v is not None)
|
| 82 |
+
logger.info(f"π Loaded session {session_id}: {collected}/16 features")
|
| 83 |
+
return state_data
|
| 84 |
+
else:
|
| 85 |
+
logger.info(f"π New session {session_id} (no prior file)")
|
| 86 |
+
return initialize_state()
|
| 87 |
+
except Exception as e:
|
| 88 |
+
logger.error(f"β Error loading state: {e} - creating fresh state")
|
| 89 |
+
return initialize_state()
|
| 90 |
+
|
| 91 |
+
def _save_state(session_id: str, state: dict):
|
| 92 |
+
"""Save state to file with detailed logging and proper error handling"""
|
| 93 |
+
try:
|
| 94 |
+
path = SESSION_DIR / f"{session_id}.json"
|
| 95 |
+
with open(path, 'w') as f:
|
| 96 |
+
json.dump(state, f)
|
| 97 |
+
collected = sum(1 for v in state.values() if v is not None)
|
| 98 |
+
logger.info(f"πΎ Saved session {session_id}: {collected}/16 features to {path}")
|
| 99 |
+
except Exception as e:
|
| 100 |
+
logger.error(f"β Error saving state to {path}: {e}")
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _handle_simple_response(message: str, feature: str) -> dict:
|
| 104 |
+
"""
|
| 105 |
+
Handle simple yes/no responses for binary features and numeric responses.
|
| 106 |
+
Maps responses like 'yes', 'no', '45', '10 days', etc. to feature values.
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
message: User's message
|
| 110 |
+
feature: Feature name being asked about
|
| 111 |
+
|
| 112 |
+
Returns:
|
| 113 |
+
Dictionary with feature mapped to value, or empty dict if not extractable
|
| 114 |
+
"""
|
| 115 |
+
import re
|
| 116 |
+
|
| 117 |
+
msg_lower = message.lower().strip()
|
| 118 |
+
|
| 119 |
+
# Binary features: Smoking, Alcohol, Family History
|
| 120 |
+
if feature in ["Smoking", "Alcohol", "Family History"]:
|
| 121 |
+
if any(word in msg_lower for word in ["yes", "yep", "yeah", "yea", "true", "i have", "i do", "positive"]):
|
| 122 |
+
return {feature: 1}
|
| 123 |
+
elif any(word in msg_lower for word in ["no", "nope", "nah", "false", "i don't", "i do not", "negative", "neither"]):
|
| 124 |
+
return {feature: 0}
|
| 125 |
+
|
| 126 |
+
# Numeric features - extract first number from message
|
| 127 |
+
numeric_features = [
|
| 128 |
+
"Age", "Glucose", "HbA1c", "BMI", "Cholesterol", "Triglycerides",
|
| 129 |
+
"Blood Pressure", "Physical Activity", "Sleep Hours", "Stress Level",
|
| 130 |
+
"Diet Score", "LengthOfStay", "Oxygen Saturation"
|
| 131 |
+
]
|
| 132 |
+
|
| 133 |
+
if feature in numeric_features:
|
| 134 |
+
# Try to extract a number from the message
|
| 135 |
+
numbers = re.findall(r"[-+]?\d*\.?\d+", message)
|
| 136 |
+
if numbers:
|
| 137 |
+
try:
|
| 138 |
+
value = float(numbers[0]) if "." in numbers[0] else int(numbers[0])
|
| 139 |
+
logger.debug(f"β
Extracted {feature} = {value} from simple response")
|
| 140 |
+
return {feature: value}
|
| 141 |
+
except (ValueError, IndexError):
|
| 142 |
+
pass
|
| 143 |
+
|
| 144 |
+
return {}
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _extract_previously_asked_features(history: List[Tuple[str, str]]) -> set:
|
| 148 |
+
"""
|
| 149 |
+
Extract what features have already been asked about in the conversation.
|
| 150 |
+
This prevents repetition.
|
| 151 |
+
|
| 152 |
+
Args:
|
| 153 |
+
history: Conversation history (list of tuples)
|
| 154 |
+
|
| 155 |
+
Returns:
|
| 156 |
+
Set of features that have been asked about
|
| 157 |
+
"""
|
| 158 |
+
asked_features = set()
|
| 159 |
+
if not history:
|
| 160 |
+
return asked_features
|
| 161 |
+
|
| 162 |
+
question_keywords = {
|
| 163 |
+
"Age": ["age", "years old"],
|
| 164 |
+
"BloodPressure": ["blood pressure", "bp"],
|
| 165 |
+
"Glucose": ["glucose", "blood sugar"],
|
| 166 |
+
"BMI": ["bmi", "body mass"],
|
| 167 |
+
"Cholesterol": ["cholesterol"],
|
| 168 |
+
"HbA1c": ["hba1c", "hemoglobin"],
|
| 169 |
+
"Triglycerides": ["triglycerides"],
|
| 170 |
+
"Smoking": ["smoke", "smoking"],
|
| 171 |
+
"Alcohol": ["drink", "alcohol"],
|
| 172 |
+
"PhysicalActivity": ["exercise", "activity", "workout"],
|
| 173 |
+
"SleepHours": ["sleep", "hours of sleep"],
|
| 174 |
+
"StressLevel": ["stress"],
|
| 175 |
+
"DietScore": ["diet", "nutrition"],
|
| 176 |
+
"FamilyHistory": ["family history", "disease history"],
|
| 177 |
+
"LengthOfStay": ["hospital", "stay", "days"],
|
| 178 |
+
"OxygenSaturation": ["oxygen", "saturation"],
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
try:
|
| 182 |
+
# Check bot's previous questions (Gradio 6 format)
|
| 183 |
+
for item in history:
|
| 184 |
+
try:
|
| 185 |
+
# Gradio 6: dict with 'role' and 'content'
|
| 186 |
+
if isinstance(item, dict):
|
| 187 |
+
if item.get("role") == "assistant":
|
| 188 |
+
bot_response = item.get("content", "")
|
| 189 |
+
else:
|
| 190 |
+
continue
|
| 191 |
+
# Fallback for tuple format
|
| 192 |
+
elif isinstance(item, (list, tuple)) and len(item) >= 2:
|
| 193 |
+
_, bot_response = item[0], item[1]
|
| 194 |
+
else:
|
| 195 |
+
continue
|
| 196 |
+
|
| 197 |
+
if bot_response:
|
| 198 |
+
response_lower = str(bot_response).lower()
|
| 199 |
+
for feature, keywords in question_keywords.items():
|
| 200 |
+
for keyword in keywords:
|
| 201 |
+
if keyword in response_lower:
|
| 202 |
+
asked_features.add(feature)
|
| 203 |
+
break
|
| 204 |
+
except (ValueError, IndexError, TypeError):
|
| 205 |
+
# Skip malformed history items
|
| 206 |
+
continue
|
| 207 |
+
|
| 208 |
+
return asked_features
|
| 209 |
+
except Exception as e:
|
| 210 |
+
logger.warning(f"β οΈ Error extracting asked features: {e}")
|
| 211 |
+
return set()
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def chat_fn(message: str, history: List[Tuple[str, str]]) -> str:
|
| 215 |
+
"""
|
| 216 |
+
Main chat function for Gradio ChatInterface.
|
| 217 |
+
|
| 218 |
+
Orchestrates the complete conversation flow:
|
| 219 |
+
1. Extract features from user input (Groq LLM)
|
| 220 |
+
2. For simple yes/no to binary features, use pattern matching
|
| 221 |
+
3. Update memory with extracted features
|
| 222 |
+
4. Check for missing features
|
| 223 |
+
5. If all features ready, make prediction
|
| 224 |
+
6. Otherwise, ask for next missing feature
|
| 225 |
+
|
| 226 |
+
Args:
|
| 227 |
+
message: User's current message
|
| 228 |
+
history: Conversation history (not used but required by Gradio)
|
| 229 |
+
|
| 230 |
+
Returns:
|
| 231 |
+
Bot response (question or prediction)
|
| 232 |
+
"""
|
| 233 |
+
try:
|
| 234 |
+
# Get/create session and load persisted state
|
| 235 |
+
# If history is empty (Turn 1), use current message for consistent session ID
|
| 236 |
+
if not history:
|
| 237 |
+
# Generate session_id from first user message to ensure consistency across turns
|
| 238 |
+
session_id = "sess_" + hashlib.md5(message.encode()).hexdigest()[:8]
|
| 239 |
+
logger.debug(f"π Turn 1 Session ID: {session_id} (from current message)")
|
| 240 |
+
else:
|
| 241 |
+
# Turn 2+: Use first message from history
|
| 242 |
+
session_id = _get_session_id(history)
|
| 243 |
+
|
| 244 |
+
state = _load_state(session_id)
|
| 245 |
+
|
| 246 |
+
# Normalize message
|
| 247 |
+
msg_lower = message.lower().strip()
|
| 248 |
+
|
| 249 |
+
# Check if conversation has started (by checking if state has any data)
|
| 250 |
+
collected = count_collected_features(state)
|
| 251 |
+
is_first_message = collected == 0 # True only if no features collected yet
|
| 252 |
+
conversation_active = collected > 0
|
| 253 |
+
previously_asked = _extract_previously_asked_features(history) if history else set()
|
| 254 |
+
|
| 255 |
+
# Track last asked feature for simple response matching
|
| 256 |
+
last_asked_feature = None
|
| 257 |
+
|
| 258 |
+
# First message greeting (ONLY if truly first message AND looks like greeting)
|
| 259 |
+
if is_first_message and len(msg_lower.split()) <= 6: # Short messages are likely greetings
|
| 260 |
+
logger.info("π― Starting new conversation")
|
| 261 |
+
|
| 262 |
+
# Check if it's a simple greeting
|
| 263 |
+
greeting_words = ["hello", "hi", "hey", "good morning", "good afternoon", "good evening", "name is", "i am", "i'm"]
|
| 264 |
+
if any(word in msg_lower for word in greeting_words):
|
| 265 |
+
# Extract name if mentioned (letters only, not numbers)
|
| 266 |
+
name_match = re.search(r"(?:i'm|i am|my name is|name's)\s+([a-z]+)", msg_lower)
|
| 267 |
+
user_name = name_match.group(1).capitalize() if name_match else "there"
|
| 268 |
+
|
| 269 |
+
greeting_response = f"Hi {user_name}! Nice to meet you. π\n\n"
|
| 270 |
+
greeting_response += """I'm here to help build your health profile. We'll gather 16 key health metrics in a natural, conversational way.
|
| 271 |
+
|
| 272 |
+
**To get started: What is your age?**"""
|
| 273 |
+
_save_state(session_id, state)
|
| 274 |
+
return _format_response(state, greeting_response)
|
| 275 |
+
|
| 276 |
+
# Handle mid-conversation greetings (don't reset, reference last topic)
|
| 277 |
+
if conversation_active:
|
| 278 |
+
greeting_words = ["hello", "hi", "hey", "good morning", "good afternoon", "good evening"]
|
| 279 |
+
if any(word in msg_lower for word in greeting_words) and len(msg_lower.split()) <= 3:
|
| 280 |
+
# It's a simple greeting mid-conversation, don't reset
|
| 281 |
+
last_bot_message = history[-1][1] if history else ""
|
| 282 |
+
context = "your health profile"
|
| 283 |
+
if "age" in last_bot_message.lower():
|
| 284 |
+
context = "your age"
|
| 285 |
+
elif "blood pressure" in last_bot_message.lower():
|
| 286 |
+
context = "your blood pressure"
|
| 287 |
+
elif "glucose" in last_bot_message.lower():
|
| 288 |
+
context = "your glucose level"
|
| 289 |
+
|
| 290 |
+
continuation = f"Hey! We were just talking about {context}. Do you have those numbers for me?"
|
| 291 |
+
_save_state(session_id, state)
|
| 292 |
+
return _format_response(state, continuation)
|
| 293 |
+
|
| 294 |
+
# Handle reset command
|
| 295 |
+
if msg_lower in ["reset", "clear", "new", "start over", "/reset"]:
|
| 296 |
+
state = initialize_state() # Fresh state
|
| 297 |
+
_save_state(session_id, state)
|
| 298 |
+
reset_msg = """Let's start fresh! π
|
| 299 |
+
|
| 300 |
+
I'm ready to help build your health profile again. Just share your health information naturally, like:
|
| 301 |
+
- "I'm 45 years old and my glucose is 150"
|
| 302 |
+
- "I smoke and my stress level is 8"
|
| 303 |
+
- "My BMI is 28 and I exercise 5 hours per week"
|
| 304 |
+
|
| 305 |
+
What would you like to share first?"""
|
| 306 |
+
return _format_response(state, reset_msg)
|
| 307 |
+
|
| 308 |
+
# Step 1: Extract features (with safety)
|
| 309 |
+
logger.info(f"π€ User: {message}")
|
| 310 |
+
try:
|
| 311 |
+
extracted = extract_features_from_text(message)
|
| 312 |
+
if not extracted:
|
| 313 |
+
extracted = {f: None for f in DEFAULT_MODEL_FEATURES}
|
| 314 |
+
except Exception as e:
|
| 315 |
+
logger.warning(f"Extraction failed: {e}, using empty")
|
| 316 |
+
extracted = {f: None for f in DEFAULT_MODEL_FEATURES}
|
| 317 |
+
|
| 318 |
+
logger.info(f"π Extracted: {[k for k,v in extracted.items() if v]}")
|
| 319 |
+
|
| 320 |
+
# Step 1.5: Check if any features were extracted
|
| 321 |
+
features_extracted = sum(1 for v in extracted.values() if v is not None)
|
| 322 |
+
if features_extracted == 0:
|
| 323 |
+
logger.debug(f"β οΈ No features extracted from: {message}")
|
| 324 |
+
|
| 325 |
+
# Step 1b: If extraction didn't work and we asked about a feature, use simple pattern matching
|
| 326 |
+
if last_asked_feature and all(v is None for v in extracted.values()):
|
| 327 |
+
simple_response = _handle_simple_response(message, last_asked_feature)
|
| 328 |
+
if simple_response:
|
| 329 |
+
logger.info(f"β
Matched simple response for {last_asked_feature}")
|
| 330 |
+
extracted.update(simple_response)
|
| 331 |
+
|
| 332 |
+
# Step 2: Update memory with extracted features (safe)
|
| 333 |
+
try:
|
| 334 |
+
state = update_state(state, extracted)
|
| 335 |
+
collected = count_collected_features(state)
|
| 336 |
+
logger.info(f"πΎ Collected: {collected}/16")
|
| 337 |
+
|
| 338 |
+
# Verify state integrity
|
| 339 |
+
if state is None:
|
| 340 |
+
logger.error("β State became None after update!")
|
| 341 |
+
state = initialize_state()
|
| 342 |
+
if not isinstance(state, dict):
|
| 343 |
+
logger.error(f"β State is not a dict: {type(state)}")
|
| 344 |
+
state = initialize_state()
|
| 345 |
+
|
| 346 |
+
except Exception as e:
|
| 347 |
+
logger.error(f"State update failed: {e}")
|
| 348 |
+
state = initialize_state()
|
| 349 |
+
collected = 0
|
| 350 |
+
|
| 351 |
+
# Step 3: Check for missing features
|
| 352 |
+
missing = get_missing_features(state)
|
| 353 |
+
|
| 354 |
+
# Step 3.5: Smart handling - if nothing extracted and no features collected yet
|
| 355 |
+
if features_extracted == 0 and collected == 0:
|
| 356 |
+
logger.info("β οΈ User input is non-medical. Prompting for medical information with positive frame.")
|
| 357 |
+
non_medical_msg = """I'm here to help build your health profile. To get started, could you tell me your age or any health details you'd like to share?
|
| 358 |
+
|
| 359 |
+
For example: "I'm 45 years old" or "My glucose is 150" β anything you're comfortable sharing helps!"""
|
| 360 |
+
return _format_response(state, non_medical_msg)
|
| 361 |
+
|
| 362 |
+
# Step 4: Decision - Can we make a prediction?
|
| 363 |
+
if len(missing) == 0 or is_ready_for_prediction(state, MIN_FEATURES_FOR_PREDICTION):
|
| 364 |
+
logger.info("π― All features collected! Making prediction...")
|
| 365 |
+
last_asked_feature = None
|
| 366 |
+
return _make_prediction(state, session_id)
|
| 367 |
+
|
| 368 |
+
# Step 5: Not ready yet - ask for next missing features (1-2 at a time)
|
| 369 |
+
else:
|
| 370 |
+
if missing:
|
| 371 |
+
last_asked_feature = missing[0]
|
| 372 |
+
question_text = _ask_next_question(missing, len(missing), extracted)
|
| 373 |
+
_save_state(session_id, state) # Save state before returning
|
| 374 |
+
return _format_response(state, "", question_text)
|
| 375 |
+
|
| 376 |
+
except Exception as e:
|
| 377 |
+
logger.error(f"β Error in chat: {e}", exc_info=True)
|
| 378 |
+
logger.error(f" History type: {type(history)}, length: {len(history) if history else 0}")
|
| 379 |
+
logger.error(f" State type: {type(state)}")
|
| 380 |
+
logger.error(f" Message: {message}")
|
| 381 |
+
|
| 382 |
+
# Return user-friendly error message
|
| 383 |
+
error_msg = "I encountered an issue processing your message. Could you try again or rephrase?"
|
| 384 |
+
return _format_response(state or initialize_state(), error_msg)
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
def _make_prediction(state: dict, session_id: str = None) -> str:
|
| 388 |
+
"""
|
| 389 |
+
Make ML prediction once all features are collected.
|
| 390 |
+
|
| 391 |
+
Args:
|
| 392 |
+
state: Dictionary with all medical features
|
| 393 |
+
session_id: Session ID for saving final state
|
| 394 |
+
|
| 395 |
+
Returns:
|
| 396 |
+
Formatted prediction result message
|
| 397 |
+
"""
|
| 398 |
+
try:
|
| 399 |
+
# Prepare feature vector for ML model
|
| 400 |
+
feature_vector = prepare_feature_vector(state)
|
| 401 |
+
logger.info(f"π’ Feature vector prepared: {feature_vector}")
|
| 402 |
+
|
| 403 |
+
# Get predictor and make prediction
|
| 404 |
+
predictor = get_predictor()
|
| 405 |
+
result = predictor.predict(feature_vector)
|
| 406 |
+
|
| 407 |
+
# Format response
|
| 408 |
+
class_name = CLASS_NAMES[result.prediction] if result.prediction < len(CLASS_NAMES) else "Unknown"
|
| 409 |
+
|
| 410 |
+
response = f"""
|
| 411 |
+
β
**All Information Collected!**
|
| 412 |
+
|
| 413 |
+
---
|
| 414 |
+
|
| 415 |
+
### π₯ Prediction Results
|
| 416 |
+
|
| 417 |
+
**Predicted Condition:** {class_name}
|
| 418 |
+
**Confidence Level:** {result.probability*100:.1f}%
|
| 419 |
+
**Risk Assessment:** **{result.risk_level}**
|
| 420 |
+
|
| 421 |
+
**Details:** {result.explanation}
|
| 422 |
+
|
| 423 |
+
---
|
| 424 |
+
|
| 425 |
+
### π Features Used for Prediction:
|
| 426 |
+
"""
|
| 427 |
+
# Add feature summary
|
| 428 |
+
for feature in DEFAULT_MODEL_FEATURES:
|
| 429 |
+
value = state.get(feature)
|
| 430 |
+
response += f"\nβ’ **{feature}:** {value if value is not None else 'Not provided'}"
|
| 431 |
+
|
| 432 |
+
response += """
|
| 433 |
+
|
| 434 |
+
---
|
| 435 |
+
|
| 436 |
+
### β οΈ Important Disclaimer
|
| 437 |
+
**This is a demonstration tool only.** The prediction should never be used as a substitute for professional medical advice. Please consult with a qualified healthcare professional to discuss these results and your health concerns.
|
| 438 |
+
|
| 439 |
+
---
|
| 440 |
+
|
| 441 |
+
**Thank you for providing this information. Your health matters!**
|
| 442 |
+
"""
|
| 443 |
+
|
| 444 |
+
# Add JSON output for backend processing
|
| 445 |
+
json_output = {
|
| 446 |
+
"FINAL_DATA": {
|
| 447 |
+
"prediction_class": result.prediction,
|
| 448 |
+
"prediction_name": class_name,
|
| 449 |
+
"confidence": round(result.probability * 100, 1),
|
| 450 |
+
"risk_level": result.risk_level,
|
| 451 |
+
"features": state.copy()
|
| 452 |
+
}
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
# Add JSON to response (for backend to capture)
|
| 456 |
+
response += f"\n\n```json\n{json.dumps(json_output, indent=2)}\n```"
|
| 457 |
+
|
| 458 |
+
logger.info("β
Prediction completed successfully")
|
| 459 |
+
if session_id:
|
| 460 |
+
_save_state(session_id, state) # Save final state
|
| 461 |
+
return _format_response(state, response)
|
| 462 |
+
|
| 463 |
+
except Exception as e:
|
| 464 |
+
logger.error(f"β Prediction error: {e}", exc_info=True)
|
| 465 |
+
return f"β Error making prediction: {str(e)}. Please try again."
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
def _format_response(current_state: dict, acknowledgment: str = "", question: str = "") -> str:
|
| 469 |
+
"""
|
| 470 |
+
Format response: ALWAYS show DATA_STATE JSON + RESPONSE text
|
| 471 |
+
"""
|
| 472 |
+
# Build JSON state
|
| 473 |
+
data_state = {}
|
| 474 |
+
for feature in DEFAULT_MODEL_FEATURES:
|
| 475 |
+
value = current_state.get(feature)
|
| 476 |
+
data_state[feature] = value
|
| 477 |
+
|
| 478 |
+
json_state = json.dumps(data_state)
|
| 479 |
+
|
| 480 |
+
# Build response text
|
| 481 |
+
response_text = acknowledgment
|
| 482 |
+
if question:
|
| 483 |
+
if acknowledgment:
|
| 484 |
+
response_text += f"\n\n{question}"
|
| 485 |
+
else:
|
| 486 |
+
response_text = question
|
| 487 |
+
|
| 488 |
+
# ALWAYS show both DATA_STATE and RESPONSE
|
| 489 |
+
formatted = f"""DATA_STATE: {json_state}
|
| 490 |
+
|
| 491 |
+
RESPONSE: {response_text}"""
|
| 492 |
+
|
| 493 |
+
return formatted
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
def _ask_next_question(missing_features: list, count_missing: int, last_extracted: dict = None) -> str:
|
| 497 |
+
"""
|
| 498 |
+
Generate professional response asking for next missing features (1-2 at a time).
|
| 499 |
+
Follows the Medical Data Analyst protocol with structured output.
|
| 500 |
+
|
| 501 |
+
Args:
|
| 502 |
+
missing_features: List of missing feature names
|
| 503 |
+
count_missing: Count of missing features
|
| 504 |
+
last_extracted: Dictionary of features just extracted
|
| 505 |
+
|
| 506 |
+
Returns:
|
| 507 |
+
Formatted response with internal state and assistant message
|
| 508 |
+
"""
|
| 509 |
+
try:
|
| 510 |
+
# Build acknowledgment of what was received
|
| 511 |
+
acknowledgment = ""
|
| 512 |
+
if last_extracted and any(v is not None for v in last_extracted.values()):
|
| 513 |
+
extracted_items = []
|
| 514 |
+
for k, v in last_extracted.items():
|
| 515 |
+
if v is not None:
|
| 516 |
+
# Format value nicely
|
| 517 |
+
if v in [0, 1]:
|
| 518 |
+
display_val = "Yes" if v == 1 else "No"
|
| 519 |
+
else:
|
| 520 |
+
display_val = f"{v}"
|
| 521 |
+
extracted_items.append(f"{k} of {display_val}")
|
| 522 |
+
|
| 523 |
+
if extracted_items:
|
| 524 |
+
acknowledgment = f"β I've noted your {' and '.join(extracted_items)}."
|
| 525 |
+
|
| 526 |
+
# Prioritize next questions
|
| 527 |
+
prioritized_missing = prioritize_features(missing_features)
|
| 528 |
+
next_questions = generate_question(prioritized_missing[:2])
|
| 529 |
+
|
| 530 |
+
progress = f"**{count_missing} more pieces of information needed to complete your profile.**"
|
| 531 |
+
|
| 532 |
+
full_question = f"{next_questions}\n\n{progress}"
|
| 533 |
+
|
| 534 |
+
logger.info(f"β Asking for: {prioritized_missing[0] if prioritized_missing else 'unknown'}")
|
| 535 |
+
return full_question
|
| 536 |
+
|
| 537 |
+
except Exception as e:
|
| 538 |
+
logger.error(f"β Error generating question: {e}", exc_info=True)
|
| 539 |
+
return "β Error generating question. Please try again."
|
| 540 |
+
|
| 541 |
+
|
| 542 |
+
def reset_session(history: List) -> List:
|
| 543 |
+
"""
|
| 544 |
+
Reset current session: clear all data and start fresh
|
| 545 |
+
|
| 546 |
+
Args:
|
| 547 |
+
history: Chat history (used to get session ID)
|
| 548 |
+
|
| 549 |
+
Returns:
|
| 550 |
+
New history with reset confirmation message
|
| 551 |
+
"""
|
| 552 |
+
try:
|
| 553 |
+
session_id = _get_session_id(history)
|
| 554 |
+
|
| 555 |
+
# Reset session in manager
|
| 556 |
+
success = session_manager.reset_session(session_id)
|
| 557 |
+
|
| 558 |
+
if success:
|
| 559 |
+
reset_msg = """π **Session Reset Successfully!**
|
| 560 |
+
|
| 561 |
+
Your health profile has been cleared. Let's start fresh with a new assessment.
|
| 562 |
+
|
| 563 |
+
Just introduce yourself and tell me about your health:
|
| 564 |
+
- "Hi, I'm John, 45 years old, my glucose is 150"
|
| 565 |
+
- "I'm Sarah, smoke occasionally, stress level is 8"
|
| 566 |
+
- Or any other health information you'd like to share"""
|
| 567 |
+
|
| 568 |
+
logger.info(f"β
Session {session_id} reset by user")
|
| 569 |
+
|
| 570 |
+
# Return new history with reset message (Gradio 6 format)
|
| 571 |
+
return [{"role": "assistant", "content": reset_msg}]
|
| 572 |
+
else:
|
| 573 |
+
return [{"role": "assistant", "content": "β Error resetting session. Please try again."}]
|
| 574 |
+
|
| 575 |
+
except Exception as e:
|
| 576 |
+
logger.error(f"β Error in reset_session: {e}")
|
| 577 |
+
return [{"role": "assistant", "content": f"β Error: {str(e)}"}]
|
| 578 |
+
|
| 579 |
+
|
| 580 |
+
def _parse_response(raw: str) -> tuple:
|
| 581 |
+
"""
|
| 582 |
+
Parse the raw chat_fn output to extract clean response and data state.
|
| 583 |
+
|
| 584 |
+
Input format: "DATA_STATE: {...}\n\nRESPONSE: <text>"
|
| 585 |
+
Returns: (clean_response_text, data_state_dict)
|
| 586 |
+
"""
|
| 587 |
+
data_state = {}
|
| 588 |
+
response = raw
|
| 589 |
+
|
| 590 |
+
if "DATA_STATE:" in raw and "RESPONSE:" in raw:
|
| 591 |
+
try:
|
| 592 |
+
parts = raw.split("RESPONSE:", 1)
|
| 593 |
+
data_part = parts[0].replace("DATA_STATE:", "").strip()
|
| 594 |
+
response = parts[1].strip()
|
| 595 |
+
data_state = json.loads(data_part)
|
| 596 |
+
except (json.JSONDecodeError, IndexError):
|
| 597 |
+
logger.warning(f"Could not parse DATA_STATE from response")
|
| 598 |
+
data_state = {f: None for f in DEFAULT_MODEL_FEATURES}
|
| 599 |
+
|
| 600 |
+
return response, data_state
|
| 601 |
+
|
| 602 |
+
|
| 603 |
+
def _get_question_hint(response: str) -> str:
|
| 604 |
+
"""
|
| 605 |
+
Extract hint text based on which feature is being asked in the bot's response.
|
| 606 |
+
Matches feature names in the response text.
|
| 607 |
+
"""
|
| 608 |
+
hints = {
|
| 609 |
+
"Blood Pressure": "Systolic pressure (e.g., 120). Normal: 90-120 mmHg.",
|
| 610 |
+
"Glucose": "Blood glucose in mg/dL (e.g., 100). Normal: 70-140.",
|
| 611 |
+
"HbA1c": "HbA1c percentage (e.g., 5.5%). Normal: 4-6%.",
|
| 612 |
+
"BMI": "Body Mass Index (e.g., 25). Normal: 18.5-24.9.",
|
| 613 |
+
"Age": "Age in years (e.g., 34). Range: 18-100.",
|
| 614 |
+
"Cholesterol": "Total cholesterol in mg/dL (e.g., 180). Normal: <200.",
|
| 615 |
+
"Triglycerides": "Triglycerides in mg/dL (e.g., 150). Normal: <150.",
|
| 616 |
+
"Oxygen Saturation": "SpO2 percentage (example: 98). Normal: 95-100%.",
|
| 617 |
+
"Sleep Hours": "Hours of sleep per night (e.g., 7). Normal: 6-9.",
|
| 618 |
+
"Stress Level": "Stress level 1-10 (e.g., 5). 1=very low, 10=very high.",
|
| 619 |
+
"Physical Activity": "Exercise hours per week (e.g., 5).",
|
| 620 |
+
"Diet Score": "Diet quality 1-10 (e.g., 7). 1=poor, 10=excellent.",
|
| 621 |
+
"Smoking": "Do you smoke? Answer yes or no.",
|
| 622 |
+
"Alcohol": "Do you drink alcohol? Answer yes or no.",
|
| 623 |
+
"Family History": "Family history of disease? Answer yes or no.",
|
| 624 |
+
"LengthOfStay": "Hospital stay in days (e.g., 3). Enter 0 if not hospitalized.",
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
response_lower = response.lower()
|
| 628 |
+
for feature, hint in hints.items():
|
| 629 |
+
if feature.lower() in response_lower:
|
| 630 |
+
return hint
|
| 631 |
+
|
| 632 |
+
return "Type your response naturally or just enter a number."
|
| 633 |
+
|
| 634 |
+
|
| 635 |
+
# Create Gradio UI with modern medical intake form design
|
| 636 |
+
css_styling = """
|
| 637 |
+
/* Base container */
|
| 638 |
+
.gradio-container {
|
| 639 |
+
background: linear-gradient(135deg, #F5F3FF 0%, #FAF9FF 100%) !important;
|
| 640 |
+
max-width: 900px !important;
|
| 641 |
+
margin: auto !important;
|
| 642 |
+
padding: 1rem !important;
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
/* Header card */
|
| 646 |
+
.header-card {
|
| 647 |
+
background: white;
|
| 648 |
+
border-radius: 16px;
|
| 649 |
+
box-shadow: 0 2px 12px rgba(124, 58, 237, 0.08);
|
| 650 |
+
padding: 2rem;
|
| 651 |
+
margin-bottom: 1.5rem;
|
| 652 |
+
text-align: center;
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
.app-title {
|
| 656 |
+
color: #7C3AED;
|
| 657 |
+
font-size: 2.2rem;
|
| 658 |
+
font-weight: 700;
|
| 659 |
+
margin: 0;
|
| 660 |
+
margin-bottom: 0.5rem;
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
.app-subtitle {
|
| 664 |
+
color: #999;
|
| 665 |
+
font-size: 0.95rem;
|
| 666 |
+
margin: 0;
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
/* Main form card */
|
| 670 |
+
.form-card {
|
| 671 |
+
background: white;
|
| 672 |
+
border-radius: 16px;
|
| 673 |
+
box-shadow: 0 4px 24px rgba(124, 58, 237, 0.1);
|
| 674 |
+
padding: 2rem;
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
/* Question section header */
|
| 678 |
+
.question-header {
|
| 679 |
+
display: flex;
|
| 680 |
+
justify-content: space-between;
|
| 681 |
+
align-items: center;
|
| 682 |
+
margin-bottom: 1rem;
|
| 683 |
+
padding-bottom: 1rem;
|
| 684 |
+
border-bottom: 2px solid #F0E7FF;
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
.header-label {
|
| 688 |
+
color: #7C3AED;
|
| 689 |
+
font-weight: 600;
|
| 690 |
+
font-size: 0.95rem;
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
+
.progress-label {
|
| 694 |
+
color: #7C3AED;
|
| 695 |
+
font-weight: 600;
|
| 696 |
+
font-size: 0.95rem;
|
| 697 |
+
text-align: right;
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
/* Progress bar */
|
| 701 |
+
.progress-bar-container {
|
| 702 |
+
background: #EDE9FE;
|
| 703 |
+
border-radius: 4px;
|
| 704 |
+
height: 6px;
|
| 705 |
+
overflow: hidden;
|
| 706 |
+
margin-bottom: 1.5rem;
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
.progress-bar-fill {
|
| 710 |
+
height: 100%;
|
| 711 |
+
background: #7C3AED;
|
| 712 |
+
transition: width 0.3s ease;
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
/* Question text */
|
| 716 |
+
.question-text {
|
| 717 |
+
font-size: 1.1rem;
|
| 718 |
+
color: #1F2937;
|
| 719 |
+
margin-bottom: 0.8rem;
|
| 720 |
+
line-height: 1.6;
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
/* Hint text */
|
| 724 |
+
.hint-text {
|
| 725 |
+
color: #888;
|
| 726 |
+
font-size: 0.9rem;
|
| 727 |
+
margin-bottom: 1.5rem;
|
| 728 |
+
padding: 0.75rem 1rem;
|
| 729 |
+
background: #F9F7FF;
|
| 730 |
+
border-left: 3px solid #DDD6FE;
|
| 731 |
+
border-radius: 4px;
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
/* Input and buttons */
|
| 735 |
+
.input-group {
|
| 736 |
+
display: flex;
|
| 737 |
+
gap: 0.75rem;
|
| 738 |
+
margin-top: 1.5rem;
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
.input-box {
|
| 742 |
+
flex: 1;
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
.btn-new {
|
| 746 |
+
background: white !important;
|
| 747 |
+
border: 1px solid #DDD !important;
|
| 748 |
+
border-radius: 8px !important;
|
| 749 |
+
color: #555 !important;
|
| 750 |
+
font-weight: 500 !important;
|
| 751 |
+
cursor: pointer;
|
| 752 |
+
transition: all 0.2s;
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
.btn-new:hover {
|
| 756 |
+
border-color: #7C3AED !important;
|
| 757 |
+
color: #7C3AED !important;
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
.btn-submit {
|
| 761 |
+
background: #7C3AED !important;
|
| 762 |
+
border: 0 !important;
|
| 763 |
+
border-radius: 8px !important;
|
| 764 |
+
color: white !important;
|
| 765 |
+
font-weight: 600 !important;
|
| 766 |
+
cursor: pointer;
|
| 767 |
+
transition: all 0.2s;
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
.btn-submit:hover {
|
| 771 |
+
background: #6D28D9 !important;
|
| 772 |
+
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
/* Disclaimer */
|
| 776 |
+
.disclaimer {
|
| 777 |
+
text-align: center;
|
| 778 |
+
color: #999;
|
| 779 |
+
font-size: 0.85rem;
|
| 780 |
+
margin-top: 1rem;
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
/* Gradio textbox customization */
|
| 784 |
+
.textbox-input input {
|
| 785 |
+
border-radius: 8px !important;
|
| 786 |
+
border: 1px solid #DDD !important;
|
| 787 |
+
padding: 0.75rem 1rem !important;
|
| 788 |
+
font-size: 0.95rem !important;
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
.textbox-input input:focus {
|
| 792 |
+
border-color: #7C3AED !important;
|
| 793 |
+
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1) !important;
|
| 794 |
+
}
|
| 795 |
+
"""
|
| 796 |
+
|
| 797 |
+
with gr.Blocks() as demo:
|
| 798 |
+
# State management
|
| 799 |
+
chat_history = gr.State([])
|
| 800 |
+
|
| 801 |
+
# Header section
|
| 802 |
+
gr.HTML("""
|
| 803 |
+
<div class="header-card">
|
| 804 |
+
<h1 class="app-title">Medical Diagnosis AI</h1>
|
| 805 |
+
<p class="app-subtitle">Clinical intake assessment powered by machine learning</p>
|
| 806 |
+
</div>
|
| 807 |
+
""")
|
| 808 |
+
|
| 809 |
+
# Main form card
|
| 810 |
+
with gr.Column(elem_classes="form-card"):
|
| 811 |
+
# Question header with progress
|
| 812 |
+
with gr.Row(elem_classes="question-header"):
|
| 813 |
+
header_label = gr.HTML('<div class="header-label">Medical Intake Assistant<br>Question 1 of 16</div>')
|
| 814 |
+
progress_label = gr.HTML('<div class="progress-label">0 / 16 questions</div>')
|
| 815 |
+
|
| 816 |
+
# Progress bar
|
| 817 |
+
gr.HTML('<div class="progress-bar-container"><div class="progress-bar-fill" id="progress-fill" style="width:0%"></div></div>')
|
| 818 |
+
|
| 819 |
+
# Question display
|
| 820 |
+
question_display = gr.Markdown(
|
| 821 |
+
"**Hi! I'm your Medical Intake Assistant. Let's begin your health assessment.**\n\n**What is your age?**",
|
| 822 |
+
elem_classes="question-text"
|
| 823 |
+
)
|
| 824 |
+
|
| 825 |
+
# Hint text
|
| 826 |
+
hint_display = gr.Markdown(
|
| 827 |
+
"Age in years (example: 34). Normal range: 18-100.",
|
| 828 |
+
elem_classes="hint-text"
|
| 829 |
+
)
|
| 830 |
+
|
| 831 |
+
# Input row
|
| 832 |
+
with gr.Row():
|
| 833 |
+
msg_input = gr.Textbox(
|
| 834 |
+
placeholder="Enter your response...",
|
| 835 |
+
show_label=False,
|
| 836 |
+
lines=1,
|
| 837 |
+
elem_classes="textbox-input",
|
| 838 |
+
scale=8
|
| 839 |
+
)
|
| 840 |
+
new_btn = gr.Button("New", scale=1, elem_classes="btn-new")
|
| 841 |
+
submit_btn = gr.Button("βΆ", scale=1, elem_classes="btn-submit")
|
| 842 |
+
|
| 843 |
+
# Hidden chatbot for state management
|
| 844 |
+
chatbot = gr.Chatbot(visible=False)
|
| 845 |
+
|
| 846 |
+
# Disclaimer
|
| 847 |
+
gr.HTML('<div class="disclaimer">β οΈ This is a demonstration tool. Always consult with healthcare professionals for medical decisions.</div>')
|
| 848 |
+
|
| 849 |
+
# Event handler for submission
|
| 850 |
+
def handle_submission(message, history):
|
| 851 |
+
"""Process user input and return updated UI elements"""
|
| 852 |
+
if not message or not message.strip():
|
| 853 |
+
return (
|
| 854 |
+
history, # Updated history
|
| 855 |
+
"Please enter a response.", # Question display
|
| 856 |
+
"Try entering your answer.", # Hint
|
| 857 |
+
"", # Clear input
|
| 858 |
+
"0 / 16" # Progress
|
| 859 |
+
)
|
| 860 |
+
|
| 861 |
+
# Call the existing chat function
|
| 862 |
+
raw_response = chat_fn(message, history)
|
| 863 |
+
clean_response, data_state = _parse_response(raw_response)
|
| 864 |
+
|
| 865 |
+
# Update history
|
| 866 |
+
new_history = history + [
|
| 867 |
+
{"role": "user", "content": message},
|
| 868 |
+
{"role": "assistant", "content": clean_response}
|
| 869 |
+
]
|
| 870 |
+
|
| 871 |
+
# Calculate progress
|
| 872 |
+
collected = sum(1 for v in data_state.values() if v is not None)
|
| 873 |
+
progress_text = f"{collected} / 16"
|
| 874 |
+
|
| 875 |
+
# Get hint for next question
|
| 876 |
+
hint_text = _get_question_hint(clean_response)
|
| 877 |
+
|
| 878 |
+
logger.info(f"π Progress: {collected}/16")
|
| 879 |
+
|
| 880 |
+
return (
|
| 881 |
+
new_history,
|
| 882 |
+
clean_response,
|
| 883 |
+
hint_text,
|
| 884 |
+
"", # Clear input for next message
|
| 885 |
+
progress_text
|
| 886 |
+
)
|
| 887 |
+
|
| 888 |
+
# Handle Enter key and submit button
|
| 889 |
+
submit_btn.click(
|
| 890 |
+
fn=handle_submission,
|
| 891 |
+
inputs=[msg_input, chatbot],
|
| 892 |
+
outputs=[chatbot, question_display, hint_display, msg_input, progress_label],
|
| 893 |
+
queue=True
|
| 894 |
+
)
|
| 895 |
+
|
| 896 |
+
msg_input.submit(
|
| 897 |
+
fn=handle_submission,
|
| 898 |
+
inputs=[msg_input, chatbot],
|
| 899 |
+
outputs=[chatbot, question_display, hint_display, msg_input, progress_label],
|
| 900 |
+
queue=True
|
| 901 |
+
)
|
| 902 |
+
|
| 903 |
+
# Reset button handler
|
| 904 |
+
def reset_conversation():
|
| 905 |
+
"""Reset the conversation"""
|
| 906 |
+
reset_history = reset_session([])
|
| 907 |
+
return (
|
| 908 |
+
reset_history, # Reset history
|
| 909 |
+
"**Hi! I'm your Medical Intake Assistant. Let's begin your health assessment.**\n\n**What is your age?**",
|
| 910 |
+
"Age in years (example: 34). Normal range: 18-100.",
|
| 911 |
+
"",
|
| 912 |
+
"0 / 16"
|
| 913 |
+
)
|
| 914 |
+
|
| 915 |
+
new_btn.click(
|
| 916 |
+
fn=reset_conversation,
|
| 917 |
+
outputs=[chatbot, question_display, hint_display, msg_input, progress_label],
|
| 918 |
+
queue=False
|
| 919 |
+
)
|
| 920 |
+
|
| 921 |
+
if __name__ == "__main__":
|
| 922 |
+
logger.info("π Starting Medical Predictor Chatbot...")
|
| 923 |
+
demo.launch()
|
app/memory.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import Dict, Any
|
| 3 |
+
|
| 4 |
+
from app.config import DEFAULT_MODEL_FEATURES, FEATURE_RANGES
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def initialize_state() -> Dict[str, Any]:
|
| 10 |
+
"""
|
| 11 |
+
Initialize conversation state with all 16 features set to None.
|
| 12 |
+
|
| 13 |
+
Returns:
|
| 14 |
+
Dictionary with all features initialized to None
|
| 15 |
+
"""
|
| 16 |
+
state = {feature: None for feature in DEFAULT_MODEL_FEATURES}
|
| 17 |
+
logger.debug(f"β
State initialized with {len(state)} features")
|
| 18 |
+
return state
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def update_state(state: dict, new_data: dict) -> dict:
|
| 22 |
+
"""
|
| 23 |
+
Merge newly extracted values into memory.
|
| 24 |
+
Only updates non-null values (preserves existing data).
|
| 25 |
+
Validates values are within acceptable ranges.
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
state: Current state dictionary
|
| 29 |
+
new_data: Dictionary with newly extracted features
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
Updated state dictionary
|
| 33 |
+
"""
|
| 34 |
+
updated_count = 0
|
| 35 |
+
for key, value in new_data.items():
|
| 36 |
+
if value is not None:
|
| 37 |
+
# Validate value is in acceptable range
|
| 38 |
+
if key in FEATURE_RANGES:
|
| 39 |
+
min_val, max_val, expected_type = FEATURE_RANGES[key]
|
| 40 |
+
try:
|
| 41 |
+
converted = expected_type(value)
|
| 42 |
+
# Check range
|
| 43 |
+
if not (min_val <= converted <= max_val):
|
| 44 |
+
logger.debug(f" SKIPPED {key}={value} (out of range [{min_val}, {max_val}])")
|
| 45 |
+
continue
|
| 46 |
+
value = converted
|
| 47 |
+
except (ValueError, TypeError):
|
| 48 |
+
logger.debug(f" SKIPPED {key}={value} (invalid type)")
|
| 49 |
+
continue
|
| 50 |
+
|
| 51 |
+
old_value = state.get(key)
|
| 52 |
+
state[key] = value
|
| 53 |
+
if old_value != value:
|
| 54 |
+
logger.debug(f" Updated {key}: {old_value} β {value}")
|
| 55 |
+
updated_count += 1
|
| 56 |
+
|
| 57 |
+
if updated_count > 0:
|
| 58 |
+
logger.debug(f"β
State updated: {updated_count} features changed")
|
| 59 |
+
return state
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def get_missing_features(state: dict) -> list:
|
| 63 |
+
"""
|
| 64 |
+
Return list of features that are still missing (None).
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
state: Current state dictionary
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
List of feature names with None values
|
| 71 |
+
"""
|
| 72 |
+
missing = [k for k, v in state.items() if v is None]
|
| 73 |
+
logger.debug(f"β Missing features: {len(missing)}/16 - {missing[:3]}{'...' if len(missing) > 3 else ''}")
|
| 74 |
+
return missing
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def get_state_summary(state: dict) -> Dict[str, Any]:
|
| 78 |
+
"""
|
| 79 |
+
Get a summary of current state.
|
| 80 |
+
|
| 81 |
+
Args:
|
| 82 |
+
state: Current state dictionary
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
Summary with counts and status
|
| 86 |
+
"""
|
| 87 |
+
total = len(state)
|
| 88 |
+
collected = sum(1 for v in state.values() if v is not None)
|
| 89 |
+
missing = total - collected
|
| 90 |
+
|
| 91 |
+
return {
|
| 92 |
+
"total_features": total,
|
| 93 |
+
"collected": collected,
|
| 94 |
+
"missing": missing,
|
| 95 |
+
"percentage": (collected / total * 100) if total > 0 else 0,
|
| 96 |
+
"state": state.copy()
|
| 97 |
+
}
|
app/schemas.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
from pydantic import BaseModel, field_validator
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class MedicalFeatures(BaseModel):
|
| 6 |
+
"""All 16 required medical features with validation"""
|
| 7 |
+
|
| 8 |
+
Age: Optional[float] = None
|
| 9 |
+
Glucose: Optional[float] = None
|
| 10 |
+
HbA1c: Optional[float] = None
|
| 11 |
+
BMI: Optional[float] = None
|
| 12 |
+
Cholesterol: Optional[float] = None
|
| 13 |
+
Triglycerides: Optional[float] = None
|
| 14 |
+
Blood_Pressure: Optional[float] = None
|
| 15 |
+
Physical_Activity: Optional[float] = None
|
| 16 |
+
Sleep_Hours: Optional[float] = None
|
| 17 |
+
Stress_Level: Optional[float] = None
|
| 18 |
+
Diet_Score: Optional[float] = None
|
| 19 |
+
Smoking: Optional[int] = None
|
| 20 |
+
Alcohol: Optional[int] = None
|
| 21 |
+
Family_History: Optional[int] = None
|
| 22 |
+
LengthOfStay: Optional[int] = None
|
| 23 |
+
Oxygen_Saturation: Optional[float] = None
|
| 24 |
+
|
| 25 |
+
@field_validator("Age")
|
| 26 |
+
@classmethod
|
| 27 |
+
def validate_age(cls, v):
|
| 28 |
+
if v is not None and not (0 <= v <= 150):
|
| 29 |
+
raise ValueError("Age must be between 0 and 150")
|
| 30 |
+
return v
|
| 31 |
+
|
| 32 |
+
@field_validator("Glucose")
|
| 33 |
+
@classmethod
|
| 34 |
+
def validate_glucose(cls, v):
|
| 35 |
+
if v is not None and not (70 <= v <= 400):
|
| 36 |
+
raise ValueError("Glucose must be between 70 and 400")
|
| 37 |
+
return v
|
| 38 |
+
|
| 39 |
+
@field_validator("HbA1c")
|
| 40 |
+
@classmethod
|
| 41 |
+
def validate_hba1c(cls, v):
|
| 42 |
+
if v is not None and not (3 <= v <= 15):
|
| 43 |
+
raise ValueError("HbA1c must be between 3 and 15")
|
| 44 |
+
return v
|
| 45 |
+
|
| 46 |
+
@field_validator("BMI")
|
| 47 |
+
@classmethod
|
| 48 |
+
def validate_bmi(cls, v):
|
| 49 |
+
if v is not None and not (10 <= v <= 60):
|
| 50 |
+
raise ValueError("BMI must be between 10 and 60")
|
| 51 |
+
return v
|
| 52 |
+
|
| 53 |
+
@field_validator("Cholesterol")
|
| 54 |
+
@classmethod
|
| 55 |
+
def validate_cholesterol(cls, v):
|
| 56 |
+
if v is not None and not (100 <= v <= 400):
|
| 57 |
+
raise ValueError("Cholesterol must be between 100 and 400")
|
| 58 |
+
return v
|
| 59 |
+
|
| 60 |
+
@field_validator("Triglycerides")
|
| 61 |
+
@classmethod
|
| 62 |
+
def validate_triglycerides(cls, v):
|
| 63 |
+
if v is not None and not (20 <= v <= 500):
|
| 64 |
+
raise ValueError("Triglycerides must be between 20 and 500")
|
| 65 |
+
return v
|
| 66 |
+
|
| 67 |
+
@field_validator("Blood_Pressure")
|
| 68 |
+
@classmethod
|
| 69 |
+
def validate_blood_pressure(cls, v):
|
| 70 |
+
if v is not None and not (60 <= v <= 200):
|
| 71 |
+
raise ValueError("Blood Pressure must be between 60 and 200")
|
| 72 |
+
return v
|
| 73 |
+
|
| 74 |
+
@field_validator("Physical_Activity")
|
| 75 |
+
@classmethod
|
| 76 |
+
def validate_physical_activity(cls, v):
|
| 77 |
+
if v is not None and not (0 <= v <= 24):
|
| 78 |
+
raise ValueError("Physical Activity must be between 0 and 24 hours/week")
|
| 79 |
+
return v
|
| 80 |
+
|
| 81 |
+
@field_validator("Sleep_Hours")
|
| 82 |
+
@classmethod
|
| 83 |
+
def validate_sleep_hours(cls, v):
|
| 84 |
+
if v is not None and not (0 <= v <= 24):
|
| 85 |
+
raise ValueError("Sleep Hours must be between 0 and 24")
|
| 86 |
+
return v
|
| 87 |
+
|
| 88 |
+
@field_validator("Stress_Level")
|
| 89 |
+
@classmethod
|
| 90 |
+
def validate_stress_level(cls, v):
|
| 91 |
+
if v is not None and not (1 <= v <= 10):
|
| 92 |
+
raise ValueError("Stress Level must be between 1 and 10")
|
| 93 |
+
return v
|
| 94 |
+
|
| 95 |
+
@field_validator("Diet_Score")
|
| 96 |
+
@classmethod
|
| 97 |
+
def validate_diet_score(cls, v):
|
| 98 |
+
if v is not None and not (1 <= v <= 10):
|
| 99 |
+
raise ValueError("Diet Score must be between 1 and 10")
|
| 100 |
+
return v
|
| 101 |
+
|
| 102 |
+
@field_validator("Smoking", "Alcohol", "Family_History")
|
| 103 |
+
@classmethod
|
| 104 |
+
def validate_binary(cls, v):
|
| 105 |
+
if v is not None and v not in (0, 1):
|
| 106 |
+
raise ValueError("Binary values must be 0 or 1")
|
| 107 |
+
return v
|
| 108 |
+
|
| 109 |
+
@field_validator("LengthOfStay")
|
| 110 |
+
@classmethod
|
| 111 |
+
def validate_length_of_stay(cls, v):
|
| 112 |
+
if v is not None and not (0 <= v <= 365):
|
| 113 |
+
raise ValueError("Length of Stay must be between 0 and 365 days")
|
| 114 |
+
return v
|
| 115 |
+
|
| 116 |
+
@field_validator("Oxygen_Saturation")
|
| 117 |
+
@classmethod
|
| 118 |
+
def validate_oxygen_saturation(cls, v):
|
| 119 |
+
if v is not None and not (80 <= v <= 100):
|
| 120 |
+
raise ValueError("Oxygen Saturation must be between 80 and 100%")
|
| 121 |
+
return v
|
| 122 |
+
|
| 123 |
+
class Config:
|
| 124 |
+
use_enum_values = True
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
class PredictionRequest(BaseModel):
|
| 128 |
+
"""Request for prediction"""
|
| 129 |
+
|
| 130 |
+
features: MedicalFeatures
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
class PredictionResponse(BaseModel):
|
| 134 |
+
"""Prediction response with confidence"""
|
| 135 |
+
|
| 136 |
+
prediction: int # 0 or 1 (disease class)
|
| 137 |
+
probability: float # 0.0 to 1.0
|
| 138 |
+
risk_level: str # "Low", "Medium", "High"
|
| 139 |
+
explanation: str
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
class ExtractionResponse(BaseModel):
|
| 143 |
+
"""LLM extraction response"""
|
| 144 |
+
|
| 145 |
+
extracted_features: MedicalFeatures
|
| 146 |
+
confidence: float
|
app/services/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Services module for Medical Predictor Chatbot"""
|
| 2 |
+
|
| 3 |
+
from app.services.llm_extractor import extract_features_from_text
|
| 4 |
+
from app.services.feature_builder import prepare_feature_vector, is_ready_for_prediction
|
| 5 |
+
from app.services.predictor import get_predictor
|
| 6 |
+
|
| 7 |
+
__all__ = [
|
| 8 |
+
"extract_features_from_text",
|
| 9 |
+
"prepare_feature_vector",
|
| 10 |
+
"is_ready_for_prediction",
|
| 11 |
+
"get_predictor",
|
| 12 |
+
]
|
app/services/feature_builder.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import List, Dict, Any
|
| 3 |
+
from app.config import FEATURE_RANGES, DEFAULT_MODEL_FEATURES
|
| 4 |
+
|
| 5 |
+
logger = logging.getLogger(__name__)
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def validate_feature(name: str, value: Any) -> Any:
|
| 9 |
+
"""
|
| 10 |
+
Validate a single feature value against its constraints.
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
name: Feature name
|
| 14 |
+
value: Value to validate
|
| 15 |
+
|
| 16 |
+
Returns:
|
| 17 |
+
Validated value or None if invalid
|
| 18 |
+
"""
|
| 19 |
+
if value is None:
|
| 20 |
+
return None
|
| 21 |
+
|
| 22 |
+
if name not in FEATURE_RANGES:
|
| 23 |
+
return None
|
| 24 |
+
|
| 25 |
+
min_val, max_val, expected_type = FEATURE_RANGES[name]
|
| 26 |
+
|
| 27 |
+
# Convert to expected type
|
| 28 |
+
try:
|
| 29 |
+
converted = expected_type(value)
|
| 30 |
+
except (ValueError, TypeError):
|
| 31 |
+
logger.warning(f"Could not convert {name}={value} to {expected_type}")
|
| 32 |
+
return None
|
| 33 |
+
|
| 34 |
+
# Check range
|
| 35 |
+
if not (min_val <= converted <= max_val):
|
| 36 |
+
logger.warning(f"{name}={converted} is outside valid range [{min_val}, {max_val}]")
|
| 37 |
+
return None
|
| 38 |
+
|
| 39 |
+
return converted
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def prepare_features_dict(state: Dict[str, Any]) -> Dict[str, Any]:
|
| 43 |
+
"""
|
| 44 |
+
Validate and prepare features dictionary.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
state: Dictionary of features from memory
|
| 48 |
+
|
| 49 |
+
Returns:
|
| 50 |
+
Dictionary with validated features
|
| 51 |
+
"""
|
| 52 |
+
validated = {}
|
| 53 |
+
|
| 54 |
+
for feature in DEFAULT_MODEL_FEATURES:
|
| 55 |
+
value = state.get(feature)
|
| 56 |
+
validated[feature] = validate_feature(feature, value)
|
| 57 |
+
|
| 58 |
+
return validated
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def prepare_feature_vector(state: Dict[str, Any]) -> List[float]:
|
| 62 |
+
"""
|
| 63 |
+
Convert state dict to feature vector for ML model.
|
| 64 |
+
|
| 65 |
+
The vector must have features in the exact order expected by the model.
|
| 66 |
+
Features are ordered as in DEFAULT_MODEL_FEATURES.
|
| 67 |
+
Missing values are filled with 0.0 (neutral value).
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
state: Dictionary with feature values from memory
|
| 71 |
+
|
| 72 |
+
Returns:
|
| 73 |
+
List of 16 floats ready for ML model prediction
|
| 74 |
+
"""
|
| 75 |
+
# Define feature order (MUST match training data order)
|
| 76 |
+
feature_order = DEFAULT_MODEL_FEATURES
|
| 77 |
+
|
| 78 |
+
vector = []
|
| 79 |
+
|
| 80 |
+
for feature_name in feature_order:
|
| 81 |
+
value = state.get(feature_name)
|
| 82 |
+
|
| 83 |
+
if value is not None:
|
| 84 |
+
try:
|
| 85 |
+
vector.append(float(value))
|
| 86 |
+
except (ValueError, TypeError):
|
| 87 |
+
logger.warning(f"Could not convert {feature_name}={value} to float, using 0.0")
|
| 88 |
+
vector.append(0.0)
|
| 89 |
+
else:
|
| 90 |
+
# Use 0.0 for missing values (neutral/default)
|
| 91 |
+
vector.append(0.0)
|
| 92 |
+
|
| 93 |
+
assert len(vector) == 16, f"Expected 16 features, got {len(vector)}"
|
| 94 |
+
return vector
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def count_collected_features(state: Dict[str, Any]) -> int:
|
| 98 |
+
"""
|
| 99 |
+
Count how many features have been collected (non-null).
|
| 100 |
+
|
| 101 |
+
Args:
|
| 102 |
+
state: Dictionary with feature values
|
| 103 |
+
|
| 104 |
+
Returns:
|
| 105 |
+
Count of non-null features
|
| 106 |
+
"""
|
| 107 |
+
return sum(1 for v in state.values() if v is not None)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def is_ready_for_prediction(state: Dict[str, Any], min_features: int = 14) -> bool:
|
| 111 |
+
"""
|
| 112 |
+
Check if enough features collected for reliable prediction.
|
| 113 |
+
|
| 114 |
+
Args:
|
| 115 |
+
state: Dictionary with feature values
|
| 116 |
+
min_features: Minimum features needed (default 14/16)
|
| 117 |
+
|
| 118 |
+
Returns:
|
| 119 |
+
True if ready for prediction
|
| 120 |
+
"""
|
| 121 |
+
collected = count_collected_features(state)
|
| 122 |
+
return collected >= min_features
|
app/services/llm_extractor.py
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
import re
|
| 5 |
+
from groq import Groq
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
|
| 8 |
+
from app.config import GROQ_API_KEY, GROQ_MODEL, GROQ_TEMPERATURE, DEFAULT_MODEL_FEATURES
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
# Load environment variables
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
# Initialize client (GLOBAL)
|
| 16 |
+
try:
|
| 17 |
+
client = Groq(api_key=GROQ_API_KEY)
|
| 18 |
+
logger.info("β
Groq client initialized")
|
| 19 |
+
except Exception as e:
|
| 20 |
+
logger.error(f"β Failed to initialize Groq client: {e}")
|
| 21 |
+
client = None
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _extract_with_regex(user_input: str, aggressive: bool = True) -> dict:
|
| 25 |
+
"""
|
| 26 |
+
SMART regex-based extraction with flexible patterns.
|
| 27 |
+
Handles: verb tenses (is/was/being), connecting words (a/up to/hit),
|
| 28 |
+
corrections (put 0/actually/not X), and multiple features in one sentence.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
user_input: User's text input
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
Dictionary with extracted features
|
| 35 |
+
"""
|
| 36 |
+
result = {feature: None for feature in DEFAULT_MODEL_FEATURES}
|
| 37 |
+
text = user_input.lower()
|
| 38 |
+
|
| 39 |
+
# Helper function to extract a number safely with ultra-flexible matching
|
| 40 |
+
def extract_number(text, keyword_pattern, value_type=int, min_val=None, max_val=None, allow_float=False):
|
| 41 |
+
"""Extract number with MAXIMUM flexibility for medical terminology.
|
| 42 |
+
Handles: all verb tenses, multiple connectors, adverbs, typos, and word order variations."""
|
| 43 |
+
|
| 44 |
+
# Comprehensive verb tenses and action words
|
| 45 |
+
verbs = r'(?:is|was|were|being|am|are|equals|measure|measured|measures|rate|rated|rates|hit|reached|have|has|say|saying|said|indicate|indicates)'
|
| 46 |
+
|
| 47 |
+
# Connector words for maximum flexibility
|
| 48 |
+
connectors = r'(?:up\s+to|actually|exactly|around|approximately|about|of|in|as|like|nearly|roughly)\s*'
|
| 49 |
+
|
| 50 |
+
# Optional article before number
|
| 51 |
+
article = r'(?:a|an|the)?\s*'
|
| 52 |
+
|
| 53 |
+
# Modifier words that can appear between keyword and verb
|
| 54 |
+
modifiers = r'(?:\s+(?:level|score|quality|quantity|range|reading|measurement|value))?'
|
| 55 |
+
|
| 56 |
+
# All possible patterns, ordered by specificity
|
| 57 |
+
patterns = [
|
| 58 |
+
# Pattern 0: keyword + [level/score/etc] + verb + connectors + article + number (MOST FLEXIBLE!)
|
| 59 |
+
rf'{keyword_pattern}{modifiers}\s+(?:{verbs})\s+(?:{connectors})?{article}(\d{{1,3}}(?:\.\d{{1,2}})?)',
|
| 60 |
+
|
| 61 |
+
# Pattern 1: keyword + verb + connectors + article + number
|
| 62 |
+
rf'{keyword_pattern}\s+(?:{verbs})\s+(?:{connectors})?{article}(\d{{1,3}}(?:\.\d{{1,2}})?)',
|
| 63 |
+
|
| 64 |
+
# Pattern 2: keyword + connector words (without verb) + article + number
|
| 65 |
+
rf'{keyword_pattern}\s+(?:{connectors}){article}(\d{{1,3}}(?:\.\d{{1,2}})?)',
|
| 66 |
+
|
| 67 |
+
# Pattern 3: prefix + keyword + [level] + verb + connectors + article + number
|
| 68 |
+
rf'(?:my|his|her|the|your)\s+{keyword_pattern}{modifiers}\s+(?:{verbs})\s+(?:{connectors})?{article}(\d{{1,3}}(?:\.\d{{1,2}})?)',
|
| 69 |
+
|
| 70 |
+
# Pattern 4: keyword + direct number
|
| 71 |
+
rf'{keyword_pattern}\s+(\d{{1,3}}(?:\.\d{{1,2}})?)',
|
| 72 |
+
|
| 73 |
+
# Pattern 5: prefix + keyword + number
|
| 74 |
+
rf'(?:my|his|her|the|your)\s+{keyword_pattern}\s+{article}(\d{{1,3}}(?:\.\d{{1,2}})?)',
|
| 75 |
+
|
| 76 |
+
# Pattern 6: "let's say number" or "say number" pattern
|
| 77 |
+
rf'(?:let\'s\s+)?say\s+(?:an?\s+)?(\d{{1,3}}(?:\.\d{{1,2}})?)\s+(?:out\s+of\s+10|for\s+{keyword_pattern})?',
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
for pattern in patterns:
|
| 81 |
+
try:
|
| 82 |
+
match = re.search(pattern, text)
|
| 83 |
+
if match:
|
| 84 |
+
val_str = match.group(1)
|
| 85 |
+
val = float(val_str) if (allow_float or '.' in val_str) else int(val_str)
|
| 86 |
+
if min_val is not None and max_val is not None:
|
| 87 |
+
if min_val <= val <= max_val:
|
| 88 |
+
return val
|
| 89 |
+
elif min_val is not None and val >= min_val:
|
| 90 |
+
return val
|
| 91 |
+
elif max_val is not None and val <= max_val:
|
| 92 |
+
return val
|
| 93 |
+
else:
|
| 94 |
+
return val if not (min_val or max_val) else None
|
| 95 |
+
except (ValueError, IndexError, AttributeError, TypeError):
|
| 96 |
+
continue
|
| 97 |
+
return None
|
| 98 |
+
|
| 99 |
+
try:
|
| 100 |
+
# ===== AGE =====
|
| 101 |
+
# Handle multiple formats: "i'm 45", "age 45", "45 years old", "45-year-old", "45 year old"
|
| 102 |
+
age_patterns = [
|
| 103 |
+
# "45 years old" or "45 year old"
|
| 104 |
+
r'(\d{1,3})\s+years?\s+old',
|
| 105 |
+
# "45-year-old" or "45year old"
|
| 106 |
+
r'(\d{1,3})\s*-?\s*years?\s*-?\s*old',
|
| 107 |
+
# Using the flexible extractor for other formats
|
| 108 |
+
r'(?:age|i\s+(?:am|\'m))\s+(\d{1,3})',
|
| 109 |
+
# "I'm 45" or "I am 45"
|
| 110 |
+
r'(?:i\'m|i\s+am)\s+(\d{1,3})',
|
| 111 |
+
]
|
| 112 |
+
age_val = None
|
| 113 |
+
for age_pat in age_patterns:
|
| 114 |
+
try:
|
| 115 |
+
age_match = re.search(age_pat, text)
|
| 116 |
+
if age_match:
|
| 117 |
+
age_candidate = int(age_match.group(1))
|
| 118 |
+
if 18 <= age_candidate <= 100:
|
| 119 |
+
age_val = float(age_candidate)
|
| 120 |
+
break
|
| 121 |
+
except (IndexError, AttributeError, ValueError):
|
| 122 |
+
continue
|
| 123 |
+
result["Age"] = age_val
|
| 124 |
+
|
| 125 |
+
# ===== GLUCOSE =====
|
| 126 |
+
glucose_val = extract_number(text, r'(?:glucose|blood\s+sugar)', min_val=70, max_val=400)
|
| 127 |
+
result["Glucose"] = float(glucose_val) if glucose_val else None
|
| 128 |
+
|
| 129 |
+
# ===== HBA1C =====
|
| 130 |
+
# Handles: "HbA1c was exactly 5.1%", "HbA1c up to 6.5", "my HbA1c is 5%", "my hba1c level is 10%"
|
| 131 |
+
hba1c_patterns = [
|
| 132 |
+
# "my hba1c level is 10%" or "hba1c level is 6%"
|
| 133 |
+
r'(?:my\s+)?hba1c\s+(?:level)?\s+(?:is|was|being)?\s+(?:exactly|around|approximately)?\s+(?:up\s+to\s+)?(\d{1,2}(?:\.\d{1,2})?)',
|
| 134 |
+
# "HbA1c was exactly 5.1%"
|
| 135 |
+
r'hba1c\s+(?:is|was|being)?\s+(?:exactly|around|approximately)?\s+(?:up\s+to\s+)?(\d{1,2}(?:\.\d{1,2})?)',
|
| 136 |
+
# "my HbA1c is 5.1"
|
| 137 |
+
r'my\s+hba1c\s+(?:is|was)?\s+(?:exactly\s+)?(\d{1,2}(?:\.\d{1,2})?)',
|
| 138 |
+
# Direct "HbA1c 5.1"
|
| 139 |
+
r'hba1c\s+(\d{1,2}(?:\.\d{1,2})?)',
|
| 140 |
+
# Just "5.1%" or "6%" after context (when HbA1c is implied)
|
| 141 |
+
r'(?:hba1c\s+)?(?:level\s+)?(?:of\s+)?(\d{1,2}(?:\.\d{1,2})?)%',
|
| 142 |
+
]
|
| 143 |
+
for hba_pat in hba1c_patterns:
|
| 144 |
+
try:
|
| 145 |
+
hba_match = re.search(hba_pat, text)
|
| 146 |
+
if hba_match:
|
| 147 |
+
hba = float(hba_match.group(1))
|
| 148 |
+
if 3 <= hba <= 15:
|
| 149 |
+
result["HbA1c"] = float(hba)
|
| 150 |
+
break
|
| 151 |
+
except (IndexError, AttributeError):
|
| 152 |
+
continue
|
| 153 |
+
|
| 154 |
+
# ===== BMI =====
|
| 155 |
+
bmi_patterns = [
|
| 156 |
+
r'bmi\s+(?:is\s+)?(\d{1,2})',
|
| 157 |
+
r'my\s+bmi\s+(?:is\s+)?(\d{1,2})',
|
| 158 |
+
r'body\s+mass\s+index\s+(?:is\s+)?(\d{1,2})',
|
| 159 |
+
]
|
| 160 |
+
for bmi_pat in bmi_patterns:
|
| 161 |
+
bmi_match = re.search(bmi_pat, text)
|
| 162 |
+
if bmi_match:
|
| 163 |
+
bmi = int(bmi_match.group(1))
|
| 164 |
+
if 10 <= bmi <= 60:
|
| 165 |
+
result["BMI"] = float(bmi)
|
| 166 |
+
break
|
| 167 |
+
|
| 168 |
+
# ===== CHOLESTEROL =====
|
| 169 |
+
chol_val = extract_number(text, r'cholesterol', min_val=100, max_val=400)
|
| 170 |
+
result["Cholesterol"] = float(chol_val) if chol_val else None
|
| 171 |
+
|
| 172 |
+
# ===== TRIGLYCERIDES =====
|
| 173 |
+
trig_val = extract_number(text, r'triglycerides', min_val=20, max_val=500)
|
| 174 |
+
result["Triglycerides"] = float(trig_val) if trig_val else None
|
| 175 |
+
|
| 176 |
+
# ===== BLOOD PRESSURE =====
|
| 177 |
+
# Special handling for BP: "120/80" or just systolic "160"
|
| 178 |
+
bp_patterns = [
|
| 179 |
+
# "blood pressure usually measures around 115"
|
| 180 |
+
r'(?:blood\s+pressure|bp)\s+(?:usually\s+)?(?:measures|measured|is|was|being)?\s+(?:around\s+|about\s+|approximately\s+)?(\d{2,3})',
|
| 181 |
+
# "blood pressure is/was 140"
|
| 182 |
+
r'(?:blood\s+pressure|bp)\s+(?:is|was|hit|reached|being)?\s+(?:around\s+|up\s+to\s+)?(\d{2,3})',
|
| 183 |
+
# "my blood pressure 115"
|
| 184 |
+
r'my\s+(?:blood\s+pressure|bp)\s+(?:is|was)?\s+(?:around\s+)?(\d{2,3})',
|
| 185 |
+
# "BP 130"
|
| 186 |
+
r'bp\s+(\d{2,3})',
|
| 187 |
+
# Systolic/diastolic format
|
| 188 |
+
r'(?:blood\s+pressure|bp)\s+(?:is|was)?\s+(?:around\s+|about\s+)?(\d{2,3})\s*/\s*(\d{2,3})',
|
| 189 |
+
]
|
| 190 |
+
for bp_pat in bp_patterns:
|
| 191 |
+
try:
|
| 192 |
+
bp_match = re.search(bp_pat, text)
|
| 193 |
+
if bp_match:
|
| 194 |
+
bp_val = int(bp_match.group(1))
|
| 195 |
+
if 60 <= bp_val <= 200:
|
| 196 |
+
result["Blood Pressure"] = float(bp_val)
|
| 197 |
+
break
|
| 198 |
+
except (IndexError, AttributeError):
|
| 199 |
+
continue
|
| 200 |
+
|
| 201 |
+
# ===== PHYSICAL ACTIVITY =====
|
| 202 |
+
# Handles: "exercise 5 hours", "play football for 4 hours", "walk to work 15 hours", "activity is 15", "walking 3 hours"
|
| 203 |
+
activity_patterns = [
|
| 204 |
+
r'(?:physical\s+)?activity\s+(?:is|was)?\s+(?:for\s+)?(?:about\s+)?(\d{1,2})\s*(?:hours?)?',
|
| 205 |
+
r'(?:exercise|activity|workout|sport|play|football|basketball|swimming|running|walk|biking)\s+(?:for\s+)?(?:about\s+|around\s+)?(\d{1,2})\s*(?:hours?)?',
|
| 206 |
+
r'(?:walk|exercise|activity|play)ing\s+(?:for\s+)?(?:about\s+)?(\d{1,2})\s*(?:hours?)?',
|
| 207 |
+
r'(\d{1,2})\s*(?:hours?)\s+(?:of\s+)?(?:exercise|activity|walking|playing|workout)',
|
| 208 |
+
]
|
| 209 |
+
for act_pat in activity_patterns:
|
| 210 |
+
activity_match = re.search(act_pat, text)
|
| 211 |
+
if activity_match:
|
| 212 |
+
activity = int(activity_match.group(1))
|
| 213 |
+
if 0 <= activity <= 24:
|
| 214 |
+
result["Physical Activity"] = float(activity)
|
| 215 |
+
break
|
| 216 |
+
|
| 217 |
+
# ===== SLEEP HOURS =====
|
| 218 |
+
# Handles: "sleep 8 hours", "sleeping 7 hours", "sleep 7", "get 6 hours of sleep", "sleep for 5 hours"
|
| 219 |
+
sleep_patterns = [
|
| 220 |
+
# "sleep for 5 hours" or "sleep 5 hours"
|
| 221 |
+
r'(?:sleep|sleeping|get|getting)\s+(?:for\s+)?(?:about\s+|around\s+)?(\d{1,2})\s*(?:hours?)?',
|
| 222 |
+
# "5 hours of sleep" or "5 hours sleep"
|
| 223 |
+
r'(\d{1,2})\s*(?:hours?)\s+(?:of\s+)?sleep',
|
| 224 |
+
]
|
| 225 |
+
for sleep_pat in sleep_patterns:
|
| 226 |
+
sleep_match = re.search(sleep_pat, text)
|
| 227 |
+
if sleep_match:
|
| 228 |
+
sleep = int(sleep_match.group(1))
|
| 229 |
+
if 0 <= sleep <= 24:
|
| 230 |
+
result["Sleep Hours"] = float(sleep)
|
| 231 |
+
break
|
| 232 |
+
|
| 233 |
+
# ===== STRESS LEVEL =====
|
| 234 |
+
# Handles: "stress 8", "stress level is 5", "I'd rate my stress level as a 3"
|
| 235 |
+
stress_patterns = [
|
| 236 |
+
# "I'd rate my stress level as a 3 out of 10"
|
| 237 |
+
r'(?:rate|rated)\s+(?:my\s+)?stress\s+(?:level)?\s+(?:as|like)\s+(?:a|an)?\s*(\d{1,2})',
|
| 238 |
+
# "stress level is/as a 5"
|
| 239 |
+
r'stress\s+(?:level)?\s+(?:is|was|as)?\s+(?:a|an)?\s*(\d{1,2})',
|
| 240 |
+
# "my stress level in 4" (typo handling - "in" for "is")
|
| 241 |
+
r'(?:my\s+)?stress\s+(?:level)?\s+(?:in|is)\s+(?:a|an)?\s*(\d{1,2})',
|
| 242 |
+
# Direct pattern
|
| 243 |
+
r'(?:my\s+)?stress\s+(?:level)?\s+(?:a|an)?\s*(\d{1,2})',
|
| 244 |
+
]
|
| 245 |
+
for stress_pat in stress_patterns:
|
| 246 |
+
try:
|
| 247 |
+
stress_match = re.search(stress_pat, text)
|
| 248 |
+
if stress_match:
|
| 249 |
+
stress = int(stress_match.group(1))
|
| 250 |
+
if 1 <= stress <= 10:
|
| 251 |
+
result["Stress Level"] = float(stress)
|
| 252 |
+
break
|
| 253 |
+
except (IndexError, AttributeError):
|
| 254 |
+
continue
|
| 255 |
+
|
| 256 |
+
# ===== OXYGEN SATURATION =====
|
| 257 |
+
# Handles: "oxygen saturation 90%", "oxygen saturation level is 95%", "oxygen was 96%", "o2 96"
|
| 258 |
+
oxygen_patterns = [
|
| 259 |
+
# "oxygen saturation level is 95%" or "oxygen saturation is 95%"
|
| 260 |
+
r'(?:oxygen|o2|oβ)\s+(?:saturation\s+)?(?:level\s+)?(?:is|was)?\s+(?:around\s+)?(\d{1,3})(?:%)?',
|
| 261 |
+
# "oxygen saturation 90%"
|
| 262 |
+
r'(?:oxygen|o2|oβ)\s+saturation\s+(?:level\s+)?(\d{1,3})(?:%)?',
|
| 263 |
+
# "oxygen was 96%" or "o2 is 95"
|
| 264 |
+
r'(?:oxygen|o2|oβ)\s+(?:was|is|being|level)?\s+(\d{1,3})(?:%)?',
|
| 265 |
+
# "my oxygen was 90%"
|
| 266 |
+
r'my\s+oxygen\s+(?:was|is)?\s+(\d{1,3})(?:%)?',
|
| 267 |
+
# "SpO2 95" or "spo2 is 92%"
|
| 268 |
+
r'(?:spo2|spo\s*2)\s+(?:is\s+)?(\d{1,3})(?:%)?',
|
| 269 |
+
# Direct "o2 97" or "oxygen 95" (needs to be number at end)
|
| 270 |
+
r'(?:oxygen|o2|oβ)\s+(\d{1,3})$',
|
| 271 |
+
]
|
| 272 |
+
for o2_pat in oxygen_patterns:
|
| 273 |
+
o2_match = re.search(o2_pat, text)
|
| 274 |
+
if o2_match:
|
| 275 |
+
o2 = int(o2_match.group(1))
|
| 276 |
+
if 80 <= o2 <= 100:
|
| 277 |
+
result["Oxygen Saturation"] = float(o2)
|
| 278 |
+
break
|
| 279 |
+
|
| 280 |
+
# ===== LENGTH OF STAY =====
|
| 281 |
+
# Handles: "hospitalized for 2 days", "2 day hospital stay", "stay 4 days", "length of stay is 10", "i was in hospital for 5 days"
|
| 282 |
+
stay_patterns = [
|
| 283 |
+
# "length of stay is 10"
|
| 284 |
+
r'length\s+of\s+stay\s+(?:is\s+)?(\d{1,3})',
|
| 285 |
+
# "my length of stay is 10"
|
| 286 |
+
r'my\s+length\s+of\s+stay\s+(?:is\s+)?(\d{1,3})',
|
| 287 |
+
# "hospitalized for 4 days" or "hospitali... for 2 days"
|
| 288 |
+
r'(?:hospitali[z]?e?d?|in\s+hospital)\s+(?:for\s+)?(\d{1,3})\s*(?:days?)?',
|
| 289 |
+
# "stay for 5 days" or "hospital stay 4 days"
|
| 290 |
+
r'(?:hospital\s+)?stay\s+(?:for\s+)?(\d{1,3})\s*(?:days?)?',
|
| 291 |
+
# "for/in 2 days hospital/stay"
|
| 292 |
+
r'(?:for|in)\s+(\d{1,3})\s*(?:days?)\s+(?:hospital|stay)',
|
| 293 |
+
# "2 day hospital stay" or "5 days in hospital"
|
| 294 |
+
r'(\d{1,3})\s*(?:days?)\s+(?:hospital|stay|in\s+hospital)',
|
| 295 |
+
]
|
| 296 |
+
if 'hospital' in text or 'stay' in text or 'length' in text:
|
| 297 |
+
for stay_pat in stay_patterns:
|
| 298 |
+
try:
|
| 299 |
+
stay_match = re.search(stay_pat, text)
|
| 300 |
+
if stay_match:
|
| 301 |
+
stay = int(stay_match.group(1))
|
| 302 |
+
if 0 <= stay <= 365:
|
| 303 |
+
result["LengthOfStay"] = float(stay)
|
| 304 |
+
break
|
| 305 |
+
except (AttributeError, IndexError):
|
| 306 |
+
continue
|
| 307 |
+
|
| 308 |
+
# ===== SMOKING =====
|
| 309 |
+
# Explicit corrections: "put 0 for smoking", "i stopped smoking", "put 0 for that"
|
| 310 |
+
if re.search(r'(?:put|mark|set)\s+0\s+(?:for\s+)?(?:smoking|smok)', text):
|
| 311 |
+
result["Smoking"] = 0
|
| 312 |
+
elif re.search(r'(?:stopped|quit|don\'t)\s+(?:smok|smoke)', text):
|
| 313 |
+
result["Smoking"] = 0
|
| 314 |
+
elif re.search(r'non[- ]smok|nonsmoker|don\'t\s+smok|no\s+smok', text):
|
| 315 |
+
result["Smoking"] = 0
|
| 316 |
+
elif re.search(r'(?:smok|smoke|smoking|smoker)(?!\s+before)', text):
|
| 317 |
+
result["Smoking"] = 1
|
| 318 |
+
|
| 319 |
+
# ===== ALCOHOL =====
|
| 320 |
+
# Explicit: "put 0 for alcohol", "don't drink", "wine daily" (means drinking)
|
| 321 |
+
if re.search(r'(?:put|mark|set)\s+0\s+(?:for\s+)?(?:alcohol|drink)', text):
|
| 322 |
+
result["Alcohol"] = 0
|
| 323 |
+
elif re.search(r'(?:don\'t|no|don\'?t)\s+(?:drink|alcohol)', text):
|
| 324 |
+
result["Alcohol"] = 0
|
| 325 |
+
elif re.search(r'(?:wine|beer|alcohol|drink|drinking|glass|daily)', text):
|
| 326 |
+
result["Alcohol"] = 1
|
| 327 |
+
|
| 328 |
+
# ===== FAMILY HISTORY =====
|
| 329 |
+
if re.search(r'(?:no|don\'t|don\'?t)\s+(?:family\s+)?(?:history|disease|condition)', text):
|
| 330 |
+
result["Family History"] = 0
|
| 331 |
+
elif re.search(r'(?:family\s+)?(?:history|disease|condition|yes)', text):
|
| 332 |
+
result["Family History"] = 1
|
| 333 |
+
|
| 334 |
+
# ===== DIET SCORE =====
|
| 335 |
+
# Handles: "diet score is 8", "diet quality is 5", "eat healthy so say 8"
|
| 336 |
+
diet_patterns = [
|
| 337 |
+
# "diet score is/quality is 8"
|
| 338 |
+
r'diet\s+(?:score|quality|nutritional?\s+value)\s+(?:is|was)?\s+(?:an?\s+)?(\d{1,2})',
|
| 339 |
+
# "nutrition score 8"
|
| 340 |
+
r'nutrition\s+(?:score|quality)?\s+(?:is|was)?\s+(?:an?\s+)?(\d{1,2})',
|
| 341 |
+
# "say an 8" pattern for diet (contextual)
|
| 342 |
+
r'(?:diet|nutrition|healthy)\s+[^.]*\bsay\s+(?:an?\s+)?(\d{1,2})(?:\s+out\s+of\s+10)?',
|
| 343 |
+
# "let's say an 8"
|
| 344 |
+
r'let\'s\s+say\s+(?:an?\s+)?(\d{1,2})\s+(?:for\s+diet|for\s+nutrition|out\s+of\s+10)',
|
| 345 |
+
# Direct "diet 8"
|
| 346 |
+
r'diet\s+(\d{1,2})',
|
| 347 |
+
]
|
| 348 |
+
for diet_pat in diet_patterns:
|
| 349 |
+
try:
|
| 350 |
+
diet_match = re.search(diet_pat, text)
|
| 351 |
+
if diet_match:
|
| 352 |
+
diet = int(diet_match.group(1))
|
| 353 |
+
if 1 <= diet <= 10:
|
| 354 |
+
result["Diet Score"] = float(diet)
|
| 355 |
+
break
|
| 356 |
+
except (IndexError, AttributeError):
|
| 357 |
+
continue
|
| 358 |
+
|
| 359 |
+
extracted_keys = {k: v for k, v in result.items() if v is not None}
|
| 360 |
+
logger.debug(f"β
Regex extraction found: {list(extracted_keys.keys())}")
|
| 361 |
+
logger.info(f"π Regex result: {extracted_keys if extracted_keys else 'EMPTY'}")
|
| 362 |
+
return result
|
| 363 |
+
|
| 364 |
+
except Exception as e:
|
| 365 |
+
logger.error(f"Regex extraction error: {e}")
|
| 366 |
+
return {feature: None for feature in DEFAULT_MODEL_FEATURES}
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
def extract_features_from_text(user_input: str) -> dict:
|
| 370 |
+
"""
|
| 371 |
+
Uses SMART extraction: Regex first (fast), then LLM (comprehensive), with smart fallbacks.
|
| 372 |
+
Returns dictionary with all required keys (values are null if not extracted).
|
| 373 |
+
|
| 374 |
+
Args:
|
| 375 |
+
user_input: User's free-form text input
|
| 376 |
+
|
| 377 |
+
Returns:
|
| 378 |
+
Dictionary with all 16 features (values null if not found)
|
| 379 |
+
"""
|
| 380 |
+
|
| 381 |
+
if not user_input or not user_input.strip():
|
| 382 |
+
logger.warning("Empty user input received")
|
| 383 |
+
return {feature: None for feature in DEFAULT_MODEL_FEATURES}
|
| 384 |
+
|
| 385 |
+
# STEP 1: TRY REGEX FIRST (fast, reliable for common patterns)
|
| 386 |
+
regex_result = _extract_with_regex(user_input, aggressive=True)
|
| 387 |
+
extracted_keys = {k: v for k, v in regex_result.items() if v is not None}
|
| 388 |
+
logger.info(f"π Regex result: {extracted_keys if extracted_keys else 'EMPTY'}")
|
| 389 |
+
|
| 390 |
+
if any(v is not None for v in regex_result.values()):
|
| 391 |
+
logger.info(f"β
Regex extraction succeeded: {list(extracted_keys.keys())}")
|
| 392 |
+
return regex_result
|
| 393 |
+
|
| 394 |
+
# STEP 2: If regex didn't work, try LLM (only if client available)
|
| 395 |
+
if client is None:
|
| 396 |
+
logger.warning("β οΈ Groq client not initialized - returning regex results (empty)")
|
| 397 |
+
return regex_result
|
| 398 |
+
|
| 399 |
+
prompt = f"""
|
| 400 |
+
You are a medical information extraction system. Extract health data from ANY mention in the text.
|
| 401 |
+
|
| 402 |
+
Features with VALID RANGES:
|
| 403 |
+
- Age (18-100): "I am 30", "30 years old", "age 45" β extract the number
|
| 404 |
+
- Glucose (70-400): any number mentioned with glucose/blood sugar
|
| 405 |
+
- HbA1c (3-15): any number with HbA1c/hemoglobin
|
| 406 |
+
- BMI (10-60): any number with BMI/body mass
|
| 407 |
+
- Cholesterol (100-400): any number with cholesterol
|
| 408 |
+
- Triglycerides (20-500): any number with triglycerides
|
| 409 |
+
- Blood Pressure (60-200): any number with BP/blood pressure
|
| 410 |
+
- Physical Activity (0-24): any number with exercise/activity/workout hours
|
| 411 |
+
- Sleep Hours (0-24): any number with sleep/hours slept
|
| 412 |
+
- Stress Level (1-10): any number (1-10) with stress
|
| 413 |
+
- Diet Score (1-10): any number (1-10) with diet/nutrition
|
| 414 |
+
- Smoking (0 or 1): "smoke/smoking/smoker" β 1, "don't/no smoking/non-smoker" β 0, "sometimes/occasionally" β 1
|
| 415 |
+
- Alcohol (0 or 1): "drink/alcohol/drinking" β 1, "don't drink/no alcohol" β 0, "sometimes" β 1
|
| 416 |
+
- Family History (0 or 1): "family history/disease history" β 1, "no family history" β 0
|
| 417 |
+
- LengthOfStay (0-365): any number with hospital/stay/days
|
| 418 |
+
- Oxygen Saturation (80-100): any number with oxygen/O2/saturation
|
| 419 |
+
|
| 420 |
+
EXTRACTION RULES:
|
| 421 |
+
1. Return ONLY valid JSON. No markdown, no text, no explanation
|
| 422 |
+
2. Use ALL 16 keys exactly as shown
|
| 423 |
+
3. Extract ANY number from text (age, measurements, etc)
|
| 424 |
+
4. For binary features (Smoking/Alcohol/Family History): ALWAYS return 0 or 1, never null if mentioned
|
| 425 |
+
5. For "sometimes/occasionally/rarely" with Smoking/Alcohol β use 1 (yes, they do it)
|
| 426 |
+
6. If value outside range, use null
|
| 427 |
+
7. If feature not mentioned at all, use null
|
| 428 |
+
|
| 429 |
+
Examples:
|
| 430 |
+
"I'm 30 years old and I smoke sometimes" β {{"Age": 30, "Smoking": 1, ...other null...}}
|
| 431 |
+
"my name is John, I don't drink" β {{"Alcohol": 0, ...other null...}}
|
| 432 |
+
"45 with diabetes" β {{"Age": 45, ...}}
|
| 433 |
+
"I exercise 5 hours" β {{"Physical Activity": 5, ...}}
|
| 434 |
+
|
| 435 |
+
User Input: "{user_input}"
|
| 436 |
+
|
| 437 |
+
Return ONLY JSON with 16 keys:"""
|
| 438 |
+
|
| 439 |
+
try:
|
| 440 |
+
# STEP 2A: Call Groq API with timeout and error handling
|
| 441 |
+
response = client.chat.completions.create(
|
| 442 |
+
model=GROQ_MODEL,
|
| 443 |
+
messages=[
|
| 444 |
+
{"role": "system", "content": "You are a strict JSON generator. Always return valid JSON."},
|
| 445 |
+
{"role": "user", "content": prompt}
|
| 446 |
+
],
|
| 447 |
+
temperature=GROQ_TEMPERATURE,
|
| 448 |
+
max_tokens=1000,
|
| 449 |
+
timeout=30
|
| 450 |
+
)
|
| 451 |
+
|
| 452 |
+
content = response.choices[0].message.content.strip()
|
| 453 |
+
|
| 454 |
+
# Try to parse JSON
|
| 455 |
+
try:
|
| 456 |
+
data = json.loads(content)
|
| 457 |
+
logger.debug(f"β
Successfully extracted via LLM: {[k for k,v in data.items() if v]}")
|
| 458 |
+
except json.JSONDecodeError:
|
| 459 |
+
# Try removing markdown code blocks
|
| 460 |
+
logger.debug(f"First parse failed, trying fallback parsing")
|
| 461 |
+
if "```" in content:
|
| 462 |
+
content = content.split("```")[1]
|
| 463 |
+
if content.startswith("json"):
|
| 464 |
+
content = content[4:].strip()
|
| 465 |
+
|
| 466 |
+
try:
|
| 467 |
+
data = json.loads(content)
|
| 468 |
+
logger.debug(f"β
Fallback JSON parse succeeded")
|
| 469 |
+
except json.JSONDecodeError as e:
|
| 470 |
+
logger.error(f"β Could not parse JSON: {e}")
|
| 471 |
+
logger.error(f" Content was: {content[:300]}")
|
| 472 |
+
logger.warning(f"β οΈ LLM JSON parsing failed, falling back to regex results")
|
| 473 |
+
data = {feature: None for feature in DEFAULT_MODEL_FEATURES}
|
| 474 |
+
|
| 475 |
+
# Ensure all features are present
|
| 476 |
+
result = {feature: None for feature in DEFAULT_MODEL_FEATURES}
|
| 477 |
+
result.update(data)
|
| 478 |
+
|
| 479 |
+
# STEP 2B: Use regex as enhancement to LLM (fill gaps)
|
| 480 |
+
regex_data = _extract_with_regex(user_input, aggressive=True)
|
| 481 |
+
|
| 482 |
+
# Merge: LLM data takes priority, regex fills gaps
|
| 483 |
+
llm_extracted = sum(1 for v in result.values() if v is not None)
|
| 484 |
+
for feature, value in regex_data.items():
|
| 485 |
+
if value is not None and result[feature] is None:
|
| 486 |
+
result[feature] = value
|
| 487 |
+
|
| 488 |
+
regex_filled = sum(1 for v in result.values() if v is not None) - llm_extracted
|
| 489 |
+
if regex_filled > 0:
|
| 490 |
+
logger.info(f"π LLM + Regex merge: LLM found {llm_extracted}, Regex filled {regex_filled} gaps")
|
| 491 |
+
|
| 492 |
+
return result
|
| 493 |
+
|
| 494 |
+
except Exception as e:
|
| 495 |
+
error_msg = str(e)
|
| 496 |
+
if "organization_restricted" in error_msg.lower():
|
| 497 |
+
logger.error(f"β Groq API BLOCKED: Organization restricted. Check your API key and account status.")
|
| 498 |
+
elif "invalid_api_key" in error_msg.lower():
|
| 499 |
+
logger.error(f"β Groq API ERROR: Invalid or expired API key")
|
| 500 |
+
elif "rate_limit" in error_msg.lower():
|
| 501 |
+
logger.warning(f"β οΈ Groq API RATE LIMITED: Too many requests, using regex only")
|
| 502 |
+
else:
|
| 503 |
+
logger.error(f"β Groq API ERROR: {type(e).__name__}: {error_msg[:200]}")
|
| 504 |
+
|
| 505 |
+
# Fallback: Return regex results if LLM fails (don't return empty)
|
| 506 |
+
logger.info("β οΈ Falling back to regex-only extraction")
|
| 507 |
+
regex_fallback = _extract_with_regex(user_input, aggressive=True)
|
| 508 |
+
return regex_fallback
|
app/services/predictor.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import joblib
|
| 2 |
+
import logging
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import List
|
| 5 |
+
|
| 6 |
+
from app.config import MODEL_PATH, CLASS_NAMES, FEATURE_RANGES
|
| 7 |
+
from app.schemas import PredictionResponse
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class Predictor:
|
| 13 |
+
"""Load and use ML model for predictions"""
|
| 14 |
+
|
| 15 |
+
def __init__(self, model_path: str = None):
|
| 16 |
+
"""
|
| 17 |
+
Initialize predictor with model path.
|
| 18 |
+
|
| 19 |
+
Args:
|
| 20 |
+
model_path: Path to model file (default: config.MODEL_PATH)
|
| 21 |
+
"""
|
| 22 |
+
if model_path is None:
|
| 23 |
+
model_path = MODEL_PATH
|
| 24 |
+
|
| 25 |
+
self.model_path = Path(model_path)
|
| 26 |
+
self.model = None
|
| 27 |
+
self._load_model()
|
| 28 |
+
|
| 29 |
+
def _load_model(self):
|
| 30 |
+
"""Load model from disk using joblib"""
|
| 31 |
+
if not self.model_path.exists():
|
| 32 |
+
raise FileNotFoundError(f"Model not found at {self.model_path}")
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
self.model = joblib.load(self.model_path)
|
| 36 |
+
logger.info(f"β
Model loaded from {self.model_path}")
|
| 37 |
+
logger.info(f" Model type: {type(self.model).__name__}")
|
| 38 |
+
logger.info(f" Expected features: {self.model.n_features_in_}")
|
| 39 |
+
except Exception as e:
|
| 40 |
+
logger.error(f"β Failed to load model: {e}")
|
| 41 |
+
raise
|
| 42 |
+
|
| 43 |
+
def predict(self, feature_vector: List[float]) -> PredictionResponse:
|
| 44 |
+
"""
|
| 45 |
+
Make prediction on feature vector.
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
feature_vector: List of 16 floats in correct order
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
PredictionResponse with prediction, probability, risk level
|
| 52 |
+
|
| 53 |
+
Raises:
|
| 54 |
+
RuntimeError: If model not loaded
|
| 55 |
+
ValueError: If feature vector wrong size
|
| 56 |
+
"""
|
| 57 |
+
if self.model is None:
|
| 58 |
+
raise RuntimeError("Model not loaded")
|
| 59 |
+
|
| 60 |
+
if len(feature_vector) != 16:
|
| 61 |
+
raise ValueError(f"Expected 16 features, got {len(feature_vector)}")
|
| 62 |
+
|
| 63 |
+
try:
|
| 64 |
+
# Make prediction on single sample
|
| 65 |
+
# Note: sklearn expects 2D array [n_samples, n_features]
|
| 66 |
+
prediction_class = int(self.model.predict([feature_vector])[0])
|
| 67 |
+
|
| 68 |
+
# Get probability/confidence array for all classes
|
| 69 |
+
proba = self.model.predict_proba([feature_vector])[0]
|
| 70 |
+
probability = float(max(proba))
|
| 71 |
+
|
| 72 |
+
# SMART LOGIC: If top prediction is "Other/Unknown" (class 7), use second-best
|
| 73 |
+
OTHER_UNKNOWN_CLASS = 7
|
| 74 |
+
if prediction_class == OTHER_UNKNOWN_CLASS:
|
| 75 |
+
# Find second-highest confidence
|
| 76 |
+
top_two_indices = (-proba).argsort()[:2] # Get top 2 class indices
|
| 77 |
+
prediction_class = int(top_two_indices[1]) # Use second-best class
|
| 78 |
+
probability = float(proba[prediction_class]) # Get its confidence
|
| 79 |
+
logger.info(f"β οΈ Top prediction was Other/Unknown, using second-best: {CLASS_NAMES[prediction_class]}")
|
| 80 |
+
|
| 81 |
+
# Map class number to class name
|
| 82 |
+
class_name = CLASS_NAMES[prediction_class] if prediction_class < len(CLASS_NAMES) else "Unknown"
|
| 83 |
+
|
| 84 |
+
# Determine risk level based on probability
|
| 85 |
+
if probability >= 0.8:
|
| 86 |
+
risk_level = "High"
|
| 87 |
+
elif probability >= 0.6:
|
| 88 |
+
risk_level = "Medium"
|
| 89 |
+
else:
|
| 90 |
+
risk_level = "Low"
|
| 91 |
+
|
| 92 |
+
# Create explanation based on the predicted condition
|
| 93 |
+
if class_name == "Healthy":
|
| 94 |
+
explanation = f"Model predicts HEALTHY status with {probability*100:.1f}% confidence. Keep up healthy lifestyle!"
|
| 95 |
+
elif class_name == "Other/Unknown":
|
| 96 |
+
explanation = f"Model unable to classify clearly with {probability*100:.1f}% confidence. Consult healthcare provider."
|
| 97 |
+
else:
|
| 98 |
+
explanation = f"Model predicts {class_name} with {probability*100:.1f}% confidence. Please consult a healthcare professional."
|
| 99 |
+
|
| 100 |
+
result = PredictionResponse(
|
| 101 |
+
prediction=prediction_class,
|
| 102 |
+
probability=probability,
|
| 103 |
+
risk_level=risk_level,
|
| 104 |
+
explanation=explanation
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
logger.info(f"β
Prediction: {class_name} (class {prediction_class}, confidence: {probability*100:.1f}%)")
|
| 108 |
+
return result
|
| 109 |
+
|
| 110 |
+
except Exception as e:
|
| 111 |
+
logger.error(f"β Prediction failed: {e}")
|
| 112 |
+
raise
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
# Global predictor instance (loaded once)
|
| 116 |
+
_predictor_instance = None
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def get_predictor() -> Predictor:
|
| 120 |
+
"""
|
| 121 |
+
Get or create global predictor instance (lazy loading).
|
| 122 |
+
|
| 123 |
+
Returns:
|
| 124 |
+
Predictor instance
|
| 125 |
+
|
| 126 |
+
Raises:
|
| 127 |
+
FileNotFoundError: If model file not found
|
| 128 |
+
"""
|
| 129 |
+
global _predictor_instance
|
| 130 |
+
|
| 131 |
+
if _predictor_instance is None:
|
| 132 |
+
_predictor_instance = Predictor()
|
| 133 |
+
|
| 134 |
+
return _predictor_instance
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def reload_predictor() -> Predictor:
|
| 138 |
+
"""
|
| 139 |
+
Force reload of predictor (useful for testing).
|
| 140 |
+
|
| 141 |
+
Returns:
|
| 142 |
+
New Predictor instance
|
| 143 |
+
"""
|
| 144 |
+
global _predictor_instance
|
| 145 |
+
_predictor_instance = Predictor()
|
| 146 |
+
return _predictor_instance
|
app/services/session_manager.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Session Management for Medical Predictor Chatbot
|
| 3 |
+
Handles: session creation, persistence, cleanup, metadata tracking
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import logging
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from typing import Dict, Optional, Any
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
# Session storage locations
|
| 15 |
+
SESSION_DIR = Path("/tmp/deepsense_sessions")
|
| 16 |
+
METADATA_FILE = SESSION_DIR / "session_metadata.json"
|
| 17 |
+
|
| 18 |
+
# Ensure directories exist
|
| 19 |
+
SESSION_DIR.mkdir(exist_ok=True)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class SessionManager:
|
| 23 |
+
"""Manages session lifecycle, persistence, and metadata tracking"""
|
| 24 |
+
|
| 25 |
+
def __init__(self, session_dir: Path = SESSION_DIR):
|
| 26 |
+
self.session_dir = session_dir
|
| 27 |
+
self.metadata_file = METADATA_FILE
|
| 28 |
+
self._load_all_metadata()
|
| 29 |
+
|
| 30 |
+
def _load_all_metadata(self):
|
| 31 |
+
"""Load all session metadata from file"""
|
| 32 |
+
try:
|
| 33 |
+
if self.metadata_file.exists():
|
| 34 |
+
with open(self.metadata_file, 'r') as f:
|
| 35 |
+
self.all_metadata = json.load(f)
|
| 36 |
+
else:
|
| 37 |
+
self.all_metadata = {}
|
| 38 |
+
except Exception as e:
|
| 39 |
+
logger.warning(f"β οΈ Could not load session metadata: {e}")
|
| 40 |
+
self.all_metadata = {}
|
| 41 |
+
|
| 42 |
+
def _save_all_metadata(self):
|
| 43 |
+
"""Persist all session metadata to file"""
|
| 44 |
+
try:
|
| 45 |
+
with open(self.metadata_file, 'w') as f:
|
| 46 |
+
json.dump(self.all_metadata, f, indent=2)
|
| 47 |
+
except Exception as e:
|
| 48 |
+
logger.error(f"β Error saving session metadata: {e}")
|
| 49 |
+
|
| 50 |
+
def create_session(self, session_id: str, user_name: str = "User") -> Dict[str, Any]:
|
| 51 |
+
"""
|
| 52 |
+
Create a new session with metadata
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
session_id: Unique session identifier
|
| 56 |
+
user_name: User name (extracted from greeting)
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
Session metadata dict
|
| 60 |
+
"""
|
| 61 |
+
metadata = {
|
| 62 |
+
"session_id": session_id,
|
| 63 |
+
"user_name": user_name,
|
| 64 |
+
"created_at": datetime.now().isoformat(),
|
| 65 |
+
"last_accessed": datetime.now().isoformat(),
|
| 66 |
+
"features_collected": 0,
|
| 67 |
+
"status": "active",
|
| 68 |
+
"reset_count": 0
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
self.all_metadata[session_id] = metadata
|
| 72 |
+
self._save_all_metadata()
|
| 73 |
+
|
| 74 |
+
logger.info(f"β
Created session: {session_id} for {user_name}")
|
| 75 |
+
return metadata
|
| 76 |
+
|
| 77 |
+
def get_session_metadata(self, session_id: str) -> Optional[Dict[str, Any]]:
|
| 78 |
+
"""Get metadata for a specific session"""
|
| 79 |
+
return self.all_metadata.get(session_id)
|
| 80 |
+
|
| 81 |
+
def update_session(self, session_id: str, features_collected: int):
|
| 82 |
+
"""Update session metadata with current stats"""
|
| 83 |
+
if session_id in self.all_metadata:
|
| 84 |
+
self.all_metadata[session_id]["last_accessed"] = datetime.now().isoformat()
|
| 85 |
+
self.all_metadata[session_id]["features_collected"] = features_collected
|
| 86 |
+
self._save_all_metadata()
|
| 87 |
+
|
| 88 |
+
def reset_session(self, session_id: str) -> bool:
|
| 89 |
+
"""
|
| 90 |
+
Reset a session: delete session file and update metadata
|
| 91 |
+
|
| 92 |
+
Args:
|
| 93 |
+
session_id: Session to reset
|
| 94 |
+
|
| 95 |
+
Returns:
|
| 96 |
+
True if successful, False otherwise
|
| 97 |
+
"""
|
| 98 |
+
try:
|
| 99 |
+
# Delete session state file
|
| 100 |
+
session_file = self.session_dir / f"{session_id}.json"
|
| 101 |
+
if session_file.exists():
|
| 102 |
+
session_file.unlink()
|
| 103 |
+
logger.info(f"ποΈ Deleted session file: {session_file}")
|
| 104 |
+
|
| 105 |
+
# Update metadata
|
| 106 |
+
if session_id in self.all_metadata:
|
| 107 |
+
self.all_metadata[session_id]["last_accessed"] = datetime.now().isoformat()
|
| 108 |
+
self.all_metadata[session_id]["features_collected"] = 0
|
| 109 |
+
self.all_metadata[session_id]["reset_count"] += 1
|
| 110 |
+
self.all_metadata[session_id]["status"] = "reset"
|
| 111 |
+
self._save_all_metadata()
|
| 112 |
+
|
| 113 |
+
reset_count = self.all_metadata[session_id]["reset_count"]
|
| 114 |
+
logger.info(f"π Session {session_id} reset (reset count: {reset_count})")
|
| 115 |
+
|
| 116 |
+
return True
|
| 117 |
+
except Exception as e:
|
| 118 |
+
logger.error(f"β Error resetting session {session_id}: {e}")
|
| 119 |
+
return False
|
| 120 |
+
|
| 121 |
+
def cleanup_old_sessions(self, max_age_hours: int = 24):
|
| 122 |
+
"""
|
| 123 |
+
Clean up session files older than max_age_hours
|
| 124 |
+
|
| 125 |
+
Args:
|
| 126 |
+
max_age_hours: Sessions older than this are deleted
|
| 127 |
+
"""
|
| 128 |
+
try:
|
| 129 |
+
import time
|
| 130 |
+
current_time = time.time()
|
| 131 |
+
deleted_count = 0
|
| 132 |
+
|
| 133 |
+
for session_id, metadata in list(self.all_metadata.items()):
|
| 134 |
+
session_file = self.session_dir / f"{session_id}.json"
|
| 135 |
+
if session_file.exists():
|
| 136 |
+
file_age_hours = (current_time - session_file.stat().st_mtime) / 3600
|
| 137 |
+
|
| 138 |
+
if file_age_hours > max_age_hours:
|
| 139 |
+
session_file.unlink()
|
| 140 |
+
logger.info(f"ποΈ Cleaned up old session: {session_id}")
|
| 141 |
+
deleted_count += 1
|
| 142 |
+
|
| 143 |
+
if deleted_count > 0:
|
| 144 |
+
logger.info(f"π§Ή Cleaned up {deleted_count} old session(s)")
|
| 145 |
+
|
| 146 |
+
except Exception as e:
|
| 147 |
+
logger.warning(f"β οΈ Error during session cleanup: {e}")
|
| 148 |
+
|
| 149 |
+
def get_all_active_sessions(self) -> Dict[str, Dict[str, Any]]:
|
| 150 |
+
"""Get all active sessions"""
|
| 151 |
+
return {sid: meta for sid, meta in self.all_metadata.items()
|
| 152 |
+
if meta.get("status") == "active"}
|
| 153 |
+
|
| 154 |
+
def list_sessions(self) -> str:
|
| 155 |
+
"""Get formatted list of all sessions"""
|
| 156 |
+
if not self.all_metadata:
|
| 157 |
+
return "No active sessions"
|
| 158 |
+
|
| 159 |
+
lines = ["Active Sessions:"]
|
| 160 |
+
for sid, meta in self.all_metadata.items():
|
| 161 |
+
lines.append(
|
| 162 |
+
f" β’ {meta['session_id']}: {meta['user_name']} "
|
| 163 |
+
f"({meta['features_collected']}/16 features, "
|
| 164 |
+
f"reset {meta['reset_count']} times)"
|
| 165 |
+
)
|
| 166 |
+
return "\n".join(lines)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
# Global session manager instance
|
| 170 |
+
_session_manager: Optional[SessionManager] = None
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def get_session_manager() -> SessionManager:
|
| 174 |
+
"""Get or create the global session manager"""
|
| 175 |
+
global _session_manager
|
| 176 |
+
if _session_manager is None:
|
| 177 |
+
_session_manager = SessionManager()
|
| 178 |
+
return _session_manager
|
app/utils/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Utilities module for Medical Predictor Chatbot"""
|
| 2 |
+
|
| 3 |
+
from app.utils.helpers import generate_question
|
| 4 |
+
|
| 5 |
+
__all__ = ["generate_question"]
|
app/utils/helpers.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def prioritize_features(missing_features: list) -> list:
|
| 2 |
+
"""
|
| 3 |
+
Prioritize missing features for asking in natural order.
|
| 4 |
+
|
| 5 |
+
Priority: Critical Basics β Key Metrics β Lifestyle β Wellness β Medical
|
| 6 |
+
"""
|
| 7 |
+
priority_order = [
|
| 8 |
+
"Age", "Blood Pressure", "Glucose", # Critical basics
|
| 9 |
+
"BMI", "Cholesterol", "HbA1c", # Key metrics
|
| 10 |
+
"Smoking", "Alcohol", "Physical Activity", # Lifestyle
|
| 11 |
+
"Sleep Hours", "Stress Level", "Diet Score", # Wellness
|
| 12 |
+
"Triglycerides", "Family History", "Oxygen Saturation", "LengthOfStay" # Medical
|
| 13 |
+
]
|
| 14 |
+
|
| 15 |
+
# Sort missing features by priority
|
| 16 |
+
sorted_features = sorted(missing_features,
|
| 17 |
+
key=lambda x: priority_order.index(x) if x in priority_order else 999)
|
| 18 |
+
return sorted_features
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def generate_question(missing_features: list) -> str:
|
| 22 |
+
"""
|
| 23 |
+
Generate warm, empathetic questions for ONE missing feature at a time.
|
| 24 |
+
This keeps the user focused on one health metric at a time, improving UX.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
missing_features: List of missing feature names (typically just 1)
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
Formatted question string asking for the first missing item
|
| 31 |
+
"""
|
| 32 |
+
questions = {
|
| 33 |
+
"Age": "Could you tell me your age?",
|
| 34 |
+
"Glucose": "What's your typical glucose level (in mg/dL)?",
|
| 35 |
+
"Smoking": "Do you smoke, or have you smoked in the past? (yes/no)",
|
| 36 |
+
"Family History": "Is there a family history of disease or health conditions? (yes/no)",
|
| 37 |
+
"HbA1c": "What's your HbA1c level (%)? (This measures average blood sugar over 3 months)",
|
| 38 |
+
"Diet Score": "How would you rate your overall diet quality on a scale of 1-10?",
|
| 39 |
+
"Alcohol": "Do you consume alcohol regularly? (yes/no)",
|
| 40 |
+
"Physical Activity": "How many hours per week do you typically exercise or stay physically active?",
|
| 41 |
+
"Blood Pressure": "What's your blood pressure reading? (e.g., 120/80)",
|
| 42 |
+
"BMI": "What's your BMI (Body Mass Index)?",
|
| 43 |
+
"Cholesterol": "What's your cholesterol level (in mg/dL)?",
|
| 44 |
+
"Sleep Hours": "How many hours of sleep do you typically get per night?",
|
| 45 |
+
"Stress Level": "How would you rate your current stress level on a scale of 1-10?",
|
| 46 |
+
"Triglycerides": "What's your triglycerides level (in mg/dL)?",
|
| 47 |
+
"Oxygen Saturation": "What's your oxygen saturation level (%)? (Normal is 95-100%)",
|
| 48 |
+
"LengthOfStay": "How many days were you hospitalized, if applicable?"
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
if not missing_features:
|
| 52 |
+
return ""
|
| 53 |
+
|
| 54 |
+
# Ask for only 1 item at a time to keep user focused
|
| 55 |
+
feature = missing_features[0]
|
| 56 |
+
return f"**{feature}:** {questions.get(feature, f'Please provide {feature}')}"
|
frontend/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>Medical Diagnosis AI</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
frontend/package-lock.json
ADDED
|
@@ -0,0 +1,2720 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "medical-predictor-frontend",
|
| 3 |
+
"version": "0.0.1",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "medical-predictor-frontend",
|
| 9 |
+
"version": "0.0.1",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"axios": "^1.6.0",
|
| 12 |
+
"lucide-react": "^0.294.0",
|
| 13 |
+
"react": "^18.2.0",
|
| 14 |
+
"react-dom": "^18.2.0"
|
| 15 |
+
},
|
| 16 |
+
"devDependencies": {
|
| 17 |
+
"@types/react": "^18.2.0",
|
| 18 |
+
"@types/react-dom": "^18.2.0",
|
| 19 |
+
"@vitejs/plugin-react": "^4.2.1",
|
| 20 |
+
"autoprefixer": "^10.4.16",
|
| 21 |
+
"postcss": "^8.4.31",
|
| 22 |
+
"tailwindcss": "^3.3.0",
|
| 23 |
+
"vite": "^5.0.0"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
"node_modules/@alloc/quick-lru": {
|
| 27 |
+
"version": "5.2.0",
|
| 28 |
+
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
| 29 |
+
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
| 30 |
+
"dev": true,
|
| 31 |
+
"engines": {
|
| 32 |
+
"node": ">=10"
|
| 33 |
+
},
|
| 34 |
+
"funding": {
|
| 35 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 36 |
+
}
|
| 37 |
+
},
|
| 38 |
+
"node_modules/@babel/code-frame": {
|
| 39 |
+
"version": "7.29.0",
|
| 40 |
+
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
| 41 |
+
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
| 42 |
+
"dev": true,
|
| 43 |
+
"dependencies": {
|
| 44 |
+
"@babel/helper-validator-identifier": "^7.28.5",
|
| 45 |
+
"js-tokens": "^4.0.0",
|
| 46 |
+
"picocolors": "^1.1.1"
|
| 47 |
+
},
|
| 48 |
+
"engines": {
|
| 49 |
+
"node": ">=6.9.0"
|
| 50 |
+
}
|
| 51 |
+
},
|
| 52 |
+
"node_modules/@babel/compat-data": {
|
| 53 |
+
"version": "7.29.0",
|
| 54 |
+
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
|
| 55 |
+
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
|
| 56 |
+
"dev": true,
|
| 57 |
+
"engines": {
|
| 58 |
+
"node": ">=6.9.0"
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"node_modules/@babel/core": {
|
| 62 |
+
"version": "7.29.0",
|
| 63 |
+
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
| 64 |
+
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
| 65 |
+
"dev": true,
|
| 66 |
+
"dependencies": {
|
| 67 |
+
"@babel/code-frame": "^7.29.0",
|
| 68 |
+
"@babel/generator": "^7.29.0",
|
| 69 |
+
"@babel/helper-compilation-targets": "^7.28.6",
|
| 70 |
+
"@babel/helper-module-transforms": "^7.28.6",
|
| 71 |
+
"@babel/helpers": "^7.28.6",
|
| 72 |
+
"@babel/parser": "^7.29.0",
|
| 73 |
+
"@babel/template": "^7.28.6",
|
| 74 |
+
"@babel/traverse": "^7.29.0",
|
| 75 |
+
"@babel/types": "^7.29.0",
|
| 76 |
+
"@jridgewell/remapping": "^2.3.5",
|
| 77 |
+
"convert-source-map": "^2.0.0",
|
| 78 |
+
"debug": "^4.1.0",
|
| 79 |
+
"gensync": "^1.0.0-beta.2",
|
| 80 |
+
"json5": "^2.2.3",
|
| 81 |
+
"semver": "^6.3.1"
|
| 82 |
+
},
|
| 83 |
+
"engines": {
|
| 84 |
+
"node": ">=6.9.0"
|
| 85 |
+
},
|
| 86 |
+
"funding": {
|
| 87 |
+
"type": "opencollective",
|
| 88 |
+
"url": "https://opencollective.com/babel"
|
| 89 |
+
}
|
| 90 |
+
},
|
| 91 |
+
"node_modules/@babel/generator": {
|
| 92 |
+
"version": "7.29.1",
|
| 93 |
+
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
|
| 94 |
+
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
|
| 95 |
+
"dev": true,
|
| 96 |
+
"dependencies": {
|
| 97 |
+
"@babel/parser": "^7.29.0",
|
| 98 |
+
"@babel/types": "^7.29.0",
|
| 99 |
+
"@jridgewell/gen-mapping": "^0.3.12",
|
| 100 |
+
"@jridgewell/trace-mapping": "^0.3.28",
|
| 101 |
+
"jsesc": "^3.0.2"
|
| 102 |
+
},
|
| 103 |
+
"engines": {
|
| 104 |
+
"node": ">=6.9.0"
|
| 105 |
+
}
|
| 106 |
+
},
|
| 107 |
+
"node_modules/@babel/helper-compilation-targets": {
|
| 108 |
+
"version": "7.28.6",
|
| 109 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
|
| 110 |
+
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
|
| 111 |
+
"dev": true,
|
| 112 |
+
"dependencies": {
|
| 113 |
+
"@babel/compat-data": "^7.28.6",
|
| 114 |
+
"@babel/helper-validator-option": "^7.27.1",
|
| 115 |
+
"browserslist": "^4.24.0",
|
| 116 |
+
"lru-cache": "^5.1.1",
|
| 117 |
+
"semver": "^6.3.1"
|
| 118 |
+
},
|
| 119 |
+
"engines": {
|
| 120 |
+
"node": ">=6.9.0"
|
| 121 |
+
}
|
| 122 |
+
},
|
| 123 |
+
"node_modules/@babel/helper-globals": {
|
| 124 |
+
"version": "7.28.0",
|
| 125 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
| 126 |
+
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
| 127 |
+
"dev": true,
|
| 128 |
+
"engines": {
|
| 129 |
+
"node": ">=6.9.0"
|
| 130 |
+
}
|
| 131 |
+
},
|
| 132 |
+
"node_modules/@babel/helper-module-imports": {
|
| 133 |
+
"version": "7.28.6",
|
| 134 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
|
| 135 |
+
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
|
| 136 |
+
"dev": true,
|
| 137 |
+
"dependencies": {
|
| 138 |
+
"@babel/traverse": "^7.28.6",
|
| 139 |
+
"@babel/types": "^7.28.6"
|
| 140 |
+
},
|
| 141 |
+
"engines": {
|
| 142 |
+
"node": ">=6.9.0"
|
| 143 |
+
}
|
| 144 |
+
},
|
| 145 |
+
"node_modules/@babel/helper-module-transforms": {
|
| 146 |
+
"version": "7.28.6",
|
| 147 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
|
| 148 |
+
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
|
| 149 |
+
"dev": true,
|
| 150 |
+
"dependencies": {
|
| 151 |
+
"@babel/helper-module-imports": "^7.28.6",
|
| 152 |
+
"@babel/helper-validator-identifier": "^7.28.5",
|
| 153 |
+
"@babel/traverse": "^7.28.6"
|
| 154 |
+
},
|
| 155 |
+
"engines": {
|
| 156 |
+
"node": ">=6.9.0"
|
| 157 |
+
},
|
| 158 |
+
"peerDependencies": {
|
| 159 |
+
"@babel/core": "^7.0.0"
|
| 160 |
+
}
|
| 161 |
+
},
|
| 162 |
+
"node_modules/@babel/helper-plugin-utils": {
|
| 163 |
+
"version": "7.28.6",
|
| 164 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
|
| 165 |
+
"integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
|
| 166 |
+
"dev": true,
|
| 167 |
+
"engines": {
|
| 168 |
+
"node": ">=6.9.0"
|
| 169 |
+
}
|
| 170 |
+
},
|
| 171 |
+
"node_modules/@babel/helper-string-parser": {
|
| 172 |
+
"version": "7.27.1",
|
| 173 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
| 174 |
+
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
| 175 |
+
"dev": true,
|
| 176 |
+
"engines": {
|
| 177 |
+
"node": ">=6.9.0"
|
| 178 |
+
}
|
| 179 |
+
},
|
| 180 |
+
"node_modules/@babel/helper-validator-identifier": {
|
| 181 |
+
"version": "7.28.5",
|
| 182 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
| 183 |
+
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
| 184 |
+
"dev": true,
|
| 185 |
+
"engines": {
|
| 186 |
+
"node": ">=6.9.0"
|
| 187 |
+
}
|
| 188 |
+
},
|
| 189 |
+
"node_modules/@babel/helper-validator-option": {
|
| 190 |
+
"version": "7.27.1",
|
| 191 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
| 192 |
+
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
| 193 |
+
"dev": true,
|
| 194 |
+
"engines": {
|
| 195 |
+
"node": ">=6.9.0"
|
| 196 |
+
}
|
| 197 |
+
},
|
| 198 |
+
"node_modules/@babel/helpers": {
|
| 199 |
+
"version": "7.29.2",
|
| 200 |
+
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
|
| 201 |
+
"integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
|
| 202 |
+
"dev": true,
|
| 203 |
+
"dependencies": {
|
| 204 |
+
"@babel/template": "^7.28.6",
|
| 205 |
+
"@babel/types": "^7.29.0"
|
| 206 |
+
},
|
| 207 |
+
"engines": {
|
| 208 |
+
"node": ">=6.9.0"
|
| 209 |
+
}
|
| 210 |
+
},
|
| 211 |
+
"node_modules/@babel/parser": {
|
| 212 |
+
"version": "7.29.2",
|
| 213 |
+
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
|
| 214 |
+
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
|
| 215 |
+
"dev": true,
|
| 216 |
+
"dependencies": {
|
| 217 |
+
"@babel/types": "^7.29.0"
|
| 218 |
+
},
|
| 219 |
+
"bin": {
|
| 220 |
+
"parser": "bin/babel-parser.js"
|
| 221 |
+
},
|
| 222 |
+
"engines": {
|
| 223 |
+
"node": ">=6.0.0"
|
| 224 |
+
}
|
| 225 |
+
},
|
| 226 |
+
"node_modules/@babel/plugin-transform-react-jsx-self": {
|
| 227 |
+
"version": "7.27.1",
|
| 228 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
|
| 229 |
+
"integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
|
| 230 |
+
"dev": true,
|
| 231 |
+
"dependencies": {
|
| 232 |
+
"@babel/helper-plugin-utils": "^7.27.1"
|
| 233 |
+
},
|
| 234 |
+
"engines": {
|
| 235 |
+
"node": ">=6.9.0"
|
| 236 |
+
},
|
| 237 |
+
"peerDependencies": {
|
| 238 |
+
"@babel/core": "^7.0.0-0"
|
| 239 |
+
}
|
| 240 |
+
},
|
| 241 |
+
"node_modules/@babel/plugin-transform-react-jsx-source": {
|
| 242 |
+
"version": "7.27.1",
|
| 243 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
|
| 244 |
+
"integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
|
| 245 |
+
"dev": true,
|
| 246 |
+
"dependencies": {
|
| 247 |
+
"@babel/helper-plugin-utils": "^7.27.1"
|
| 248 |
+
},
|
| 249 |
+
"engines": {
|
| 250 |
+
"node": ">=6.9.0"
|
| 251 |
+
},
|
| 252 |
+
"peerDependencies": {
|
| 253 |
+
"@babel/core": "^7.0.0-0"
|
| 254 |
+
}
|
| 255 |
+
},
|
| 256 |
+
"node_modules/@babel/template": {
|
| 257 |
+
"version": "7.28.6",
|
| 258 |
+
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
| 259 |
+
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
|
| 260 |
+
"dev": true,
|
| 261 |
+
"dependencies": {
|
| 262 |
+
"@babel/code-frame": "^7.28.6",
|
| 263 |
+
"@babel/parser": "^7.28.6",
|
| 264 |
+
"@babel/types": "^7.28.6"
|
| 265 |
+
},
|
| 266 |
+
"engines": {
|
| 267 |
+
"node": ">=6.9.0"
|
| 268 |
+
}
|
| 269 |
+
},
|
| 270 |
+
"node_modules/@babel/traverse": {
|
| 271 |
+
"version": "7.29.0",
|
| 272 |
+
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
|
| 273 |
+
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
|
| 274 |
+
"dev": true,
|
| 275 |
+
"dependencies": {
|
| 276 |
+
"@babel/code-frame": "^7.29.0",
|
| 277 |
+
"@babel/generator": "^7.29.0",
|
| 278 |
+
"@babel/helper-globals": "^7.28.0",
|
| 279 |
+
"@babel/parser": "^7.29.0",
|
| 280 |
+
"@babel/template": "^7.28.6",
|
| 281 |
+
"@babel/types": "^7.29.0",
|
| 282 |
+
"debug": "^4.3.1"
|
| 283 |
+
},
|
| 284 |
+
"engines": {
|
| 285 |
+
"node": ">=6.9.0"
|
| 286 |
+
}
|
| 287 |
+
},
|
| 288 |
+
"node_modules/@babel/types": {
|
| 289 |
+
"version": "7.29.0",
|
| 290 |
+
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
| 291 |
+
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
| 292 |
+
"dev": true,
|
| 293 |
+
"dependencies": {
|
| 294 |
+
"@babel/helper-string-parser": "^7.27.1",
|
| 295 |
+
"@babel/helper-validator-identifier": "^7.28.5"
|
| 296 |
+
},
|
| 297 |
+
"engines": {
|
| 298 |
+
"node": ">=6.9.0"
|
| 299 |
+
}
|
| 300 |
+
},
|
| 301 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 302 |
+
"version": "0.21.5",
|
| 303 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
| 304 |
+
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
| 305 |
+
"cpu": [
|
| 306 |
+
"ppc64"
|
| 307 |
+
],
|
| 308 |
+
"dev": true,
|
| 309 |
+
"optional": true,
|
| 310 |
+
"os": [
|
| 311 |
+
"aix"
|
| 312 |
+
],
|
| 313 |
+
"engines": {
|
| 314 |
+
"node": ">=12"
|
| 315 |
+
}
|
| 316 |
+
},
|
| 317 |
+
"node_modules/@esbuild/android-arm": {
|
| 318 |
+
"version": "0.21.5",
|
| 319 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
| 320 |
+
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
| 321 |
+
"cpu": [
|
| 322 |
+
"arm"
|
| 323 |
+
],
|
| 324 |
+
"dev": true,
|
| 325 |
+
"optional": true,
|
| 326 |
+
"os": [
|
| 327 |
+
"android"
|
| 328 |
+
],
|
| 329 |
+
"engines": {
|
| 330 |
+
"node": ">=12"
|
| 331 |
+
}
|
| 332 |
+
},
|
| 333 |
+
"node_modules/@esbuild/android-arm64": {
|
| 334 |
+
"version": "0.21.5",
|
| 335 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
| 336 |
+
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
| 337 |
+
"cpu": [
|
| 338 |
+
"arm64"
|
| 339 |
+
],
|
| 340 |
+
"dev": true,
|
| 341 |
+
"optional": true,
|
| 342 |
+
"os": [
|
| 343 |
+
"android"
|
| 344 |
+
],
|
| 345 |
+
"engines": {
|
| 346 |
+
"node": ">=12"
|
| 347 |
+
}
|
| 348 |
+
},
|
| 349 |
+
"node_modules/@esbuild/android-x64": {
|
| 350 |
+
"version": "0.21.5",
|
| 351 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
| 352 |
+
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
| 353 |
+
"cpu": [
|
| 354 |
+
"x64"
|
| 355 |
+
],
|
| 356 |
+
"dev": true,
|
| 357 |
+
"optional": true,
|
| 358 |
+
"os": [
|
| 359 |
+
"android"
|
| 360 |
+
],
|
| 361 |
+
"engines": {
|
| 362 |
+
"node": ">=12"
|
| 363 |
+
}
|
| 364 |
+
},
|
| 365 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 366 |
+
"version": "0.21.5",
|
| 367 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
| 368 |
+
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
| 369 |
+
"cpu": [
|
| 370 |
+
"arm64"
|
| 371 |
+
],
|
| 372 |
+
"dev": true,
|
| 373 |
+
"optional": true,
|
| 374 |
+
"os": [
|
| 375 |
+
"darwin"
|
| 376 |
+
],
|
| 377 |
+
"engines": {
|
| 378 |
+
"node": ">=12"
|
| 379 |
+
}
|
| 380 |
+
},
|
| 381 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 382 |
+
"version": "0.21.5",
|
| 383 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
| 384 |
+
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
| 385 |
+
"cpu": [
|
| 386 |
+
"x64"
|
| 387 |
+
],
|
| 388 |
+
"dev": true,
|
| 389 |
+
"optional": true,
|
| 390 |
+
"os": [
|
| 391 |
+
"darwin"
|
| 392 |
+
],
|
| 393 |
+
"engines": {
|
| 394 |
+
"node": ">=12"
|
| 395 |
+
}
|
| 396 |
+
},
|
| 397 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 398 |
+
"version": "0.21.5",
|
| 399 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
| 400 |
+
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
| 401 |
+
"cpu": [
|
| 402 |
+
"arm64"
|
| 403 |
+
],
|
| 404 |
+
"dev": true,
|
| 405 |
+
"optional": true,
|
| 406 |
+
"os": [
|
| 407 |
+
"freebsd"
|
| 408 |
+
],
|
| 409 |
+
"engines": {
|
| 410 |
+
"node": ">=12"
|
| 411 |
+
}
|
| 412 |
+
},
|
| 413 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 414 |
+
"version": "0.21.5",
|
| 415 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
| 416 |
+
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
| 417 |
+
"cpu": [
|
| 418 |
+
"x64"
|
| 419 |
+
],
|
| 420 |
+
"dev": true,
|
| 421 |
+
"optional": true,
|
| 422 |
+
"os": [
|
| 423 |
+
"freebsd"
|
| 424 |
+
],
|
| 425 |
+
"engines": {
|
| 426 |
+
"node": ">=12"
|
| 427 |
+
}
|
| 428 |
+
},
|
| 429 |
+
"node_modules/@esbuild/linux-arm": {
|
| 430 |
+
"version": "0.21.5",
|
| 431 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
| 432 |
+
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
| 433 |
+
"cpu": [
|
| 434 |
+
"arm"
|
| 435 |
+
],
|
| 436 |
+
"dev": true,
|
| 437 |
+
"optional": true,
|
| 438 |
+
"os": [
|
| 439 |
+
"linux"
|
| 440 |
+
],
|
| 441 |
+
"engines": {
|
| 442 |
+
"node": ">=12"
|
| 443 |
+
}
|
| 444 |
+
},
|
| 445 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 446 |
+
"version": "0.21.5",
|
| 447 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
| 448 |
+
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
| 449 |
+
"cpu": [
|
| 450 |
+
"arm64"
|
| 451 |
+
],
|
| 452 |
+
"dev": true,
|
| 453 |
+
"optional": true,
|
| 454 |
+
"os": [
|
| 455 |
+
"linux"
|
| 456 |
+
],
|
| 457 |
+
"engines": {
|
| 458 |
+
"node": ">=12"
|
| 459 |
+
}
|
| 460 |
+
},
|
| 461 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 462 |
+
"version": "0.21.5",
|
| 463 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
| 464 |
+
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
| 465 |
+
"cpu": [
|
| 466 |
+
"ia32"
|
| 467 |
+
],
|
| 468 |
+
"dev": true,
|
| 469 |
+
"optional": true,
|
| 470 |
+
"os": [
|
| 471 |
+
"linux"
|
| 472 |
+
],
|
| 473 |
+
"engines": {
|
| 474 |
+
"node": ">=12"
|
| 475 |
+
}
|
| 476 |
+
},
|
| 477 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 478 |
+
"version": "0.21.5",
|
| 479 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
| 480 |
+
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
| 481 |
+
"cpu": [
|
| 482 |
+
"loong64"
|
| 483 |
+
],
|
| 484 |
+
"dev": true,
|
| 485 |
+
"optional": true,
|
| 486 |
+
"os": [
|
| 487 |
+
"linux"
|
| 488 |
+
],
|
| 489 |
+
"engines": {
|
| 490 |
+
"node": ">=12"
|
| 491 |
+
}
|
| 492 |
+
},
|
| 493 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 494 |
+
"version": "0.21.5",
|
| 495 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
| 496 |
+
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
| 497 |
+
"cpu": [
|
| 498 |
+
"mips64el"
|
| 499 |
+
],
|
| 500 |
+
"dev": true,
|
| 501 |
+
"optional": true,
|
| 502 |
+
"os": [
|
| 503 |
+
"linux"
|
| 504 |
+
],
|
| 505 |
+
"engines": {
|
| 506 |
+
"node": ">=12"
|
| 507 |
+
}
|
| 508 |
+
},
|
| 509 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 510 |
+
"version": "0.21.5",
|
| 511 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
| 512 |
+
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
| 513 |
+
"cpu": [
|
| 514 |
+
"ppc64"
|
| 515 |
+
],
|
| 516 |
+
"dev": true,
|
| 517 |
+
"optional": true,
|
| 518 |
+
"os": [
|
| 519 |
+
"linux"
|
| 520 |
+
],
|
| 521 |
+
"engines": {
|
| 522 |
+
"node": ">=12"
|
| 523 |
+
}
|
| 524 |
+
},
|
| 525 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 526 |
+
"version": "0.21.5",
|
| 527 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
| 528 |
+
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
| 529 |
+
"cpu": [
|
| 530 |
+
"riscv64"
|
| 531 |
+
],
|
| 532 |
+
"dev": true,
|
| 533 |
+
"optional": true,
|
| 534 |
+
"os": [
|
| 535 |
+
"linux"
|
| 536 |
+
],
|
| 537 |
+
"engines": {
|
| 538 |
+
"node": ">=12"
|
| 539 |
+
}
|
| 540 |
+
},
|
| 541 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 542 |
+
"version": "0.21.5",
|
| 543 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
| 544 |
+
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
| 545 |
+
"cpu": [
|
| 546 |
+
"s390x"
|
| 547 |
+
],
|
| 548 |
+
"dev": true,
|
| 549 |
+
"optional": true,
|
| 550 |
+
"os": [
|
| 551 |
+
"linux"
|
| 552 |
+
],
|
| 553 |
+
"engines": {
|
| 554 |
+
"node": ">=12"
|
| 555 |
+
}
|
| 556 |
+
},
|
| 557 |
+
"node_modules/@esbuild/linux-x64": {
|
| 558 |
+
"version": "0.21.5",
|
| 559 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
| 560 |
+
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
| 561 |
+
"cpu": [
|
| 562 |
+
"x64"
|
| 563 |
+
],
|
| 564 |
+
"dev": true,
|
| 565 |
+
"optional": true,
|
| 566 |
+
"os": [
|
| 567 |
+
"linux"
|
| 568 |
+
],
|
| 569 |
+
"engines": {
|
| 570 |
+
"node": ">=12"
|
| 571 |
+
}
|
| 572 |
+
},
|
| 573 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 574 |
+
"version": "0.21.5",
|
| 575 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
| 576 |
+
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
| 577 |
+
"cpu": [
|
| 578 |
+
"x64"
|
| 579 |
+
],
|
| 580 |
+
"dev": true,
|
| 581 |
+
"optional": true,
|
| 582 |
+
"os": [
|
| 583 |
+
"netbsd"
|
| 584 |
+
],
|
| 585 |
+
"engines": {
|
| 586 |
+
"node": ">=12"
|
| 587 |
+
}
|
| 588 |
+
},
|
| 589 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 590 |
+
"version": "0.21.5",
|
| 591 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
| 592 |
+
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
| 593 |
+
"cpu": [
|
| 594 |
+
"x64"
|
| 595 |
+
],
|
| 596 |
+
"dev": true,
|
| 597 |
+
"optional": true,
|
| 598 |
+
"os": [
|
| 599 |
+
"openbsd"
|
| 600 |
+
],
|
| 601 |
+
"engines": {
|
| 602 |
+
"node": ">=12"
|
| 603 |
+
}
|
| 604 |
+
},
|
| 605 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 606 |
+
"version": "0.21.5",
|
| 607 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
| 608 |
+
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
| 609 |
+
"cpu": [
|
| 610 |
+
"x64"
|
| 611 |
+
],
|
| 612 |
+
"dev": true,
|
| 613 |
+
"optional": true,
|
| 614 |
+
"os": [
|
| 615 |
+
"sunos"
|
| 616 |
+
],
|
| 617 |
+
"engines": {
|
| 618 |
+
"node": ">=12"
|
| 619 |
+
}
|
| 620 |
+
},
|
| 621 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 622 |
+
"version": "0.21.5",
|
| 623 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
| 624 |
+
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
| 625 |
+
"cpu": [
|
| 626 |
+
"arm64"
|
| 627 |
+
],
|
| 628 |
+
"dev": true,
|
| 629 |
+
"optional": true,
|
| 630 |
+
"os": [
|
| 631 |
+
"win32"
|
| 632 |
+
],
|
| 633 |
+
"engines": {
|
| 634 |
+
"node": ">=12"
|
| 635 |
+
}
|
| 636 |
+
},
|
| 637 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 638 |
+
"version": "0.21.5",
|
| 639 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
| 640 |
+
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
| 641 |
+
"cpu": [
|
| 642 |
+
"ia32"
|
| 643 |
+
],
|
| 644 |
+
"dev": true,
|
| 645 |
+
"optional": true,
|
| 646 |
+
"os": [
|
| 647 |
+
"win32"
|
| 648 |
+
],
|
| 649 |
+
"engines": {
|
| 650 |
+
"node": ">=12"
|
| 651 |
+
}
|
| 652 |
+
},
|
| 653 |
+
"node_modules/@esbuild/win32-x64": {
|
| 654 |
+
"version": "0.21.5",
|
| 655 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
| 656 |
+
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
| 657 |
+
"cpu": [
|
| 658 |
+
"x64"
|
| 659 |
+
],
|
| 660 |
+
"dev": true,
|
| 661 |
+
"optional": true,
|
| 662 |
+
"os": [
|
| 663 |
+
"win32"
|
| 664 |
+
],
|
| 665 |
+
"engines": {
|
| 666 |
+
"node": ">=12"
|
| 667 |
+
}
|
| 668 |
+
},
|
| 669 |
+
"node_modules/@jridgewell/gen-mapping": {
|
| 670 |
+
"version": "0.3.13",
|
| 671 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
| 672 |
+
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
| 673 |
+
"dev": true,
|
| 674 |
+
"dependencies": {
|
| 675 |
+
"@jridgewell/sourcemap-codec": "^1.5.0",
|
| 676 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 677 |
+
}
|
| 678 |
+
},
|
| 679 |
+
"node_modules/@jridgewell/remapping": {
|
| 680 |
+
"version": "2.3.5",
|
| 681 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
| 682 |
+
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
| 683 |
+
"dev": true,
|
| 684 |
+
"dependencies": {
|
| 685 |
+
"@jridgewell/gen-mapping": "^0.3.5",
|
| 686 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 687 |
+
}
|
| 688 |
+
},
|
| 689 |
+
"node_modules/@jridgewell/resolve-uri": {
|
| 690 |
+
"version": "3.1.2",
|
| 691 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
| 692 |
+
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
| 693 |
+
"dev": true,
|
| 694 |
+
"engines": {
|
| 695 |
+
"node": ">=6.0.0"
|
| 696 |
+
}
|
| 697 |
+
},
|
| 698 |
+
"node_modules/@jridgewell/sourcemap-codec": {
|
| 699 |
+
"version": "1.5.5",
|
| 700 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
| 701 |
+
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
| 702 |
+
"dev": true
|
| 703 |
+
},
|
| 704 |
+
"node_modules/@jridgewell/trace-mapping": {
|
| 705 |
+
"version": "0.3.31",
|
| 706 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
| 707 |
+
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
| 708 |
+
"dev": true,
|
| 709 |
+
"dependencies": {
|
| 710 |
+
"@jridgewell/resolve-uri": "^3.1.0",
|
| 711 |
+
"@jridgewell/sourcemap-codec": "^1.4.14"
|
| 712 |
+
}
|
| 713 |
+
},
|
| 714 |
+
"node_modules/@nodelib/fs.scandir": {
|
| 715 |
+
"version": "2.1.5",
|
| 716 |
+
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
| 717 |
+
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
| 718 |
+
"dev": true,
|
| 719 |
+
"dependencies": {
|
| 720 |
+
"@nodelib/fs.stat": "2.0.5",
|
| 721 |
+
"run-parallel": "^1.1.9"
|
| 722 |
+
},
|
| 723 |
+
"engines": {
|
| 724 |
+
"node": ">= 8"
|
| 725 |
+
}
|
| 726 |
+
},
|
| 727 |
+
"node_modules/@nodelib/fs.stat": {
|
| 728 |
+
"version": "2.0.5",
|
| 729 |
+
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
| 730 |
+
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
| 731 |
+
"dev": true,
|
| 732 |
+
"engines": {
|
| 733 |
+
"node": ">= 8"
|
| 734 |
+
}
|
| 735 |
+
},
|
| 736 |
+
"node_modules/@nodelib/fs.walk": {
|
| 737 |
+
"version": "1.2.8",
|
| 738 |
+
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
| 739 |
+
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
| 740 |
+
"dev": true,
|
| 741 |
+
"dependencies": {
|
| 742 |
+
"@nodelib/fs.scandir": "2.1.5",
|
| 743 |
+
"fastq": "^1.6.0"
|
| 744 |
+
},
|
| 745 |
+
"engines": {
|
| 746 |
+
"node": ">= 8"
|
| 747 |
+
}
|
| 748 |
+
},
|
| 749 |
+
"node_modules/@rolldown/pluginutils": {
|
| 750 |
+
"version": "1.0.0-beta.27",
|
| 751 |
+
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
| 752 |
+
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
|
| 753 |
+
"dev": true
|
| 754 |
+
},
|
| 755 |
+
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 756 |
+
"version": "4.60.1",
|
| 757 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
|
| 758 |
+
"integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
|
| 759 |
+
"cpu": [
|
| 760 |
+
"arm"
|
| 761 |
+
],
|
| 762 |
+
"dev": true,
|
| 763 |
+
"optional": true,
|
| 764 |
+
"os": [
|
| 765 |
+
"android"
|
| 766 |
+
]
|
| 767 |
+
},
|
| 768 |
+
"node_modules/@rollup/rollup-android-arm64": {
|
| 769 |
+
"version": "4.60.1",
|
| 770 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
|
| 771 |
+
"integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
|
| 772 |
+
"cpu": [
|
| 773 |
+
"arm64"
|
| 774 |
+
],
|
| 775 |
+
"dev": true,
|
| 776 |
+
"optional": true,
|
| 777 |
+
"os": [
|
| 778 |
+
"android"
|
| 779 |
+
]
|
| 780 |
+
},
|
| 781 |
+
"node_modules/@rollup/rollup-darwin-arm64": {
|
| 782 |
+
"version": "4.60.1",
|
| 783 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
|
| 784 |
+
"integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
|
| 785 |
+
"cpu": [
|
| 786 |
+
"arm64"
|
| 787 |
+
],
|
| 788 |
+
"dev": true,
|
| 789 |
+
"optional": true,
|
| 790 |
+
"os": [
|
| 791 |
+
"darwin"
|
| 792 |
+
]
|
| 793 |
+
},
|
| 794 |
+
"node_modules/@rollup/rollup-darwin-x64": {
|
| 795 |
+
"version": "4.60.1",
|
| 796 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
|
| 797 |
+
"integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
|
| 798 |
+
"cpu": [
|
| 799 |
+
"x64"
|
| 800 |
+
],
|
| 801 |
+
"dev": true,
|
| 802 |
+
"optional": true,
|
| 803 |
+
"os": [
|
| 804 |
+
"darwin"
|
| 805 |
+
]
|
| 806 |
+
},
|
| 807 |
+
"node_modules/@rollup/rollup-freebsd-arm64": {
|
| 808 |
+
"version": "4.60.1",
|
| 809 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
|
| 810 |
+
"integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
|
| 811 |
+
"cpu": [
|
| 812 |
+
"arm64"
|
| 813 |
+
],
|
| 814 |
+
"dev": true,
|
| 815 |
+
"optional": true,
|
| 816 |
+
"os": [
|
| 817 |
+
"freebsd"
|
| 818 |
+
]
|
| 819 |
+
},
|
| 820 |
+
"node_modules/@rollup/rollup-freebsd-x64": {
|
| 821 |
+
"version": "4.60.1",
|
| 822 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
|
| 823 |
+
"integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
|
| 824 |
+
"cpu": [
|
| 825 |
+
"x64"
|
| 826 |
+
],
|
| 827 |
+
"dev": true,
|
| 828 |
+
"optional": true,
|
| 829 |
+
"os": [
|
| 830 |
+
"freebsd"
|
| 831 |
+
]
|
| 832 |
+
},
|
| 833 |
+
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
| 834 |
+
"version": "4.60.1",
|
| 835 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
|
| 836 |
+
"integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
|
| 837 |
+
"cpu": [
|
| 838 |
+
"arm"
|
| 839 |
+
],
|
| 840 |
+
"dev": true,
|
| 841 |
+
"optional": true,
|
| 842 |
+
"os": [
|
| 843 |
+
"linux"
|
| 844 |
+
]
|
| 845 |
+
},
|
| 846 |
+
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
| 847 |
+
"version": "4.60.1",
|
| 848 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
|
| 849 |
+
"integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
|
| 850 |
+
"cpu": [
|
| 851 |
+
"arm"
|
| 852 |
+
],
|
| 853 |
+
"dev": true,
|
| 854 |
+
"optional": true,
|
| 855 |
+
"os": [
|
| 856 |
+
"linux"
|
| 857 |
+
]
|
| 858 |
+
},
|
| 859 |
+
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
| 860 |
+
"version": "4.60.1",
|
| 861 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
|
| 862 |
+
"integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
|
| 863 |
+
"cpu": [
|
| 864 |
+
"arm64"
|
| 865 |
+
],
|
| 866 |
+
"dev": true,
|
| 867 |
+
"optional": true,
|
| 868 |
+
"os": [
|
| 869 |
+
"linux"
|
| 870 |
+
]
|
| 871 |
+
},
|
| 872 |
+
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
| 873 |
+
"version": "4.60.1",
|
| 874 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
|
| 875 |
+
"integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
|
| 876 |
+
"cpu": [
|
| 877 |
+
"arm64"
|
| 878 |
+
],
|
| 879 |
+
"dev": true,
|
| 880 |
+
"optional": true,
|
| 881 |
+
"os": [
|
| 882 |
+
"linux"
|
| 883 |
+
]
|
| 884 |
+
},
|
| 885 |
+
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
| 886 |
+
"version": "4.60.1",
|
| 887 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
|
| 888 |
+
"integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
|
| 889 |
+
"cpu": [
|
| 890 |
+
"loong64"
|
| 891 |
+
],
|
| 892 |
+
"dev": true,
|
| 893 |
+
"optional": true,
|
| 894 |
+
"os": [
|
| 895 |
+
"linux"
|
| 896 |
+
]
|
| 897 |
+
},
|
| 898 |
+
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
| 899 |
+
"version": "4.60.1",
|
| 900 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
|
| 901 |
+
"integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
|
| 902 |
+
"cpu": [
|
| 903 |
+
"loong64"
|
| 904 |
+
],
|
| 905 |
+
"dev": true,
|
| 906 |
+
"optional": true,
|
| 907 |
+
"os": [
|
| 908 |
+
"linux"
|
| 909 |
+
]
|
| 910 |
+
},
|
| 911 |
+
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
| 912 |
+
"version": "4.60.1",
|
| 913 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
|
| 914 |
+
"integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
|
| 915 |
+
"cpu": [
|
| 916 |
+
"ppc64"
|
| 917 |
+
],
|
| 918 |
+
"dev": true,
|
| 919 |
+
"optional": true,
|
| 920 |
+
"os": [
|
| 921 |
+
"linux"
|
| 922 |
+
]
|
| 923 |
+
},
|
| 924 |
+
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
| 925 |
+
"version": "4.60.1",
|
| 926 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
|
| 927 |
+
"integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
|
| 928 |
+
"cpu": [
|
| 929 |
+
"ppc64"
|
| 930 |
+
],
|
| 931 |
+
"dev": true,
|
| 932 |
+
"optional": true,
|
| 933 |
+
"os": [
|
| 934 |
+
"linux"
|
| 935 |
+
]
|
| 936 |
+
},
|
| 937 |
+
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
| 938 |
+
"version": "4.60.1",
|
| 939 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
|
| 940 |
+
"integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
|
| 941 |
+
"cpu": [
|
| 942 |
+
"riscv64"
|
| 943 |
+
],
|
| 944 |
+
"dev": true,
|
| 945 |
+
"optional": true,
|
| 946 |
+
"os": [
|
| 947 |
+
"linux"
|
| 948 |
+
]
|
| 949 |
+
},
|
| 950 |
+
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
| 951 |
+
"version": "4.60.1",
|
| 952 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
|
| 953 |
+
"integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
|
| 954 |
+
"cpu": [
|
| 955 |
+
"riscv64"
|
| 956 |
+
],
|
| 957 |
+
"dev": true,
|
| 958 |
+
"optional": true,
|
| 959 |
+
"os": [
|
| 960 |
+
"linux"
|
| 961 |
+
]
|
| 962 |
+
},
|
| 963 |
+
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
| 964 |
+
"version": "4.60.1",
|
| 965 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
|
| 966 |
+
"integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
|
| 967 |
+
"cpu": [
|
| 968 |
+
"s390x"
|
| 969 |
+
],
|
| 970 |
+
"dev": true,
|
| 971 |
+
"optional": true,
|
| 972 |
+
"os": [
|
| 973 |
+
"linux"
|
| 974 |
+
]
|
| 975 |
+
},
|
| 976 |
+
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
| 977 |
+
"version": "4.60.1",
|
| 978 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
|
| 979 |
+
"integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
|
| 980 |
+
"cpu": [
|
| 981 |
+
"x64"
|
| 982 |
+
],
|
| 983 |
+
"dev": true,
|
| 984 |
+
"optional": true,
|
| 985 |
+
"os": [
|
| 986 |
+
"linux"
|
| 987 |
+
]
|
| 988 |
+
},
|
| 989 |
+
"node_modules/@rollup/rollup-linux-x64-musl": {
|
| 990 |
+
"version": "4.60.1",
|
| 991 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
|
| 992 |
+
"integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
|
| 993 |
+
"cpu": [
|
| 994 |
+
"x64"
|
| 995 |
+
],
|
| 996 |
+
"dev": true,
|
| 997 |
+
"optional": true,
|
| 998 |
+
"os": [
|
| 999 |
+
"linux"
|
| 1000 |
+
]
|
| 1001 |
+
},
|
| 1002 |
+
"node_modules/@rollup/rollup-openbsd-x64": {
|
| 1003 |
+
"version": "4.60.1",
|
| 1004 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
|
| 1005 |
+
"integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
|
| 1006 |
+
"cpu": [
|
| 1007 |
+
"x64"
|
| 1008 |
+
],
|
| 1009 |
+
"dev": true,
|
| 1010 |
+
"optional": true,
|
| 1011 |
+
"os": [
|
| 1012 |
+
"openbsd"
|
| 1013 |
+
]
|
| 1014 |
+
},
|
| 1015 |
+
"node_modules/@rollup/rollup-openharmony-arm64": {
|
| 1016 |
+
"version": "4.60.1",
|
| 1017 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
|
| 1018 |
+
"integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
|
| 1019 |
+
"cpu": [
|
| 1020 |
+
"arm64"
|
| 1021 |
+
],
|
| 1022 |
+
"dev": true,
|
| 1023 |
+
"optional": true,
|
| 1024 |
+
"os": [
|
| 1025 |
+
"openharmony"
|
| 1026 |
+
]
|
| 1027 |
+
},
|
| 1028 |
+
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
| 1029 |
+
"version": "4.60.1",
|
| 1030 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
|
| 1031 |
+
"integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
|
| 1032 |
+
"cpu": [
|
| 1033 |
+
"arm64"
|
| 1034 |
+
],
|
| 1035 |
+
"dev": true,
|
| 1036 |
+
"optional": true,
|
| 1037 |
+
"os": [
|
| 1038 |
+
"win32"
|
| 1039 |
+
]
|
| 1040 |
+
},
|
| 1041 |
+
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
| 1042 |
+
"version": "4.60.1",
|
| 1043 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
|
| 1044 |
+
"integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
|
| 1045 |
+
"cpu": [
|
| 1046 |
+
"ia32"
|
| 1047 |
+
],
|
| 1048 |
+
"dev": true,
|
| 1049 |
+
"optional": true,
|
| 1050 |
+
"os": [
|
| 1051 |
+
"win32"
|
| 1052 |
+
]
|
| 1053 |
+
},
|
| 1054 |
+
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
| 1055 |
+
"version": "4.60.1",
|
| 1056 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
|
| 1057 |
+
"integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
|
| 1058 |
+
"cpu": [
|
| 1059 |
+
"x64"
|
| 1060 |
+
],
|
| 1061 |
+
"dev": true,
|
| 1062 |
+
"optional": true,
|
| 1063 |
+
"os": [
|
| 1064 |
+
"win32"
|
| 1065 |
+
]
|
| 1066 |
+
},
|
| 1067 |
+
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
| 1068 |
+
"version": "4.60.1",
|
| 1069 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
|
| 1070 |
+
"integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
|
| 1071 |
+
"cpu": [
|
| 1072 |
+
"x64"
|
| 1073 |
+
],
|
| 1074 |
+
"dev": true,
|
| 1075 |
+
"optional": true,
|
| 1076 |
+
"os": [
|
| 1077 |
+
"win32"
|
| 1078 |
+
]
|
| 1079 |
+
},
|
| 1080 |
+
"node_modules/@types/babel__core": {
|
| 1081 |
+
"version": "7.20.5",
|
| 1082 |
+
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
| 1083 |
+
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
| 1084 |
+
"dev": true,
|
| 1085 |
+
"dependencies": {
|
| 1086 |
+
"@babel/parser": "^7.20.7",
|
| 1087 |
+
"@babel/types": "^7.20.7",
|
| 1088 |
+
"@types/babel__generator": "*",
|
| 1089 |
+
"@types/babel__template": "*",
|
| 1090 |
+
"@types/babel__traverse": "*"
|
| 1091 |
+
}
|
| 1092 |
+
},
|
| 1093 |
+
"node_modules/@types/babel__generator": {
|
| 1094 |
+
"version": "7.27.0",
|
| 1095 |
+
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
|
| 1096 |
+
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
|
| 1097 |
+
"dev": true,
|
| 1098 |
+
"dependencies": {
|
| 1099 |
+
"@babel/types": "^7.0.0"
|
| 1100 |
+
}
|
| 1101 |
+
},
|
| 1102 |
+
"node_modules/@types/babel__template": {
|
| 1103 |
+
"version": "7.4.4",
|
| 1104 |
+
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
|
| 1105 |
+
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
|
| 1106 |
+
"dev": true,
|
| 1107 |
+
"dependencies": {
|
| 1108 |
+
"@babel/parser": "^7.1.0",
|
| 1109 |
+
"@babel/types": "^7.0.0"
|
| 1110 |
+
}
|
| 1111 |
+
},
|
| 1112 |
+
"node_modules/@types/babel__traverse": {
|
| 1113 |
+
"version": "7.28.0",
|
| 1114 |
+
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
|
| 1115 |
+
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
|
| 1116 |
+
"dev": true,
|
| 1117 |
+
"dependencies": {
|
| 1118 |
+
"@babel/types": "^7.28.2"
|
| 1119 |
+
}
|
| 1120 |
+
},
|
| 1121 |
+
"node_modules/@types/estree": {
|
| 1122 |
+
"version": "1.0.8",
|
| 1123 |
+
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
| 1124 |
+
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
| 1125 |
+
"dev": true
|
| 1126 |
+
},
|
| 1127 |
+
"node_modules/@types/prop-types": {
|
| 1128 |
+
"version": "15.7.15",
|
| 1129 |
+
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
| 1130 |
+
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
| 1131 |
+
"dev": true
|
| 1132 |
+
},
|
| 1133 |
+
"node_modules/@types/react": {
|
| 1134 |
+
"version": "18.3.28",
|
| 1135 |
+
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
|
| 1136 |
+
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
|
| 1137 |
+
"dev": true,
|
| 1138 |
+
"dependencies": {
|
| 1139 |
+
"@types/prop-types": "*",
|
| 1140 |
+
"csstype": "^3.2.2"
|
| 1141 |
+
}
|
| 1142 |
+
},
|
| 1143 |
+
"node_modules/@types/react-dom": {
|
| 1144 |
+
"version": "18.3.7",
|
| 1145 |
+
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
| 1146 |
+
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
| 1147 |
+
"dev": true,
|
| 1148 |
+
"peerDependencies": {
|
| 1149 |
+
"@types/react": "^18.0.0"
|
| 1150 |
+
}
|
| 1151 |
+
},
|
| 1152 |
+
"node_modules/@vitejs/plugin-react": {
|
| 1153 |
+
"version": "4.7.0",
|
| 1154 |
+
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
| 1155 |
+
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
|
| 1156 |
+
"dev": true,
|
| 1157 |
+
"dependencies": {
|
| 1158 |
+
"@babel/core": "^7.28.0",
|
| 1159 |
+
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
| 1160 |
+
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
| 1161 |
+
"@rolldown/pluginutils": "1.0.0-beta.27",
|
| 1162 |
+
"@types/babel__core": "^7.20.5",
|
| 1163 |
+
"react-refresh": "^0.17.0"
|
| 1164 |
+
},
|
| 1165 |
+
"engines": {
|
| 1166 |
+
"node": "^14.18.0 || >=16.0.0"
|
| 1167 |
+
},
|
| 1168 |
+
"peerDependencies": {
|
| 1169 |
+
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
| 1170 |
+
}
|
| 1171 |
+
},
|
| 1172 |
+
"node_modules/any-promise": {
|
| 1173 |
+
"version": "1.3.0",
|
| 1174 |
+
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
| 1175 |
+
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
| 1176 |
+
"dev": true
|
| 1177 |
+
},
|
| 1178 |
+
"node_modules/anymatch": {
|
| 1179 |
+
"version": "3.1.3",
|
| 1180 |
+
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
| 1181 |
+
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
| 1182 |
+
"dev": true,
|
| 1183 |
+
"dependencies": {
|
| 1184 |
+
"normalize-path": "^3.0.0",
|
| 1185 |
+
"picomatch": "^2.0.4"
|
| 1186 |
+
},
|
| 1187 |
+
"engines": {
|
| 1188 |
+
"node": ">= 8"
|
| 1189 |
+
}
|
| 1190 |
+
},
|
| 1191 |
+
"node_modules/arg": {
|
| 1192 |
+
"version": "5.0.2",
|
| 1193 |
+
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
| 1194 |
+
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
| 1195 |
+
"dev": true
|
| 1196 |
+
},
|
| 1197 |
+
"node_modules/asynckit": {
|
| 1198 |
+
"version": "0.4.0",
|
| 1199 |
+
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
| 1200 |
+
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
| 1201 |
+
},
|
| 1202 |
+
"node_modules/autoprefixer": {
|
| 1203 |
+
"version": "10.4.27",
|
| 1204 |
+
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz",
|
| 1205 |
+
"integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==",
|
| 1206 |
+
"dev": true,
|
| 1207 |
+
"funding": [
|
| 1208 |
+
{
|
| 1209 |
+
"type": "opencollective",
|
| 1210 |
+
"url": "https://opencollective.com/postcss/"
|
| 1211 |
+
},
|
| 1212 |
+
{
|
| 1213 |
+
"type": "tidelift",
|
| 1214 |
+
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
|
| 1215 |
+
},
|
| 1216 |
+
{
|
| 1217 |
+
"type": "github",
|
| 1218 |
+
"url": "https://github.com/sponsors/ai"
|
| 1219 |
+
}
|
| 1220 |
+
],
|
| 1221 |
+
"dependencies": {
|
| 1222 |
+
"browserslist": "^4.28.1",
|
| 1223 |
+
"caniuse-lite": "^1.0.30001774",
|
| 1224 |
+
"fraction.js": "^5.3.4",
|
| 1225 |
+
"picocolors": "^1.1.1",
|
| 1226 |
+
"postcss-value-parser": "^4.2.0"
|
| 1227 |
+
},
|
| 1228 |
+
"bin": {
|
| 1229 |
+
"autoprefixer": "bin/autoprefixer"
|
| 1230 |
+
},
|
| 1231 |
+
"engines": {
|
| 1232 |
+
"node": "^10 || ^12 || >=14"
|
| 1233 |
+
},
|
| 1234 |
+
"peerDependencies": {
|
| 1235 |
+
"postcss": "^8.1.0"
|
| 1236 |
+
}
|
| 1237 |
+
},
|
| 1238 |
+
"node_modules/axios": {
|
| 1239 |
+
"version": "1.14.0",
|
| 1240 |
+
"resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz",
|
| 1241 |
+
"integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==",
|
| 1242 |
+
"dependencies": {
|
| 1243 |
+
"follow-redirects": "^1.15.11",
|
| 1244 |
+
"form-data": "^4.0.5",
|
| 1245 |
+
"proxy-from-env": "^2.1.0"
|
| 1246 |
+
}
|
| 1247 |
+
},
|
| 1248 |
+
"node_modules/baseline-browser-mapping": {
|
| 1249 |
+
"version": "2.10.16",
|
| 1250 |
+
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz",
|
| 1251 |
+
"integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==",
|
| 1252 |
+
"dev": true,
|
| 1253 |
+
"bin": {
|
| 1254 |
+
"baseline-browser-mapping": "dist/cli.cjs"
|
| 1255 |
+
},
|
| 1256 |
+
"engines": {
|
| 1257 |
+
"node": ">=6.0.0"
|
| 1258 |
+
}
|
| 1259 |
+
},
|
| 1260 |
+
"node_modules/binary-extensions": {
|
| 1261 |
+
"version": "2.3.0",
|
| 1262 |
+
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
| 1263 |
+
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
| 1264 |
+
"dev": true,
|
| 1265 |
+
"engines": {
|
| 1266 |
+
"node": ">=8"
|
| 1267 |
+
},
|
| 1268 |
+
"funding": {
|
| 1269 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1270 |
+
}
|
| 1271 |
+
},
|
| 1272 |
+
"node_modules/braces": {
|
| 1273 |
+
"version": "3.0.3",
|
| 1274 |
+
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
| 1275 |
+
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
| 1276 |
+
"dev": true,
|
| 1277 |
+
"dependencies": {
|
| 1278 |
+
"fill-range": "^7.1.1"
|
| 1279 |
+
},
|
| 1280 |
+
"engines": {
|
| 1281 |
+
"node": ">=8"
|
| 1282 |
+
}
|
| 1283 |
+
},
|
| 1284 |
+
"node_modules/browserslist": {
|
| 1285 |
+
"version": "4.28.2",
|
| 1286 |
+
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
|
| 1287 |
+
"integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
|
| 1288 |
+
"dev": true,
|
| 1289 |
+
"funding": [
|
| 1290 |
+
{
|
| 1291 |
+
"type": "opencollective",
|
| 1292 |
+
"url": "https://opencollective.com/browserslist"
|
| 1293 |
+
},
|
| 1294 |
+
{
|
| 1295 |
+
"type": "tidelift",
|
| 1296 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 1297 |
+
},
|
| 1298 |
+
{
|
| 1299 |
+
"type": "github",
|
| 1300 |
+
"url": "https://github.com/sponsors/ai"
|
| 1301 |
+
}
|
| 1302 |
+
],
|
| 1303 |
+
"dependencies": {
|
| 1304 |
+
"baseline-browser-mapping": "^2.10.12",
|
| 1305 |
+
"caniuse-lite": "^1.0.30001782",
|
| 1306 |
+
"electron-to-chromium": "^1.5.328",
|
| 1307 |
+
"node-releases": "^2.0.36",
|
| 1308 |
+
"update-browserslist-db": "^1.2.3"
|
| 1309 |
+
},
|
| 1310 |
+
"bin": {
|
| 1311 |
+
"browserslist": "cli.js"
|
| 1312 |
+
},
|
| 1313 |
+
"engines": {
|
| 1314 |
+
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
| 1315 |
+
}
|
| 1316 |
+
},
|
| 1317 |
+
"node_modules/call-bind-apply-helpers": {
|
| 1318 |
+
"version": "1.0.2",
|
| 1319 |
+
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
| 1320 |
+
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
| 1321 |
+
"dependencies": {
|
| 1322 |
+
"es-errors": "^1.3.0",
|
| 1323 |
+
"function-bind": "^1.1.2"
|
| 1324 |
+
},
|
| 1325 |
+
"engines": {
|
| 1326 |
+
"node": ">= 0.4"
|
| 1327 |
+
}
|
| 1328 |
+
},
|
| 1329 |
+
"node_modules/camelcase-css": {
|
| 1330 |
+
"version": "2.0.1",
|
| 1331 |
+
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
| 1332 |
+
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
|
| 1333 |
+
"dev": true,
|
| 1334 |
+
"engines": {
|
| 1335 |
+
"node": ">= 6"
|
| 1336 |
+
}
|
| 1337 |
+
},
|
| 1338 |
+
"node_modules/caniuse-lite": {
|
| 1339 |
+
"version": "1.0.30001786",
|
| 1340 |
+
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz",
|
| 1341 |
+
"integrity": "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==",
|
| 1342 |
+
"dev": true,
|
| 1343 |
+
"funding": [
|
| 1344 |
+
{
|
| 1345 |
+
"type": "opencollective",
|
| 1346 |
+
"url": "https://opencollective.com/browserslist"
|
| 1347 |
+
},
|
| 1348 |
+
{
|
| 1349 |
+
"type": "tidelift",
|
| 1350 |
+
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
| 1351 |
+
},
|
| 1352 |
+
{
|
| 1353 |
+
"type": "github",
|
| 1354 |
+
"url": "https://github.com/sponsors/ai"
|
| 1355 |
+
}
|
| 1356 |
+
]
|
| 1357 |
+
},
|
| 1358 |
+
"node_modules/chokidar": {
|
| 1359 |
+
"version": "3.6.0",
|
| 1360 |
+
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
| 1361 |
+
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
| 1362 |
+
"dev": true,
|
| 1363 |
+
"dependencies": {
|
| 1364 |
+
"anymatch": "~3.1.2",
|
| 1365 |
+
"braces": "~3.0.2",
|
| 1366 |
+
"glob-parent": "~5.1.2",
|
| 1367 |
+
"is-binary-path": "~2.1.0",
|
| 1368 |
+
"is-glob": "~4.0.1",
|
| 1369 |
+
"normalize-path": "~3.0.0",
|
| 1370 |
+
"readdirp": "~3.6.0"
|
| 1371 |
+
},
|
| 1372 |
+
"engines": {
|
| 1373 |
+
"node": ">= 8.10.0"
|
| 1374 |
+
},
|
| 1375 |
+
"funding": {
|
| 1376 |
+
"url": "https://paulmillr.com/funding/"
|
| 1377 |
+
},
|
| 1378 |
+
"optionalDependencies": {
|
| 1379 |
+
"fsevents": "~2.3.2"
|
| 1380 |
+
}
|
| 1381 |
+
},
|
| 1382 |
+
"node_modules/chokidar/node_modules/glob-parent": {
|
| 1383 |
+
"version": "5.1.2",
|
| 1384 |
+
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
| 1385 |
+
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
| 1386 |
+
"dev": true,
|
| 1387 |
+
"dependencies": {
|
| 1388 |
+
"is-glob": "^4.0.1"
|
| 1389 |
+
},
|
| 1390 |
+
"engines": {
|
| 1391 |
+
"node": ">= 6"
|
| 1392 |
+
}
|
| 1393 |
+
},
|
| 1394 |
+
"node_modules/combined-stream": {
|
| 1395 |
+
"version": "1.0.8",
|
| 1396 |
+
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
| 1397 |
+
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
| 1398 |
+
"dependencies": {
|
| 1399 |
+
"delayed-stream": "~1.0.0"
|
| 1400 |
+
},
|
| 1401 |
+
"engines": {
|
| 1402 |
+
"node": ">= 0.8"
|
| 1403 |
+
}
|
| 1404 |
+
},
|
| 1405 |
+
"node_modules/commander": {
|
| 1406 |
+
"version": "4.1.1",
|
| 1407 |
+
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
| 1408 |
+
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
| 1409 |
+
"dev": true,
|
| 1410 |
+
"engines": {
|
| 1411 |
+
"node": ">= 6"
|
| 1412 |
+
}
|
| 1413 |
+
},
|
| 1414 |
+
"node_modules/convert-source-map": {
|
| 1415 |
+
"version": "2.0.0",
|
| 1416 |
+
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
| 1417 |
+
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
| 1418 |
+
"dev": true
|
| 1419 |
+
},
|
| 1420 |
+
"node_modules/cssesc": {
|
| 1421 |
+
"version": "3.0.0",
|
| 1422 |
+
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
| 1423 |
+
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
| 1424 |
+
"dev": true,
|
| 1425 |
+
"bin": {
|
| 1426 |
+
"cssesc": "bin/cssesc"
|
| 1427 |
+
},
|
| 1428 |
+
"engines": {
|
| 1429 |
+
"node": ">=4"
|
| 1430 |
+
}
|
| 1431 |
+
},
|
| 1432 |
+
"node_modules/csstype": {
|
| 1433 |
+
"version": "3.2.3",
|
| 1434 |
+
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
| 1435 |
+
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
| 1436 |
+
"dev": true
|
| 1437 |
+
},
|
| 1438 |
+
"node_modules/debug": {
|
| 1439 |
+
"version": "4.4.3",
|
| 1440 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 1441 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 1442 |
+
"dev": true,
|
| 1443 |
+
"dependencies": {
|
| 1444 |
+
"ms": "^2.1.3"
|
| 1445 |
+
},
|
| 1446 |
+
"engines": {
|
| 1447 |
+
"node": ">=6.0"
|
| 1448 |
+
},
|
| 1449 |
+
"peerDependenciesMeta": {
|
| 1450 |
+
"supports-color": {
|
| 1451 |
+
"optional": true
|
| 1452 |
+
}
|
| 1453 |
+
}
|
| 1454 |
+
},
|
| 1455 |
+
"node_modules/delayed-stream": {
|
| 1456 |
+
"version": "1.0.0",
|
| 1457 |
+
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
| 1458 |
+
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
| 1459 |
+
"engines": {
|
| 1460 |
+
"node": ">=0.4.0"
|
| 1461 |
+
}
|
| 1462 |
+
},
|
| 1463 |
+
"node_modules/didyoumean": {
|
| 1464 |
+
"version": "1.2.2",
|
| 1465 |
+
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
| 1466 |
+
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
| 1467 |
+
"dev": true
|
| 1468 |
+
},
|
| 1469 |
+
"node_modules/dlv": {
|
| 1470 |
+
"version": "1.1.3",
|
| 1471 |
+
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
| 1472 |
+
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
| 1473 |
+
"dev": true
|
| 1474 |
+
},
|
| 1475 |
+
"node_modules/dunder-proto": {
|
| 1476 |
+
"version": "1.0.1",
|
| 1477 |
+
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
| 1478 |
+
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
| 1479 |
+
"dependencies": {
|
| 1480 |
+
"call-bind-apply-helpers": "^1.0.1",
|
| 1481 |
+
"es-errors": "^1.3.0",
|
| 1482 |
+
"gopd": "^1.2.0"
|
| 1483 |
+
},
|
| 1484 |
+
"engines": {
|
| 1485 |
+
"node": ">= 0.4"
|
| 1486 |
+
}
|
| 1487 |
+
},
|
| 1488 |
+
"node_modules/electron-to-chromium": {
|
| 1489 |
+
"version": "1.5.331",
|
| 1490 |
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz",
|
| 1491 |
+
"integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==",
|
| 1492 |
+
"dev": true
|
| 1493 |
+
},
|
| 1494 |
+
"node_modules/es-define-property": {
|
| 1495 |
+
"version": "1.0.1",
|
| 1496 |
+
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
| 1497 |
+
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
| 1498 |
+
"engines": {
|
| 1499 |
+
"node": ">= 0.4"
|
| 1500 |
+
}
|
| 1501 |
+
},
|
| 1502 |
+
"node_modules/es-errors": {
|
| 1503 |
+
"version": "1.3.0",
|
| 1504 |
+
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
| 1505 |
+
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
| 1506 |
+
"engines": {
|
| 1507 |
+
"node": ">= 0.4"
|
| 1508 |
+
}
|
| 1509 |
+
},
|
| 1510 |
+
"node_modules/es-object-atoms": {
|
| 1511 |
+
"version": "1.1.1",
|
| 1512 |
+
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
| 1513 |
+
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
| 1514 |
+
"dependencies": {
|
| 1515 |
+
"es-errors": "^1.3.0"
|
| 1516 |
+
},
|
| 1517 |
+
"engines": {
|
| 1518 |
+
"node": ">= 0.4"
|
| 1519 |
+
}
|
| 1520 |
+
},
|
| 1521 |
+
"node_modules/es-set-tostringtag": {
|
| 1522 |
+
"version": "2.1.0",
|
| 1523 |
+
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
| 1524 |
+
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
| 1525 |
+
"dependencies": {
|
| 1526 |
+
"es-errors": "^1.3.0",
|
| 1527 |
+
"get-intrinsic": "^1.2.6",
|
| 1528 |
+
"has-tostringtag": "^1.0.2",
|
| 1529 |
+
"hasown": "^2.0.2"
|
| 1530 |
+
},
|
| 1531 |
+
"engines": {
|
| 1532 |
+
"node": ">= 0.4"
|
| 1533 |
+
}
|
| 1534 |
+
},
|
| 1535 |
+
"node_modules/esbuild": {
|
| 1536 |
+
"version": "0.21.5",
|
| 1537 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
| 1538 |
+
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
| 1539 |
+
"dev": true,
|
| 1540 |
+
"hasInstallScript": true,
|
| 1541 |
+
"bin": {
|
| 1542 |
+
"esbuild": "bin/esbuild"
|
| 1543 |
+
},
|
| 1544 |
+
"engines": {
|
| 1545 |
+
"node": ">=12"
|
| 1546 |
+
},
|
| 1547 |
+
"optionalDependencies": {
|
| 1548 |
+
"@esbuild/aix-ppc64": "0.21.5",
|
| 1549 |
+
"@esbuild/android-arm": "0.21.5",
|
| 1550 |
+
"@esbuild/android-arm64": "0.21.5",
|
| 1551 |
+
"@esbuild/android-x64": "0.21.5",
|
| 1552 |
+
"@esbuild/darwin-arm64": "0.21.5",
|
| 1553 |
+
"@esbuild/darwin-x64": "0.21.5",
|
| 1554 |
+
"@esbuild/freebsd-arm64": "0.21.5",
|
| 1555 |
+
"@esbuild/freebsd-x64": "0.21.5",
|
| 1556 |
+
"@esbuild/linux-arm": "0.21.5",
|
| 1557 |
+
"@esbuild/linux-arm64": "0.21.5",
|
| 1558 |
+
"@esbuild/linux-ia32": "0.21.5",
|
| 1559 |
+
"@esbuild/linux-loong64": "0.21.5",
|
| 1560 |
+
"@esbuild/linux-mips64el": "0.21.5",
|
| 1561 |
+
"@esbuild/linux-ppc64": "0.21.5",
|
| 1562 |
+
"@esbuild/linux-riscv64": "0.21.5",
|
| 1563 |
+
"@esbuild/linux-s390x": "0.21.5",
|
| 1564 |
+
"@esbuild/linux-x64": "0.21.5",
|
| 1565 |
+
"@esbuild/netbsd-x64": "0.21.5",
|
| 1566 |
+
"@esbuild/openbsd-x64": "0.21.5",
|
| 1567 |
+
"@esbuild/sunos-x64": "0.21.5",
|
| 1568 |
+
"@esbuild/win32-arm64": "0.21.5",
|
| 1569 |
+
"@esbuild/win32-ia32": "0.21.5",
|
| 1570 |
+
"@esbuild/win32-x64": "0.21.5"
|
| 1571 |
+
}
|
| 1572 |
+
},
|
| 1573 |
+
"node_modules/escalade": {
|
| 1574 |
+
"version": "3.2.0",
|
| 1575 |
+
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
| 1576 |
+
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
| 1577 |
+
"dev": true,
|
| 1578 |
+
"engines": {
|
| 1579 |
+
"node": ">=6"
|
| 1580 |
+
}
|
| 1581 |
+
},
|
| 1582 |
+
"node_modules/fast-glob": {
|
| 1583 |
+
"version": "3.3.3",
|
| 1584 |
+
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
| 1585 |
+
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
| 1586 |
+
"dev": true,
|
| 1587 |
+
"dependencies": {
|
| 1588 |
+
"@nodelib/fs.stat": "^2.0.2",
|
| 1589 |
+
"@nodelib/fs.walk": "^1.2.3",
|
| 1590 |
+
"glob-parent": "^5.1.2",
|
| 1591 |
+
"merge2": "^1.3.0",
|
| 1592 |
+
"micromatch": "^4.0.8"
|
| 1593 |
+
},
|
| 1594 |
+
"engines": {
|
| 1595 |
+
"node": ">=8.6.0"
|
| 1596 |
+
}
|
| 1597 |
+
},
|
| 1598 |
+
"node_modules/fast-glob/node_modules/glob-parent": {
|
| 1599 |
+
"version": "5.1.2",
|
| 1600 |
+
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
| 1601 |
+
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
| 1602 |
+
"dev": true,
|
| 1603 |
+
"dependencies": {
|
| 1604 |
+
"is-glob": "^4.0.1"
|
| 1605 |
+
},
|
| 1606 |
+
"engines": {
|
| 1607 |
+
"node": ">= 6"
|
| 1608 |
+
}
|
| 1609 |
+
},
|
| 1610 |
+
"node_modules/fastq": {
|
| 1611 |
+
"version": "1.20.1",
|
| 1612 |
+
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
| 1613 |
+
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
|
| 1614 |
+
"dev": true,
|
| 1615 |
+
"dependencies": {
|
| 1616 |
+
"reusify": "^1.0.4"
|
| 1617 |
+
}
|
| 1618 |
+
},
|
| 1619 |
+
"node_modules/fill-range": {
|
| 1620 |
+
"version": "7.1.1",
|
| 1621 |
+
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
| 1622 |
+
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
| 1623 |
+
"dev": true,
|
| 1624 |
+
"dependencies": {
|
| 1625 |
+
"to-regex-range": "^5.0.1"
|
| 1626 |
+
},
|
| 1627 |
+
"engines": {
|
| 1628 |
+
"node": ">=8"
|
| 1629 |
+
}
|
| 1630 |
+
},
|
| 1631 |
+
"node_modules/follow-redirects": {
|
| 1632 |
+
"version": "1.15.11",
|
| 1633 |
+
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
| 1634 |
+
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
| 1635 |
+
"funding": [
|
| 1636 |
+
{
|
| 1637 |
+
"type": "individual",
|
| 1638 |
+
"url": "https://github.com/sponsors/RubenVerborgh"
|
| 1639 |
+
}
|
| 1640 |
+
],
|
| 1641 |
+
"engines": {
|
| 1642 |
+
"node": ">=4.0"
|
| 1643 |
+
},
|
| 1644 |
+
"peerDependenciesMeta": {
|
| 1645 |
+
"debug": {
|
| 1646 |
+
"optional": true
|
| 1647 |
+
}
|
| 1648 |
+
}
|
| 1649 |
+
},
|
| 1650 |
+
"node_modules/form-data": {
|
| 1651 |
+
"version": "4.0.5",
|
| 1652 |
+
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
| 1653 |
+
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
| 1654 |
+
"dependencies": {
|
| 1655 |
+
"asynckit": "^0.4.0",
|
| 1656 |
+
"combined-stream": "^1.0.8",
|
| 1657 |
+
"es-set-tostringtag": "^2.1.0",
|
| 1658 |
+
"hasown": "^2.0.2",
|
| 1659 |
+
"mime-types": "^2.1.12"
|
| 1660 |
+
},
|
| 1661 |
+
"engines": {
|
| 1662 |
+
"node": ">= 6"
|
| 1663 |
+
}
|
| 1664 |
+
},
|
| 1665 |
+
"node_modules/fraction.js": {
|
| 1666 |
+
"version": "5.3.4",
|
| 1667 |
+
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
| 1668 |
+
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
|
| 1669 |
+
"dev": true,
|
| 1670 |
+
"engines": {
|
| 1671 |
+
"node": "*"
|
| 1672 |
+
},
|
| 1673 |
+
"funding": {
|
| 1674 |
+
"type": "github",
|
| 1675 |
+
"url": "https://github.com/sponsors/rawify"
|
| 1676 |
+
}
|
| 1677 |
+
},
|
| 1678 |
+
"node_modules/fsevents": {
|
| 1679 |
+
"version": "2.3.3",
|
| 1680 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 1681 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 1682 |
+
"dev": true,
|
| 1683 |
+
"hasInstallScript": true,
|
| 1684 |
+
"optional": true,
|
| 1685 |
+
"os": [
|
| 1686 |
+
"darwin"
|
| 1687 |
+
],
|
| 1688 |
+
"engines": {
|
| 1689 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 1690 |
+
}
|
| 1691 |
+
},
|
| 1692 |
+
"node_modules/function-bind": {
|
| 1693 |
+
"version": "1.1.2",
|
| 1694 |
+
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
| 1695 |
+
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
| 1696 |
+
"funding": {
|
| 1697 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1698 |
+
}
|
| 1699 |
+
},
|
| 1700 |
+
"node_modules/gensync": {
|
| 1701 |
+
"version": "1.0.0-beta.2",
|
| 1702 |
+
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
| 1703 |
+
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
| 1704 |
+
"dev": true,
|
| 1705 |
+
"engines": {
|
| 1706 |
+
"node": ">=6.9.0"
|
| 1707 |
+
}
|
| 1708 |
+
},
|
| 1709 |
+
"node_modules/get-intrinsic": {
|
| 1710 |
+
"version": "1.3.0",
|
| 1711 |
+
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
| 1712 |
+
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
| 1713 |
+
"dependencies": {
|
| 1714 |
+
"call-bind-apply-helpers": "^1.0.2",
|
| 1715 |
+
"es-define-property": "^1.0.1",
|
| 1716 |
+
"es-errors": "^1.3.0",
|
| 1717 |
+
"es-object-atoms": "^1.1.1",
|
| 1718 |
+
"function-bind": "^1.1.2",
|
| 1719 |
+
"get-proto": "^1.0.1",
|
| 1720 |
+
"gopd": "^1.2.0",
|
| 1721 |
+
"has-symbols": "^1.1.0",
|
| 1722 |
+
"hasown": "^2.0.2",
|
| 1723 |
+
"math-intrinsics": "^1.1.0"
|
| 1724 |
+
},
|
| 1725 |
+
"engines": {
|
| 1726 |
+
"node": ">= 0.4"
|
| 1727 |
+
},
|
| 1728 |
+
"funding": {
|
| 1729 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1730 |
+
}
|
| 1731 |
+
},
|
| 1732 |
+
"node_modules/get-proto": {
|
| 1733 |
+
"version": "1.0.1",
|
| 1734 |
+
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
| 1735 |
+
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
| 1736 |
+
"dependencies": {
|
| 1737 |
+
"dunder-proto": "^1.0.1",
|
| 1738 |
+
"es-object-atoms": "^1.0.0"
|
| 1739 |
+
},
|
| 1740 |
+
"engines": {
|
| 1741 |
+
"node": ">= 0.4"
|
| 1742 |
+
}
|
| 1743 |
+
},
|
| 1744 |
+
"node_modules/glob-parent": {
|
| 1745 |
+
"version": "6.0.2",
|
| 1746 |
+
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
| 1747 |
+
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
| 1748 |
+
"dev": true,
|
| 1749 |
+
"dependencies": {
|
| 1750 |
+
"is-glob": "^4.0.3"
|
| 1751 |
+
},
|
| 1752 |
+
"engines": {
|
| 1753 |
+
"node": ">=10.13.0"
|
| 1754 |
+
}
|
| 1755 |
+
},
|
| 1756 |
+
"node_modules/gopd": {
|
| 1757 |
+
"version": "1.2.0",
|
| 1758 |
+
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
| 1759 |
+
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
| 1760 |
+
"engines": {
|
| 1761 |
+
"node": ">= 0.4"
|
| 1762 |
+
},
|
| 1763 |
+
"funding": {
|
| 1764 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1765 |
+
}
|
| 1766 |
+
},
|
| 1767 |
+
"node_modules/has-symbols": {
|
| 1768 |
+
"version": "1.1.0",
|
| 1769 |
+
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
| 1770 |
+
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
| 1771 |
+
"engines": {
|
| 1772 |
+
"node": ">= 0.4"
|
| 1773 |
+
},
|
| 1774 |
+
"funding": {
|
| 1775 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1776 |
+
}
|
| 1777 |
+
},
|
| 1778 |
+
"node_modules/has-tostringtag": {
|
| 1779 |
+
"version": "1.0.2",
|
| 1780 |
+
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
| 1781 |
+
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
| 1782 |
+
"dependencies": {
|
| 1783 |
+
"has-symbols": "^1.0.3"
|
| 1784 |
+
},
|
| 1785 |
+
"engines": {
|
| 1786 |
+
"node": ">= 0.4"
|
| 1787 |
+
},
|
| 1788 |
+
"funding": {
|
| 1789 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1790 |
+
}
|
| 1791 |
+
},
|
| 1792 |
+
"node_modules/hasown": {
|
| 1793 |
+
"version": "2.0.2",
|
| 1794 |
+
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
| 1795 |
+
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
| 1796 |
+
"dependencies": {
|
| 1797 |
+
"function-bind": "^1.1.2"
|
| 1798 |
+
},
|
| 1799 |
+
"engines": {
|
| 1800 |
+
"node": ">= 0.4"
|
| 1801 |
+
}
|
| 1802 |
+
},
|
| 1803 |
+
"node_modules/is-binary-path": {
|
| 1804 |
+
"version": "2.1.0",
|
| 1805 |
+
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
| 1806 |
+
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
| 1807 |
+
"dev": true,
|
| 1808 |
+
"dependencies": {
|
| 1809 |
+
"binary-extensions": "^2.0.0"
|
| 1810 |
+
},
|
| 1811 |
+
"engines": {
|
| 1812 |
+
"node": ">=8"
|
| 1813 |
+
}
|
| 1814 |
+
},
|
| 1815 |
+
"node_modules/is-core-module": {
|
| 1816 |
+
"version": "2.16.1",
|
| 1817 |
+
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
| 1818 |
+
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
| 1819 |
+
"dev": true,
|
| 1820 |
+
"dependencies": {
|
| 1821 |
+
"hasown": "^2.0.2"
|
| 1822 |
+
},
|
| 1823 |
+
"engines": {
|
| 1824 |
+
"node": ">= 0.4"
|
| 1825 |
+
},
|
| 1826 |
+
"funding": {
|
| 1827 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1828 |
+
}
|
| 1829 |
+
},
|
| 1830 |
+
"node_modules/is-extglob": {
|
| 1831 |
+
"version": "2.1.1",
|
| 1832 |
+
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
| 1833 |
+
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
| 1834 |
+
"dev": true,
|
| 1835 |
+
"engines": {
|
| 1836 |
+
"node": ">=0.10.0"
|
| 1837 |
+
}
|
| 1838 |
+
},
|
| 1839 |
+
"node_modules/is-glob": {
|
| 1840 |
+
"version": "4.0.3",
|
| 1841 |
+
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
| 1842 |
+
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
| 1843 |
+
"dev": true,
|
| 1844 |
+
"dependencies": {
|
| 1845 |
+
"is-extglob": "^2.1.1"
|
| 1846 |
+
},
|
| 1847 |
+
"engines": {
|
| 1848 |
+
"node": ">=0.10.0"
|
| 1849 |
+
}
|
| 1850 |
+
},
|
| 1851 |
+
"node_modules/is-number": {
|
| 1852 |
+
"version": "7.0.0",
|
| 1853 |
+
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
| 1854 |
+
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
| 1855 |
+
"dev": true,
|
| 1856 |
+
"engines": {
|
| 1857 |
+
"node": ">=0.12.0"
|
| 1858 |
+
}
|
| 1859 |
+
},
|
| 1860 |
+
"node_modules/jiti": {
|
| 1861 |
+
"version": "1.21.7",
|
| 1862 |
+
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
| 1863 |
+
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
| 1864 |
+
"dev": true,
|
| 1865 |
+
"bin": {
|
| 1866 |
+
"jiti": "bin/jiti.js"
|
| 1867 |
+
}
|
| 1868 |
+
},
|
| 1869 |
+
"node_modules/js-tokens": {
|
| 1870 |
+
"version": "4.0.0",
|
| 1871 |
+
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
| 1872 |
+
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
| 1873 |
+
},
|
| 1874 |
+
"node_modules/jsesc": {
|
| 1875 |
+
"version": "3.1.0",
|
| 1876 |
+
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
| 1877 |
+
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
| 1878 |
+
"dev": true,
|
| 1879 |
+
"bin": {
|
| 1880 |
+
"jsesc": "bin/jsesc"
|
| 1881 |
+
},
|
| 1882 |
+
"engines": {
|
| 1883 |
+
"node": ">=6"
|
| 1884 |
+
}
|
| 1885 |
+
},
|
| 1886 |
+
"node_modules/json5": {
|
| 1887 |
+
"version": "2.2.3",
|
| 1888 |
+
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
| 1889 |
+
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
| 1890 |
+
"dev": true,
|
| 1891 |
+
"bin": {
|
| 1892 |
+
"json5": "lib/cli.js"
|
| 1893 |
+
},
|
| 1894 |
+
"engines": {
|
| 1895 |
+
"node": ">=6"
|
| 1896 |
+
}
|
| 1897 |
+
},
|
| 1898 |
+
"node_modules/lilconfig": {
|
| 1899 |
+
"version": "3.1.3",
|
| 1900 |
+
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
| 1901 |
+
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
| 1902 |
+
"dev": true,
|
| 1903 |
+
"engines": {
|
| 1904 |
+
"node": ">=14"
|
| 1905 |
+
},
|
| 1906 |
+
"funding": {
|
| 1907 |
+
"url": "https://github.com/sponsors/antonk52"
|
| 1908 |
+
}
|
| 1909 |
+
},
|
| 1910 |
+
"node_modules/lines-and-columns": {
|
| 1911 |
+
"version": "1.2.4",
|
| 1912 |
+
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
| 1913 |
+
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
| 1914 |
+
"dev": true
|
| 1915 |
+
},
|
| 1916 |
+
"node_modules/loose-envify": {
|
| 1917 |
+
"version": "1.4.0",
|
| 1918 |
+
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
| 1919 |
+
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
| 1920 |
+
"dependencies": {
|
| 1921 |
+
"js-tokens": "^3.0.0 || ^4.0.0"
|
| 1922 |
+
},
|
| 1923 |
+
"bin": {
|
| 1924 |
+
"loose-envify": "cli.js"
|
| 1925 |
+
}
|
| 1926 |
+
},
|
| 1927 |
+
"node_modules/lru-cache": {
|
| 1928 |
+
"version": "5.1.1",
|
| 1929 |
+
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
| 1930 |
+
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
| 1931 |
+
"dev": true,
|
| 1932 |
+
"dependencies": {
|
| 1933 |
+
"yallist": "^3.0.2"
|
| 1934 |
+
}
|
| 1935 |
+
},
|
| 1936 |
+
"node_modules/lucide-react": {
|
| 1937 |
+
"version": "0.294.0",
|
| 1938 |
+
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.294.0.tgz",
|
| 1939 |
+
"integrity": "sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA==",
|
| 1940 |
+
"peerDependencies": {
|
| 1941 |
+
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
| 1942 |
+
}
|
| 1943 |
+
},
|
| 1944 |
+
"node_modules/math-intrinsics": {
|
| 1945 |
+
"version": "1.1.0",
|
| 1946 |
+
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
| 1947 |
+
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
| 1948 |
+
"engines": {
|
| 1949 |
+
"node": ">= 0.4"
|
| 1950 |
+
}
|
| 1951 |
+
},
|
| 1952 |
+
"node_modules/merge2": {
|
| 1953 |
+
"version": "1.4.1",
|
| 1954 |
+
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
| 1955 |
+
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
| 1956 |
+
"dev": true,
|
| 1957 |
+
"engines": {
|
| 1958 |
+
"node": ">= 8"
|
| 1959 |
+
}
|
| 1960 |
+
},
|
| 1961 |
+
"node_modules/micromatch": {
|
| 1962 |
+
"version": "4.0.8",
|
| 1963 |
+
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
| 1964 |
+
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
| 1965 |
+
"dev": true,
|
| 1966 |
+
"dependencies": {
|
| 1967 |
+
"braces": "^3.0.3",
|
| 1968 |
+
"picomatch": "^2.3.1"
|
| 1969 |
+
},
|
| 1970 |
+
"engines": {
|
| 1971 |
+
"node": ">=8.6"
|
| 1972 |
+
}
|
| 1973 |
+
},
|
| 1974 |
+
"node_modules/mime-db": {
|
| 1975 |
+
"version": "1.52.0",
|
| 1976 |
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
| 1977 |
+
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
| 1978 |
+
"engines": {
|
| 1979 |
+
"node": ">= 0.6"
|
| 1980 |
+
}
|
| 1981 |
+
},
|
| 1982 |
+
"node_modules/mime-types": {
|
| 1983 |
+
"version": "2.1.35",
|
| 1984 |
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
| 1985 |
+
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
| 1986 |
+
"dependencies": {
|
| 1987 |
+
"mime-db": "1.52.0"
|
| 1988 |
+
},
|
| 1989 |
+
"engines": {
|
| 1990 |
+
"node": ">= 0.6"
|
| 1991 |
+
}
|
| 1992 |
+
},
|
| 1993 |
+
"node_modules/ms": {
|
| 1994 |
+
"version": "2.1.3",
|
| 1995 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 1996 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 1997 |
+
"dev": true
|
| 1998 |
+
},
|
| 1999 |
+
"node_modules/mz": {
|
| 2000 |
+
"version": "2.7.0",
|
| 2001 |
+
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
| 2002 |
+
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
| 2003 |
+
"dev": true,
|
| 2004 |
+
"dependencies": {
|
| 2005 |
+
"any-promise": "^1.0.0",
|
| 2006 |
+
"object-assign": "^4.0.1",
|
| 2007 |
+
"thenify-all": "^1.0.0"
|
| 2008 |
+
}
|
| 2009 |
+
},
|
| 2010 |
+
"node_modules/nanoid": {
|
| 2011 |
+
"version": "3.3.11",
|
| 2012 |
+
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
| 2013 |
+
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
| 2014 |
+
"dev": true,
|
| 2015 |
+
"funding": [
|
| 2016 |
+
{
|
| 2017 |
+
"type": "github",
|
| 2018 |
+
"url": "https://github.com/sponsors/ai"
|
| 2019 |
+
}
|
| 2020 |
+
],
|
| 2021 |
+
"bin": {
|
| 2022 |
+
"nanoid": "bin/nanoid.cjs"
|
| 2023 |
+
},
|
| 2024 |
+
"engines": {
|
| 2025 |
+
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 2026 |
+
}
|
| 2027 |
+
},
|
| 2028 |
+
"node_modules/node-releases": {
|
| 2029 |
+
"version": "2.0.37",
|
| 2030 |
+
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
|
| 2031 |
+
"integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
|
| 2032 |
+
"dev": true
|
| 2033 |
+
},
|
| 2034 |
+
"node_modules/normalize-path": {
|
| 2035 |
+
"version": "3.0.0",
|
| 2036 |
+
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
| 2037 |
+
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
| 2038 |
+
"dev": true,
|
| 2039 |
+
"engines": {
|
| 2040 |
+
"node": ">=0.10.0"
|
| 2041 |
+
}
|
| 2042 |
+
},
|
| 2043 |
+
"node_modules/object-assign": {
|
| 2044 |
+
"version": "4.1.1",
|
| 2045 |
+
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
| 2046 |
+
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
| 2047 |
+
"dev": true,
|
| 2048 |
+
"engines": {
|
| 2049 |
+
"node": ">=0.10.0"
|
| 2050 |
+
}
|
| 2051 |
+
},
|
| 2052 |
+
"node_modules/object-hash": {
|
| 2053 |
+
"version": "3.0.0",
|
| 2054 |
+
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
| 2055 |
+
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
| 2056 |
+
"dev": true,
|
| 2057 |
+
"engines": {
|
| 2058 |
+
"node": ">= 6"
|
| 2059 |
+
}
|
| 2060 |
+
},
|
| 2061 |
+
"node_modules/path-parse": {
|
| 2062 |
+
"version": "1.0.7",
|
| 2063 |
+
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
| 2064 |
+
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
| 2065 |
+
"dev": true
|
| 2066 |
+
},
|
| 2067 |
+
"node_modules/picocolors": {
|
| 2068 |
+
"version": "1.1.1",
|
| 2069 |
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 2070 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 2071 |
+
"dev": true
|
| 2072 |
+
},
|
| 2073 |
+
"node_modules/picomatch": {
|
| 2074 |
+
"version": "2.3.2",
|
| 2075 |
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
| 2076 |
+
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
| 2077 |
+
"dev": true,
|
| 2078 |
+
"engines": {
|
| 2079 |
+
"node": ">=8.6"
|
| 2080 |
+
},
|
| 2081 |
+
"funding": {
|
| 2082 |
+
"url": "https://github.com/sponsors/jonschlinkert"
|
| 2083 |
+
}
|
| 2084 |
+
},
|
| 2085 |
+
"node_modules/pify": {
|
| 2086 |
+
"version": "2.3.0",
|
| 2087 |
+
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
| 2088 |
+
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
| 2089 |
+
"dev": true,
|
| 2090 |
+
"engines": {
|
| 2091 |
+
"node": ">=0.10.0"
|
| 2092 |
+
}
|
| 2093 |
+
},
|
| 2094 |
+
"node_modules/pirates": {
|
| 2095 |
+
"version": "4.0.7",
|
| 2096 |
+
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
| 2097 |
+
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
|
| 2098 |
+
"dev": true,
|
| 2099 |
+
"engines": {
|
| 2100 |
+
"node": ">= 6"
|
| 2101 |
+
}
|
| 2102 |
+
},
|
| 2103 |
+
"node_modules/postcss": {
|
| 2104 |
+
"version": "8.5.8",
|
| 2105 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
| 2106 |
+
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
| 2107 |
+
"dev": true,
|
| 2108 |
+
"funding": [
|
| 2109 |
+
{
|
| 2110 |
+
"type": "opencollective",
|
| 2111 |
+
"url": "https://opencollective.com/postcss/"
|
| 2112 |
+
},
|
| 2113 |
+
{
|
| 2114 |
+
"type": "tidelift",
|
| 2115 |
+
"url": "https://tidelift.com/funding/github/npm/postcss"
|
| 2116 |
+
},
|
| 2117 |
+
{
|
| 2118 |
+
"type": "github",
|
| 2119 |
+
"url": "https://github.com/sponsors/ai"
|
| 2120 |
+
}
|
| 2121 |
+
],
|
| 2122 |
+
"dependencies": {
|
| 2123 |
+
"nanoid": "^3.3.11",
|
| 2124 |
+
"picocolors": "^1.1.1",
|
| 2125 |
+
"source-map-js": "^1.2.1"
|
| 2126 |
+
},
|
| 2127 |
+
"engines": {
|
| 2128 |
+
"node": "^10 || ^12 || >=14"
|
| 2129 |
+
}
|
| 2130 |
+
},
|
| 2131 |
+
"node_modules/postcss-import": {
|
| 2132 |
+
"version": "15.1.0",
|
| 2133 |
+
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
|
| 2134 |
+
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
|
| 2135 |
+
"dev": true,
|
| 2136 |
+
"dependencies": {
|
| 2137 |
+
"postcss-value-parser": "^4.0.0",
|
| 2138 |
+
"read-cache": "^1.0.0",
|
| 2139 |
+
"resolve": "^1.1.7"
|
| 2140 |
+
},
|
| 2141 |
+
"engines": {
|
| 2142 |
+
"node": ">=14.0.0"
|
| 2143 |
+
},
|
| 2144 |
+
"peerDependencies": {
|
| 2145 |
+
"postcss": "^8.0.0"
|
| 2146 |
+
}
|
| 2147 |
+
},
|
| 2148 |
+
"node_modules/postcss-js": {
|
| 2149 |
+
"version": "4.1.0",
|
| 2150 |
+
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
|
| 2151 |
+
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
|
| 2152 |
+
"dev": true,
|
| 2153 |
+
"funding": [
|
| 2154 |
+
{
|
| 2155 |
+
"type": "opencollective",
|
| 2156 |
+
"url": "https://opencollective.com/postcss/"
|
| 2157 |
+
},
|
| 2158 |
+
{
|
| 2159 |
+
"type": "github",
|
| 2160 |
+
"url": "https://github.com/sponsors/ai"
|
| 2161 |
+
}
|
| 2162 |
+
],
|
| 2163 |
+
"dependencies": {
|
| 2164 |
+
"camelcase-css": "^2.0.1"
|
| 2165 |
+
},
|
| 2166 |
+
"engines": {
|
| 2167 |
+
"node": "^12 || ^14 || >= 16"
|
| 2168 |
+
},
|
| 2169 |
+
"peerDependencies": {
|
| 2170 |
+
"postcss": "^8.4.21"
|
| 2171 |
+
}
|
| 2172 |
+
},
|
| 2173 |
+
"node_modules/postcss-load-config": {
|
| 2174 |
+
"version": "6.0.1",
|
| 2175 |
+
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
|
| 2176 |
+
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
|
| 2177 |
+
"dev": true,
|
| 2178 |
+
"funding": [
|
| 2179 |
+
{
|
| 2180 |
+
"type": "opencollective",
|
| 2181 |
+
"url": "https://opencollective.com/postcss/"
|
| 2182 |
+
},
|
| 2183 |
+
{
|
| 2184 |
+
"type": "github",
|
| 2185 |
+
"url": "https://github.com/sponsors/ai"
|
| 2186 |
+
}
|
| 2187 |
+
],
|
| 2188 |
+
"dependencies": {
|
| 2189 |
+
"lilconfig": "^3.1.1"
|
| 2190 |
+
},
|
| 2191 |
+
"engines": {
|
| 2192 |
+
"node": ">= 18"
|
| 2193 |
+
},
|
| 2194 |
+
"peerDependencies": {
|
| 2195 |
+
"jiti": ">=1.21.0",
|
| 2196 |
+
"postcss": ">=8.0.9",
|
| 2197 |
+
"tsx": "^4.8.1",
|
| 2198 |
+
"yaml": "^2.4.2"
|
| 2199 |
+
},
|
| 2200 |
+
"peerDependenciesMeta": {
|
| 2201 |
+
"jiti": {
|
| 2202 |
+
"optional": true
|
| 2203 |
+
},
|
| 2204 |
+
"postcss": {
|
| 2205 |
+
"optional": true
|
| 2206 |
+
},
|
| 2207 |
+
"tsx": {
|
| 2208 |
+
"optional": true
|
| 2209 |
+
},
|
| 2210 |
+
"yaml": {
|
| 2211 |
+
"optional": true
|
| 2212 |
+
}
|
| 2213 |
+
}
|
| 2214 |
+
},
|
| 2215 |
+
"node_modules/postcss-nested": {
|
| 2216 |
+
"version": "6.2.0",
|
| 2217 |
+
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
|
| 2218 |
+
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
|
| 2219 |
+
"dev": true,
|
| 2220 |
+
"funding": [
|
| 2221 |
+
{
|
| 2222 |
+
"type": "opencollective",
|
| 2223 |
+
"url": "https://opencollective.com/postcss/"
|
| 2224 |
+
},
|
| 2225 |
+
{
|
| 2226 |
+
"type": "github",
|
| 2227 |
+
"url": "https://github.com/sponsors/ai"
|
| 2228 |
+
}
|
| 2229 |
+
],
|
| 2230 |
+
"dependencies": {
|
| 2231 |
+
"postcss-selector-parser": "^6.1.1"
|
| 2232 |
+
},
|
| 2233 |
+
"engines": {
|
| 2234 |
+
"node": ">=12.0"
|
| 2235 |
+
},
|
| 2236 |
+
"peerDependencies": {
|
| 2237 |
+
"postcss": "^8.2.14"
|
| 2238 |
+
}
|
| 2239 |
+
},
|
| 2240 |
+
"node_modules/postcss-selector-parser": {
|
| 2241 |
+
"version": "6.1.2",
|
| 2242 |
+
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
| 2243 |
+
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
| 2244 |
+
"dev": true,
|
| 2245 |
+
"dependencies": {
|
| 2246 |
+
"cssesc": "^3.0.0",
|
| 2247 |
+
"util-deprecate": "^1.0.2"
|
| 2248 |
+
},
|
| 2249 |
+
"engines": {
|
| 2250 |
+
"node": ">=4"
|
| 2251 |
+
}
|
| 2252 |
+
},
|
| 2253 |
+
"node_modules/postcss-value-parser": {
|
| 2254 |
+
"version": "4.2.0",
|
| 2255 |
+
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
| 2256 |
+
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
| 2257 |
+
"dev": true
|
| 2258 |
+
},
|
| 2259 |
+
"node_modules/proxy-from-env": {
|
| 2260 |
+
"version": "2.1.0",
|
| 2261 |
+
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
| 2262 |
+
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
| 2263 |
+
"engines": {
|
| 2264 |
+
"node": ">=10"
|
| 2265 |
+
}
|
| 2266 |
+
},
|
| 2267 |
+
"node_modules/queue-microtask": {
|
| 2268 |
+
"version": "1.2.3",
|
| 2269 |
+
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
| 2270 |
+
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
| 2271 |
+
"dev": true,
|
| 2272 |
+
"funding": [
|
| 2273 |
+
{
|
| 2274 |
+
"type": "github",
|
| 2275 |
+
"url": "https://github.com/sponsors/feross"
|
| 2276 |
+
},
|
| 2277 |
+
{
|
| 2278 |
+
"type": "patreon",
|
| 2279 |
+
"url": "https://www.patreon.com/feross"
|
| 2280 |
+
},
|
| 2281 |
+
{
|
| 2282 |
+
"type": "consulting",
|
| 2283 |
+
"url": "https://feross.org/support"
|
| 2284 |
+
}
|
| 2285 |
+
]
|
| 2286 |
+
},
|
| 2287 |
+
"node_modules/react": {
|
| 2288 |
+
"version": "18.3.1",
|
| 2289 |
+
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
| 2290 |
+
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
| 2291 |
+
"dependencies": {
|
| 2292 |
+
"loose-envify": "^1.1.0"
|
| 2293 |
+
},
|
| 2294 |
+
"engines": {
|
| 2295 |
+
"node": ">=0.10.0"
|
| 2296 |
+
}
|
| 2297 |
+
},
|
| 2298 |
+
"node_modules/react-dom": {
|
| 2299 |
+
"version": "18.3.1",
|
| 2300 |
+
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
| 2301 |
+
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
| 2302 |
+
"dependencies": {
|
| 2303 |
+
"loose-envify": "^1.1.0",
|
| 2304 |
+
"scheduler": "^0.23.2"
|
| 2305 |
+
},
|
| 2306 |
+
"peerDependencies": {
|
| 2307 |
+
"react": "^18.3.1"
|
| 2308 |
+
}
|
| 2309 |
+
},
|
| 2310 |
+
"node_modules/react-refresh": {
|
| 2311 |
+
"version": "0.17.0",
|
| 2312 |
+
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
| 2313 |
+
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
|
| 2314 |
+
"dev": true,
|
| 2315 |
+
"engines": {
|
| 2316 |
+
"node": ">=0.10.0"
|
| 2317 |
+
}
|
| 2318 |
+
},
|
| 2319 |
+
"node_modules/read-cache": {
|
| 2320 |
+
"version": "1.0.0",
|
| 2321 |
+
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
| 2322 |
+
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
| 2323 |
+
"dev": true,
|
| 2324 |
+
"dependencies": {
|
| 2325 |
+
"pify": "^2.3.0"
|
| 2326 |
+
}
|
| 2327 |
+
},
|
| 2328 |
+
"node_modules/readdirp": {
|
| 2329 |
+
"version": "3.6.0",
|
| 2330 |
+
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
| 2331 |
+
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
| 2332 |
+
"dev": true,
|
| 2333 |
+
"dependencies": {
|
| 2334 |
+
"picomatch": "^2.2.1"
|
| 2335 |
+
},
|
| 2336 |
+
"engines": {
|
| 2337 |
+
"node": ">=8.10.0"
|
| 2338 |
+
}
|
| 2339 |
+
},
|
| 2340 |
+
"node_modules/resolve": {
|
| 2341 |
+
"version": "1.22.11",
|
| 2342 |
+
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
| 2343 |
+
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
| 2344 |
+
"dev": true,
|
| 2345 |
+
"dependencies": {
|
| 2346 |
+
"is-core-module": "^2.16.1",
|
| 2347 |
+
"path-parse": "^1.0.7",
|
| 2348 |
+
"supports-preserve-symlinks-flag": "^1.0.0"
|
| 2349 |
+
},
|
| 2350 |
+
"bin": {
|
| 2351 |
+
"resolve": "bin/resolve"
|
| 2352 |
+
},
|
| 2353 |
+
"engines": {
|
| 2354 |
+
"node": ">= 0.4"
|
| 2355 |
+
},
|
| 2356 |
+
"funding": {
|
| 2357 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 2358 |
+
}
|
| 2359 |
+
},
|
| 2360 |
+
"node_modules/reusify": {
|
| 2361 |
+
"version": "1.1.0",
|
| 2362 |
+
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
| 2363 |
+
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
| 2364 |
+
"dev": true,
|
| 2365 |
+
"engines": {
|
| 2366 |
+
"iojs": ">=1.0.0",
|
| 2367 |
+
"node": ">=0.10.0"
|
| 2368 |
+
}
|
| 2369 |
+
},
|
| 2370 |
+
"node_modules/rollup": {
|
| 2371 |
+
"version": "4.60.1",
|
| 2372 |
+
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
|
| 2373 |
+
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
|
| 2374 |
+
"dev": true,
|
| 2375 |
+
"dependencies": {
|
| 2376 |
+
"@types/estree": "1.0.8"
|
| 2377 |
+
},
|
| 2378 |
+
"bin": {
|
| 2379 |
+
"rollup": "dist/bin/rollup"
|
| 2380 |
+
},
|
| 2381 |
+
"engines": {
|
| 2382 |
+
"node": ">=18.0.0",
|
| 2383 |
+
"npm": ">=8.0.0"
|
| 2384 |
+
},
|
| 2385 |
+
"optionalDependencies": {
|
| 2386 |
+
"@rollup/rollup-android-arm-eabi": "4.60.1",
|
| 2387 |
+
"@rollup/rollup-android-arm64": "4.60.1",
|
| 2388 |
+
"@rollup/rollup-darwin-arm64": "4.60.1",
|
| 2389 |
+
"@rollup/rollup-darwin-x64": "4.60.1",
|
| 2390 |
+
"@rollup/rollup-freebsd-arm64": "4.60.1",
|
| 2391 |
+
"@rollup/rollup-freebsd-x64": "4.60.1",
|
| 2392 |
+
"@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
|
| 2393 |
+
"@rollup/rollup-linux-arm-musleabihf": "4.60.1",
|
| 2394 |
+
"@rollup/rollup-linux-arm64-gnu": "4.60.1",
|
| 2395 |
+
"@rollup/rollup-linux-arm64-musl": "4.60.1",
|
| 2396 |
+
"@rollup/rollup-linux-loong64-gnu": "4.60.1",
|
| 2397 |
+
"@rollup/rollup-linux-loong64-musl": "4.60.1",
|
| 2398 |
+
"@rollup/rollup-linux-ppc64-gnu": "4.60.1",
|
| 2399 |
+
"@rollup/rollup-linux-ppc64-musl": "4.60.1",
|
| 2400 |
+
"@rollup/rollup-linux-riscv64-gnu": "4.60.1",
|
| 2401 |
+
"@rollup/rollup-linux-riscv64-musl": "4.60.1",
|
| 2402 |
+
"@rollup/rollup-linux-s390x-gnu": "4.60.1",
|
| 2403 |
+
"@rollup/rollup-linux-x64-gnu": "4.60.1",
|
| 2404 |
+
"@rollup/rollup-linux-x64-musl": "4.60.1",
|
| 2405 |
+
"@rollup/rollup-openbsd-x64": "4.60.1",
|
| 2406 |
+
"@rollup/rollup-openharmony-arm64": "4.60.1",
|
| 2407 |
+
"@rollup/rollup-win32-arm64-msvc": "4.60.1",
|
| 2408 |
+
"@rollup/rollup-win32-ia32-msvc": "4.60.1",
|
| 2409 |
+
"@rollup/rollup-win32-x64-gnu": "4.60.1",
|
| 2410 |
+
"@rollup/rollup-win32-x64-msvc": "4.60.1",
|
| 2411 |
+
"fsevents": "~2.3.2"
|
| 2412 |
+
}
|
| 2413 |
+
},
|
| 2414 |
+
"node_modules/run-parallel": {
|
| 2415 |
+
"version": "1.2.0",
|
| 2416 |
+
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
| 2417 |
+
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
| 2418 |
+
"dev": true,
|
| 2419 |
+
"funding": [
|
| 2420 |
+
{
|
| 2421 |
+
"type": "github",
|
| 2422 |
+
"url": "https://github.com/sponsors/feross"
|
| 2423 |
+
},
|
| 2424 |
+
{
|
| 2425 |
+
"type": "patreon",
|
| 2426 |
+
"url": "https://www.patreon.com/feross"
|
| 2427 |
+
},
|
| 2428 |
+
{
|
| 2429 |
+
"type": "consulting",
|
| 2430 |
+
"url": "https://feross.org/support"
|
| 2431 |
+
}
|
| 2432 |
+
],
|
| 2433 |
+
"dependencies": {
|
| 2434 |
+
"queue-microtask": "^1.2.2"
|
| 2435 |
+
}
|
| 2436 |
+
},
|
| 2437 |
+
"node_modules/scheduler": {
|
| 2438 |
+
"version": "0.23.2",
|
| 2439 |
+
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
| 2440 |
+
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
| 2441 |
+
"dependencies": {
|
| 2442 |
+
"loose-envify": "^1.1.0"
|
| 2443 |
+
}
|
| 2444 |
+
},
|
| 2445 |
+
"node_modules/semver": {
|
| 2446 |
+
"version": "6.3.1",
|
| 2447 |
+
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
| 2448 |
+
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
| 2449 |
+
"dev": true,
|
| 2450 |
+
"bin": {
|
| 2451 |
+
"semver": "bin/semver.js"
|
| 2452 |
+
}
|
| 2453 |
+
},
|
| 2454 |
+
"node_modules/source-map-js": {
|
| 2455 |
+
"version": "1.2.1",
|
| 2456 |
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
| 2457 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
| 2458 |
+
"dev": true,
|
| 2459 |
+
"engines": {
|
| 2460 |
+
"node": ">=0.10.0"
|
| 2461 |
+
}
|
| 2462 |
+
},
|
| 2463 |
+
"node_modules/sucrase": {
|
| 2464 |
+
"version": "3.35.1",
|
| 2465 |
+
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
|
| 2466 |
+
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
|
| 2467 |
+
"dev": true,
|
| 2468 |
+
"dependencies": {
|
| 2469 |
+
"@jridgewell/gen-mapping": "^0.3.2",
|
| 2470 |
+
"commander": "^4.0.0",
|
| 2471 |
+
"lines-and-columns": "^1.1.6",
|
| 2472 |
+
"mz": "^2.7.0",
|
| 2473 |
+
"pirates": "^4.0.1",
|
| 2474 |
+
"tinyglobby": "^0.2.11",
|
| 2475 |
+
"ts-interface-checker": "^0.1.9"
|
| 2476 |
+
},
|
| 2477 |
+
"bin": {
|
| 2478 |
+
"sucrase": "bin/sucrase",
|
| 2479 |
+
"sucrase-node": "bin/sucrase-node"
|
| 2480 |
+
},
|
| 2481 |
+
"engines": {
|
| 2482 |
+
"node": ">=16 || 14 >=14.17"
|
| 2483 |
+
}
|
| 2484 |
+
},
|
| 2485 |
+
"node_modules/supports-preserve-symlinks-flag": {
|
| 2486 |
+
"version": "1.0.0",
|
| 2487 |
+
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
| 2488 |
+
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
| 2489 |
+
"dev": true,
|
| 2490 |
+
"engines": {
|
| 2491 |
+
"node": ">= 0.4"
|
| 2492 |
+
},
|
| 2493 |
+
"funding": {
|
| 2494 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 2495 |
+
}
|
| 2496 |
+
},
|
| 2497 |
+
"node_modules/tailwindcss": {
|
| 2498 |
+
"version": "3.4.19",
|
| 2499 |
+
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
| 2500 |
+
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
| 2501 |
+
"dev": true,
|
| 2502 |
+
"dependencies": {
|
| 2503 |
+
"@alloc/quick-lru": "^5.2.0",
|
| 2504 |
+
"arg": "^5.0.2",
|
| 2505 |
+
"chokidar": "^3.6.0",
|
| 2506 |
+
"didyoumean": "^1.2.2",
|
| 2507 |
+
"dlv": "^1.1.3",
|
| 2508 |
+
"fast-glob": "^3.3.2",
|
| 2509 |
+
"glob-parent": "^6.0.2",
|
| 2510 |
+
"is-glob": "^4.0.3",
|
| 2511 |
+
"jiti": "^1.21.7",
|
| 2512 |
+
"lilconfig": "^3.1.3",
|
| 2513 |
+
"micromatch": "^4.0.8",
|
| 2514 |
+
"normalize-path": "^3.0.0",
|
| 2515 |
+
"object-hash": "^3.0.0",
|
| 2516 |
+
"picocolors": "^1.1.1",
|
| 2517 |
+
"postcss": "^8.4.47",
|
| 2518 |
+
"postcss-import": "^15.1.0",
|
| 2519 |
+
"postcss-js": "^4.0.1",
|
| 2520 |
+
"postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
|
| 2521 |
+
"postcss-nested": "^6.2.0",
|
| 2522 |
+
"postcss-selector-parser": "^6.1.2",
|
| 2523 |
+
"resolve": "^1.22.8",
|
| 2524 |
+
"sucrase": "^3.35.0"
|
| 2525 |
+
},
|
| 2526 |
+
"bin": {
|
| 2527 |
+
"tailwind": "lib/cli.js",
|
| 2528 |
+
"tailwindcss": "lib/cli.js"
|
| 2529 |
+
},
|
| 2530 |
+
"engines": {
|
| 2531 |
+
"node": ">=14.0.0"
|
| 2532 |
+
}
|
| 2533 |
+
},
|
| 2534 |
+
"node_modules/thenify": {
|
| 2535 |
+
"version": "3.3.1",
|
| 2536 |
+
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
| 2537 |
+
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
| 2538 |
+
"dev": true,
|
| 2539 |
+
"dependencies": {
|
| 2540 |
+
"any-promise": "^1.0.0"
|
| 2541 |
+
}
|
| 2542 |
+
},
|
| 2543 |
+
"node_modules/thenify-all": {
|
| 2544 |
+
"version": "1.6.0",
|
| 2545 |
+
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
| 2546 |
+
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
|
| 2547 |
+
"dev": true,
|
| 2548 |
+
"dependencies": {
|
| 2549 |
+
"thenify": ">= 3.1.0 < 4"
|
| 2550 |
+
},
|
| 2551 |
+
"engines": {
|
| 2552 |
+
"node": ">=0.8"
|
| 2553 |
+
}
|
| 2554 |
+
},
|
| 2555 |
+
"node_modules/tinyglobby": {
|
| 2556 |
+
"version": "0.2.15",
|
| 2557 |
+
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
| 2558 |
+
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
| 2559 |
+
"dev": true,
|
| 2560 |
+
"dependencies": {
|
| 2561 |
+
"fdir": "^6.5.0",
|
| 2562 |
+
"picomatch": "^4.0.3"
|
| 2563 |
+
},
|
| 2564 |
+
"engines": {
|
| 2565 |
+
"node": ">=12.0.0"
|
| 2566 |
+
},
|
| 2567 |
+
"funding": {
|
| 2568 |
+
"url": "https://github.com/sponsors/SuperchupuDev"
|
| 2569 |
+
}
|
| 2570 |
+
},
|
| 2571 |
+
"node_modules/tinyglobby/node_modules/fdir": {
|
| 2572 |
+
"version": "6.5.0",
|
| 2573 |
+
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
| 2574 |
+
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
| 2575 |
+
"dev": true,
|
| 2576 |
+
"engines": {
|
| 2577 |
+
"node": ">=12.0.0"
|
| 2578 |
+
},
|
| 2579 |
+
"peerDependencies": {
|
| 2580 |
+
"picomatch": "^3 || ^4"
|
| 2581 |
+
},
|
| 2582 |
+
"peerDependenciesMeta": {
|
| 2583 |
+
"picomatch": {
|
| 2584 |
+
"optional": true
|
| 2585 |
+
}
|
| 2586 |
+
}
|
| 2587 |
+
},
|
| 2588 |
+
"node_modules/tinyglobby/node_modules/picomatch": {
|
| 2589 |
+
"version": "4.0.4",
|
| 2590 |
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
| 2591 |
+
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
| 2592 |
+
"dev": true,
|
| 2593 |
+
"engines": {
|
| 2594 |
+
"node": ">=12"
|
| 2595 |
+
},
|
| 2596 |
+
"funding": {
|
| 2597 |
+
"url": "https://github.com/sponsors/jonschlinkert"
|
| 2598 |
+
}
|
| 2599 |
+
},
|
| 2600 |
+
"node_modules/to-regex-range": {
|
| 2601 |
+
"version": "5.0.1",
|
| 2602 |
+
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
| 2603 |
+
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
| 2604 |
+
"dev": true,
|
| 2605 |
+
"dependencies": {
|
| 2606 |
+
"is-number": "^7.0.0"
|
| 2607 |
+
},
|
| 2608 |
+
"engines": {
|
| 2609 |
+
"node": ">=8.0"
|
| 2610 |
+
}
|
| 2611 |
+
},
|
| 2612 |
+
"node_modules/ts-interface-checker": {
|
| 2613 |
+
"version": "0.1.13",
|
| 2614 |
+
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
| 2615 |
+
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
| 2616 |
+
"dev": true
|
| 2617 |
+
},
|
| 2618 |
+
"node_modules/update-browserslist-db": {
|
| 2619 |
+
"version": "1.2.3",
|
| 2620 |
+
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
| 2621 |
+
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
| 2622 |
+
"dev": true,
|
| 2623 |
+
"funding": [
|
| 2624 |
+
{
|
| 2625 |
+
"type": "opencollective",
|
| 2626 |
+
"url": "https://opencollective.com/browserslist"
|
| 2627 |
+
},
|
| 2628 |
+
{
|
| 2629 |
+
"type": "tidelift",
|
| 2630 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 2631 |
+
},
|
| 2632 |
+
{
|
| 2633 |
+
"type": "github",
|
| 2634 |
+
"url": "https://github.com/sponsors/ai"
|
| 2635 |
+
}
|
| 2636 |
+
],
|
| 2637 |
+
"dependencies": {
|
| 2638 |
+
"escalade": "^3.2.0",
|
| 2639 |
+
"picocolors": "^1.1.1"
|
| 2640 |
+
},
|
| 2641 |
+
"bin": {
|
| 2642 |
+
"update-browserslist-db": "cli.js"
|
| 2643 |
+
},
|
| 2644 |
+
"peerDependencies": {
|
| 2645 |
+
"browserslist": ">= 4.21.0"
|
| 2646 |
+
}
|
| 2647 |
+
},
|
| 2648 |
+
"node_modules/util-deprecate": {
|
| 2649 |
+
"version": "1.0.2",
|
| 2650 |
+
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
| 2651 |
+
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
| 2652 |
+
"dev": true
|
| 2653 |
+
},
|
| 2654 |
+
"node_modules/vite": {
|
| 2655 |
+
"version": "5.4.21",
|
| 2656 |
+
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
| 2657 |
+
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
| 2658 |
+
"dev": true,
|
| 2659 |
+
"dependencies": {
|
| 2660 |
+
"esbuild": "^0.21.3",
|
| 2661 |
+
"postcss": "^8.4.43",
|
| 2662 |
+
"rollup": "^4.20.0"
|
| 2663 |
+
},
|
| 2664 |
+
"bin": {
|
| 2665 |
+
"vite": "bin/vite.js"
|
| 2666 |
+
},
|
| 2667 |
+
"engines": {
|
| 2668 |
+
"node": "^18.0.0 || >=20.0.0"
|
| 2669 |
+
},
|
| 2670 |
+
"funding": {
|
| 2671 |
+
"url": "https://github.com/vitejs/vite?sponsor=1"
|
| 2672 |
+
},
|
| 2673 |
+
"optionalDependencies": {
|
| 2674 |
+
"fsevents": "~2.3.3"
|
| 2675 |
+
},
|
| 2676 |
+
"peerDependencies": {
|
| 2677 |
+
"@types/node": "^18.0.0 || >=20.0.0",
|
| 2678 |
+
"less": "*",
|
| 2679 |
+
"lightningcss": "^1.21.0",
|
| 2680 |
+
"sass": "*",
|
| 2681 |
+
"sass-embedded": "*",
|
| 2682 |
+
"stylus": "*",
|
| 2683 |
+
"sugarss": "*",
|
| 2684 |
+
"terser": "^5.4.0"
|
| 2685 |
+
},
|
| 2686 |
+
"peerDependenciesMeta": {
|
| 2687 |
+
"@types/node": {
|
| 2688 |
+
"optional": true
|
| 2689 |
+
},
|
| 2690 |
+
"less": {
|
| 2691 |
+
"optional": true
|
| 2692 |
+
},
|
| 2693 |
+
"lightningcss": {
|
| 2694 |
+
"optional": true
|
| 2695 |
+
},
|
| 2696 |
+
"sass": {
|
| 2697 |
+
"optional": true
|
| 2698 |
+
},
|
| 2699 |
+
"sass-embedded": {
|
| 2700 |
+
"optional": true
|
| 2701 |
+
},
|
| 2702 |
+
"stylus": {
|
| 2703 |
+
"optional": true
|
| 2704 |
+
},
|
| 2705 |
+
"sugarss": {
|
| 2706 |
+
"optional": true
|
| 2707 |
+
},
|
| 2708 |
+
"terser": {
|
| 2709 |
+
"optional": true
|
| 2710 |
+
}
|
| 2711 |
+
}
|
| 2712 |
+
},
|
| 2713 |
+
"node_modules/yallist": {
|
| 2714 |
+
"version": "3.1.1",
|
| 2715 |
+
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
| 2716 |
+
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
| 2717 |
+
"dev": true
|
| 2718 |
+
}
|
| 2719 |
+
}
|
| 2720 |
+
}
|
frontend/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "medical-predictor-frontend",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.1",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"react": "^18.2.0",
|
| 13 |
+
"react-dom": "^18.2.0",
|
| 14 |
+
"axios": "^1.6.0",
|
| 15 |
+
"lucide-react": "^0.294.0"
|
| 16 |
+
},
|
| 17 |
+
"devDependencies": {
|
| 18 |
+
"@types/react": "^18.2.0",
|
| 19 |
+
"@types/react-dom": "^18.2.0",
|
| 20 |
+
"@vitejs/plugin-react": "^4.2.1",
|
| 21 |
+
"vite": "^5.0.0",
|
| 22 |
+
"tailwindcss": "^3.3.0",
|
| 23 |
+
"postcss": "^8.4.31",
|
| 24 |
+
"autoprefixer": "^10.4.16"
|
| 25 |
+
}
|
| 26 |
+
}
|
frontend/postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
}
|
frontend/src/App.jsx
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react'
|
| 2 |
+
import useChat from './hooks/useChat'
|
| 3 |
+
import FeatureSidebar from './components/FeatureSidebar'
|
| 4 |
+
import ChatWindow from './components/ChatWindow'
|
| 5 |
+
import InputBar from './components/InputBar'
|
| 6 |
+
|
| 7 |
+
function App() {
|
| 8 |
+
const { messages, features, loading, prediction, sendMessage, resetChat } = useChat()
|
| 9 |
+
const [showConfirm, setShowConfirm] = useState(false)
|
| 10 |
+
|
| 11 |
+
const handleReset = async () => {
|
| 12 |
+
setShowConfirm(true)
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
const confirmReset = async () => {
|
| 16 |
+
setShowConfirm(false)
|
| 17 |
+
await resetChat()
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
return (
|
| 21 |
+
<div className="flex h-screen bg-white overflow-hidden">
|
| 22 |
+
{/* Sidebar with feature tracking */}
|
| 23 |
+
<FeatureSidebar features={features} />
|
| 24 |
+
|
| 25 |
+
{/* Main chat area */}
|
| 26 |
+
<div className="flex-1 flex flex-col">
|
| 27 |
+
{/* Chat messages */}
|
| 28 |
+
<ChatWindow messages={messages} prediction={prediction} />
|
| 29 |
+
|
| 30 |
+
{/* Input bar */}
|
| 31 |
+
<InputBar
|
| 32 |
+
onSend={sendMessage}
|
| 33 |
+
onReset={handleReset}
|
| 34 |
+
disabled={loading}
|
| 35 |
+
/>
|
| 36 |
+
</div>
|
| 37 |
+
|
| 38 |
+
{/* Reset Confirmation Modal */}
|
| 39 |
+
{showConfirm && (
|
| 40 |
+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
| 41 |
+
<div className="bg-white rounded-lg shadow-xl p-6 max-w-sm mx-4">
|
| 42 |
+
<h3 className="text-lg font-semibold text-gray-900 mb-2">Start New Assessment?</h3>
|
| 43 |
+
<p className="text-gray-600 mb-6">
|
| 44 |
+
This will clear all your current health data and start a fresh assessment.
|
| 45 |
+
</p>
|
| 46 |
+
<div className="flex gap-3 justify-end">
|
| 47 |
+
<button
|
| 48 |
+
onClick={() => setShowConfirm(false)}
|
| 49 |
+
className="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 font-medium transition-colors"
|
| 50 |
+
>
|
| 51 |
+
Cancel
|
| 52 |
+
</button>
|
| 53 |
+
<button
|
| 54 |
+
onClick={confirmReset}
|
| 55 |
+
className="px-4 py-2 rounded-lg bg-purple-600 hover:bg-purple-700 text-white font-medium transition-colors"
|
| 56 |
+
>
|
| 57 |
+
Start New
|
| 58 |
+
</button>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
)}
|
| 63 |
+
</div>
|
| 64 |
+
)
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
export default App
|
frontend/src/components/ChatWindow.jsx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useRef } from 'react'
|
| 2 |
+
import MessageBubble from './MessageBubble'
|
| 3 |
+
import DiagnosisCard from './DiagnosisCard'
|
| 4 |
+
|
| 5 |
+
const ChatWindow = ({ messages, prediction }) => {
|
| 6 |
+
const messagesEndRef = useRef(null)
|
| 7 |
+
|
| 8 |
+
useEffect(() => {
|
| 9 |
+
// Auto-scroll to bottom on new message
|
| 10 |
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
| 11 |
+
}, [messages, prediction])
|
| 12 |
+
|
| 13 |
+
return (
|
| 14 |
+
<div className="flex-1 overflow-y-auto bg-white px-6 py-6">
|
| 15 |
+
<div className="max-w-2xl mx-auto">
|
| 16 |
+
{messages.length === 0 && (
|
| 17 |
+
<div className="flex items-center justify-center h-full text-center">
|
| 18 |
+
<div>
|
| 19 |
+
<h2 className="text-3xl font-bold text-gray-800 mb-3">Medical Diagnosis AI</h2>
|
| 20 |
+
<p className="text-gray-500 mb-2">Your personalized health assessment</p>
|
| 21 |
+
<p className="text-xs text-gray-400 max-w-md mx-auto">
|
| 22 |
+
Answer health questions to receive a personalized diagnosis assessment.
|
| 23 |
+
All information is secure and used only for analysis.
|
| 24 |
+
</p>
|
| 25 |
+
</div>
|
| 26 |
+
</div>
|
| 27 |
+
)}
|
| 28 |
+
|
| 29 |
+
{messages.map((msg, idx) => (
|
| 30 |
+
<MessageBubble key={idx} role={msg.role} content={msg.content} />
|
| 31 |
+
))}
|
| 32 |
+
|
| 33 |
+
{/* Show diagnosis card when prediction is complete */}
|
| 34 |
+
{prediction && (
|
| 35 |
+
<div className="mt-6 mb-4">
|
| 36 |
+
<DiagnosisCard diagnosis={prediction} />
|
| 37 |
+
</div>
|
| 38 |
+
)}
|
| 39 |
+
|
| 40 |
+
<div ref={messagesEndRef} />
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
)
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
export default ChatWindow
|
frontend/src/components/DiagnosisCard.jsx
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* DiagnosisCard Component
|
| 3 |
+
*
|
| 4 |
+
* Professional diagnosis result card with:
|
| 5 |
+
* - Light background with professional styling
|
| 6 |
+
* - Confidence gauge with progress bar
|
| 7 |
+
* - Comprehensive Patient Data Report grid
|
| 8 |
+
* - Clean, focused layout
|
| 9 |
+
*/
|
| 10 |
+
const DiagnosisCard = ({ diagnosis }) => {
|
| 11 |
+
if (!diagnosis) return null
|
| 12 |
+
|
| 13 |
+
const {
|
| 14 |
+
prediction_name,
|
| 15 |
+
confidence,
|
| 16 |
+
explanation,
|
| 17 |
+
features = {},
|
| 18 |
+
} = diagnosis
|
| 19 |
+
|
| 20 |
+
// Calculate confidence percentage
|
| 21 |
+
const confidencePercent = (confidence * 100).toFixed(1)
|
| 22 |
+
|
| 23 |
+
// Feature display labels for the Patient Data Report
|
| 24 |
+
const featureLabels = {
|
| 25 |
+
Age: 'Age',
|
| 26 |
+
Glucose: 'Blood Glucose',
|
| 27 |
+
HbA1c: 'HbA1c',
|
| 28 |
+
BMI: 'BMI',
|
| 29 |
+
Cholesterol: 'Cholesterol',
|
| 30 |
+
Triglycerides: 'Triglycerides',
|
| 31 |
+
BloodPressure: 'Blood Pressure',
|
| 32 |
+
PhysicalActivity: 'Physical Activity',
|
| 33 |
+
SleepHours: 'Sleep Hours',
|
| 34 |
+
StressLevel: 'Stress Level',
|
| 35 |
+
DietScore: 'Diet Score',
|
| 36 |
+
Smoking: 'Smoking',
|
| 37 |
+
Alcohol: 'Alcohol',
|
| 38 |
+
FamilyHistory: 'Family History',
|
| 39 |
+
LengthOfStay: 'Length of Stay',
|
| 40 |
+
OxygenSaturation: 'Oxygen Saturation',
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// Get progress bar color based on confidence
|
| 44 |
+
const getProgressBarColor = () => {
|
| 45 |
+
if (confidencePercent >= 80) return 'bg-green-500'
|
| 46 |
+
if (confidencePercent >= 60) return 'bg-amber-500'
|
| 47 |
+
return 'bg-red-500'
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
return (
|
| 51 |
+
<div className="rounded-2xl p-6 border-2 bg-slate-50 border-slate-200">
|
| 52 |
+
{/* Header */}
|
| 53 |
+
<h2 className="text-xl font-bold text-slate-900 mb-4">
|
| 54 |
+
Health Assessment Summary
|
| 55 |
+
</h2>
|
| 56 |
+
|
| 57 |
+
{/* Diagnosis Display */}
|
| 58 |
+
<div className="mb-6">
|
| 59 |
+
<p className="text-sm font-medium text-slate-600 mb-2">Diagnosis</p>
|
| 60 |
+
<h3 className="text-3xl font-bold text-slate-900">
|
| 61 |
+
{prediction_name}
|
| 62 |
+
</h3>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
{/* Confidence Gauge */}
|
| 66 |
+
<div className="mb-6">
|
| 67 |
+
<div className="flex items-center justify-between mb-2">
|
| 68 |
+
<p className="text-sm font-medium text-slate-700">Confidence Level</p>
|
| 69 |
+
<p className="text-sm font-bold text-slate-900">{confidencePercent}%</p>
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
{/* Progress Bar */}
|
| 73 |
+
<div className="w-full h-2 bg-slate-200 rounded-full overflow-hidden">
|
| 74 |
+
<div
|
| 75 |
+
className={`h-full ${getProgressBarColor()} transition-all duration-500`}
|
| 76 |
+
style={{ width: `${confidencePercent}%` }}
|
| 77 |
+
/>
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
{/* Confidence Description */}
|
| 81 |
+
<p className="text-xs text-slate-500 mt-2">
|
| 82 |
+
{confidencePercent >= 80
|
| 83 |
+
? 'High confidence prediction'
|
| 84 |
+
: confidencePercent >= 60
|
| 85 |
+
? 'Moderate confidence prediction'
|
| 86 |
+
: 'Lower confidence prediction - consult healthcare provider'}
|
| 87 |
+
</p>
|
| 88 |
+
</div>
|
| 89 |
+
|
| 90 |
+
{/* Explanation */}
|
| 91 |
+
<div className="bg-white/50 rounded-lg p-4 border border-slate-200 mb-6">
|
| 92 |
+
<p className="text-sm text-slate-700 leading-relaxed">
|
| 93 |
+
{explanation}
|
| 94 |
+
</p>
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
{/* Patient Data Report */}
|
| 98 |
+
<div className="border-t border-slate-200 mt-4 pt-4">
|
| 99 |
+
<h3 className="text-lg font-bold text-slate-900 mb-4">Patient Data Report</h3>
|
| 100 |
+
|
| 101 |
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
| 102 |
+
{Object.entries(features).map(([key, value]) => {
|
| 103 |
+
// Get the display label for this feature
|
| 104 |
+
const displayLabel = featureLabels[key] || key
|
| 105 |
+
|
| 106 |
+
return (
|
| 107 |
+
<div key={key} className="space-y-1">
|
| 108 |
+
<p className="text-xs font-medium text-slate-500 uppercase tracking-wider">
|
| 109 |
+
{displayLabel}
|
| 110 |
+
</p>
|
| 111 |
+
<p className="text-sm font-semibold text-slate-900">
|
| 112 |
+
{value !== null && value !== undefined ? String(value) : 'β'}
|
| 113 |
+
</p>
|
| 114 |
+
</div>
|
| 115 |
+
)
|
| 116 |
+
})}
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
</div>
|
| 120 |
+
)
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
export default DiagnosisCard
|
frontend/src/components/FeatureSidebar.jsx
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import MetricItem from './MetricItem'
|
| 2 |
+
|
| 3 |
+
const FEATURE_LABELS = {
|
| 4 |
+
'Age': 'Age',
|
| 5 |
+
'Glucose': 'Blood Glucose',
|
| 6 |
+
'HbA1c': 'HbA1c',
|
| 7 |
+
'BMI': 'BMI',
|
| 8 |
+
'Cholesterol': 'Cholesterol',
|
| 9 |
+
'Triglycerides': 'Triglycerides',
|
| 10 |
+
'Blood Pressure': 'Blood Pressure',
|
| 11 |
+
'Physical Activity': 'Physical Activity',
|
| 12 |
+
'Sleep Hours': 'Sleep Hours',
|
| 13 |
+
'Stress Level': 'Stress Level',
|
| 14 |
+
'Diet Score': 'Diet Score',
|
| 15 |
+
'Smoking': 'Smoking',
|
| 16 |
+
'Alcohol': 'Alcohol',
|
| 17 |
+
'Family History': 'Family History',
|
| 18 |
+
'LengthOfStay': 'Length of Stay',
|
| 19 |
+
'Oxygen Saturation': 'Oxygen Saturation',
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
const FEATURE_DEFINITIONS = {
|
| 23 |
+
'Age': 'Your current age in years.',
|
| 24 |
+
'Glucose': 'Current blood sugar level (mg/dL).',
|
| 25 |
+
'HbA1c': 'Average blood sugar over the last 3 months (%).',
|
| 26 |
+
'BMI': 'Body Mass Index; a measure of body fat based on height and weight.',
|
| 27 |
+
'Cholesterol': 'Total amount of cholesterol in your blood (mg/dL).',
|
| 28 |
+
'Triglycerides': 'A type of fat found in your blood (mg/dL).',
|
| 29 |
+
'Blood Pressure': 'The force of your blood against artery walls (e.g., 120).',
|
| 30 |
+
'Physical Activity': 'Total hours of exercise or active movement per week.',
|
| 31 |
+
'Sleep Hours': 'Average hours of sleep you get per 24-hour period.',
|
| 32 |
+
'Stress Level': '1-3 (Low): Calm, in control. 4-6 (Moderate): Busy, pressured. 7-8 (High): Anxious, overwhelmed. 9-10 (Very High): Exhausted, unable to cope.',
|
| 33 |
+
'Diet Score': 'Self-rating of how healthy your meals are (1-10).',
|
| 34 |
+
'Smoking': 'Whether you currently smoke or have a history of smoking.',
|
| 35 |
+
'Alcohol': 'Your frequency of alcohol consumption.',
|
| 36 |
+
'Family History': 'Whether close relatives have had chronic conditions like heart disease or diabetes.',
|
| 37 |
+
'LengthOfStay': 'Total days spent in a hospital during your last visit.',
|
| 38 |
+
'Oxygen Saturation': 'The percentage of oxygen in your blood (SpO2).',
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
const DEFAULT_FEATURES = [
|
| 42 |
+
'Age', 'Glucose', 'HbA1c', 'BMI',
|
| 43 |
+
'Cholesterol', 'Triglycerides', 'Blood Pressure', 'Physical Activity',
|
| 44 |
+
'Sleep Hours', 'Stress Level', 'Diet Score', 'Smoking',
|
| 45 |
+
'Alcohol', 'Family History', 'LengthOfStay', 'Oxygen Saturation'
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
const FeatureSidebar = ({ features }) => {
|
| 49 |
+
const collectedCount = DEFAULT_FEATURES.filter(
|
| 50 |
+
feature => features[feature] !== null && features[feature] !== undefined
|
| 51 |
+
).length
|
| 52 |
+
|
| 53 |
+
const progressPercent = (collectedCount / 16) * 100
|
| 54 |
+
|
| 55 |
+
return (
|
| 56 |
+
<div className="w-72 bg-gray-50 border-r border-gray-200 flex flex-col h-full">
|
| 57 |
+
{/* Header */}
|
| 58 |
+
<div className="p-4 border-b border-gray-200">
|
| 59 |
+
<h2 className="text-sm font-semibold text-gray-900 mb-3">Health Metrics</h2>
|
| 60 |
+
|
| 61 |
+
{/* Progress bar */}
|
| 62 |
+
<div className="mb-2">
|
| 63 |
+
<div className="flex justify-between mb-1">
|
| 64 |
+
<span className="text-xs text-gray-600">{collectedCount}/16</span>
|
| 65 |
+
<span className="text-xs text-gray-600">{Math.round(progressPercent)}%</span>
|
| 66 |
+
</div>
|
| 67 |
+
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
|
| 68 |
+
<div
|
| 69 |
+
className="h-full bg-purple-600 transition-all duration-300"
|
| 70 |
+
style={{ width: `${progressPercent}%` }}
|
| 71 |
+
/>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
{/* Features list */}
|
| 77 |
+
<div className="flex-1 overflow-y-auto px-4 py-3">
|
| 78 |
+
<div className="space-y-2">
|
| 79 |
+
{DEFAULT_FEATURES.map(feature => {
|
| 80 |
+
const isCollected = features[feature] !== null && features[feature] !== undefined
|
| 81 |
+
const displayLabel = FEATURE_LABELS[feature] || feature
|
| 82 |
+
const definition = FEATURE_DEFINITIONS[feature] || 'No description available.'
|
| 83 |
+
|
| 84 |
+
return (
|
| 85 |
+
<MetricItem
|
| 86 |
+
key={feature}
|
| 87 |
+
feature={feature}
|
| 88 |
+
displayLabel={displayLabel}
|
| 89 |
+
definition={definition}
|
| 90 |
+
isCollected={isCollected}
|
| 91 |
+
/>
|
| 92 |
+
)
|
| 93 |
+
})}
|
| 94 |
+
</div>
|
| 95 |
+
|
| 96 |
+
{/* Help text at bottom */}
|
| 97 |
+
<div className="mt-4 pt-3 border-t border-gray-200 text-xs text-gray-500">
|
| 98 |
+
<p>π‘ <strong>Tip:</strong> Hover over or click the <span className="text-gray-600">β</span> icon next to each metric to learn what it measures.</p>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
)
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
export default FeatureSidebar
|
frontend/src/components/FeatureTooltip.jsx
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useRef, useEffect } from 'react'
|
| 2 |
+
import { Info, X } from 'lucide-react'
|
| 3 |
+
|
| 4 |
+
const FeatureTooltip = ({ feature, definition }) => {
|
| 5 |
+
const [isOpen, setIsOpen] = useState(false)
|
| 6 |
+
const [position, setPosition] = useState({ top: 0, left: 0 })
|
| 7 |
+
const iconRef = useRef(null)
|
| 8 |
+
const tooltipRef = useRef(null)
|
| 9 |
+
|
| 10 |
+
// Handle tooltip positioning to stay visible
|
| 11 |
+
useEffect(() => {
|
| 12 |
+
if (!isOpen || !iconRef.current || !tooltipRef.current) return
|
| 13 |
+
|
| 14 |
+
const calculatePosition = () => {
|
| 15 |
+
const iconRect = iconRef.current.getBoundingClientRect()
|
| 16 |
+
const tooltipRect = tooltipRef.current.getBoundingClientRect()
|
| 17 |
+
|
| 18 |
+
let top = iconRect.top - 8
|
| 19 |
+
let left = iconRect.right + 12
|
| 20 |
+
|
| 21 |
+
// Adjust if tooltip goes off-screen to the right
|
| 22 |
+
if (left + tooltipRect.width > window.innerWidth - 10) {
|
| 23 |
+
left = iconRect.left - tooltipRect.width - 12
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// Adjust if tooltip goes off-screen at the bottom
|
| 27 |
+
if (top + tooltipRect.height > window.innerHeight - 10) {
|
| 28 |
+
top = window.innerHeight - tooltipRect.height - 10
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// Adjust if tooltip goes off-screen at the top
|
| 32 |
+
if (top < 10) {
|
| 33 |
+
top = 10
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
setPosition({ top, left })
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
calculatePosition()
|
| 40 |
+
|
| 41 |
+
// Recalculate on window resize
|
| 42 |
+
window.addEventListener('resize', calculatePosition)
|
| 43 |
+
return () => window.removeEventListener('resize', calculatePosition)
|
| 44 |
+
}, [isOpen])
|
| 45 |
+
|
| 46 |
+
// Close tooltip when clicking outside
|
| 47 |
+
useEffect(() => {
|
| 48 |
+
if (!isOpen) return
|
| 49 |
+
|
| 50 |
+
const handleClickOutside = (e) => {
|
| 51 |
+
if (
|
| 52 |
+
tooltipRef.current &&
|
| 53 |
+
!tooltipRef.current.contains(e.target) &&
|
| 54 |
+
iconRef.current &&
|
| 55 |
+
!iconRef.current.contains(e.target)
|
| 56 |
+
) {
|
| 57 |
+
setIsOpen(false)
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
document.addEventListener('mousedown', handleClickOutside)
|
| 62 |
+
return () => document.removeEventListener('mousedown', handleClickOutside)
|
| 63 |
+
}, [isOpen])
|
| 64 |
+
|
| 65 |
+
const handleIconClick = (e) => {
|
| 66 |
+
e.stopPropagation()
|
| 67 |
+
setIsOpen(!isOpen)
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
const handleIconHover = (open) => {
|
| 71 |
+
// Only auto-close on hover leave for desktop, not on click
|
| 72 |
+
if (open) {
|
| 73 |
+
setIsOpen(true)
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
return (
|
| 78 |
+
<div className="relative inline-block">
|
| 79 |
+
{/* Info Icon */}
|
| 80 |
+
<button
|
| 81 |
+
ref={iconRef}
|
| 82 |
+
onClick={handleIconClick}
|
| 83 |
+
onMouseEnter={() => handleIconHover(true)}
|
| 84 |
+
onMouseLeave={() => {
|
| 85 |
+
// Auto-close on hover only if opened via hover (not click)
|
| 86 |
+
if (!isOpen) return
|
| 87 |
+
// Check if it was opened by click or hover
|
| 88 |
+
const isClickOpened = isOpen && true
|
| 89 |
+
// Keep it open if clicked, close if just hovered
|
| 90 |
+
}}
|
| 91 |
+
className="p-1 rounded-full text-gray-400 hover:text-gray-600 hover:bg-gray-100 transition-all duration-150 cursor-help"
|
| 92 |
+
title="Click or hover for definition"
|
| 93 |
+
aria-label={`Information about ${feature}`}
|
| 94 |
+
>
|
| 95 |
+
<Info size={14} className="flex-shrink-0" />
|
| 96 |
+
</button>
|
| 97 |
+
|
| 98 |
+
{/* Tooltip */}
|
| 99 |
+
{isOpen && (
|
| 100 |
+
<div
|
| 101 |
+
ref={tooltipRef}
|
| 102 |
+
className="fixed max-w-xs bg-slate-800 text-white rounded-lg shadow-xl p-3 text-xs leading-relaxed z-50 border border-slate-700 animate-in fade-in duration-200"
|
| 103 |
+
style={{
|
| 104 |
+
top: `${position.top}px`,
|
| 105 |
+
left: `${position.left}px`,
|
| 106 |
+
}}
|
| 107 |
+
>
|
| 108 |
+
{/* Header with feature name and close button */}
|
| 109 |
+
<div className="flex items-start justify-between gap-2 mb-2">
|
| 110 |
+
<span className="font-semibold text-slate-200">{feature}</span>
|
| 111 |
+
<button
|
| 112 |
+
onClick={() => setIsOpen(false)}
|
| 113 |
+
className="flex-shrink-0 text-slate-400 hover:text-slate-200 transition-colors"
|
| 114 |
+
aria-label="Close tooltip"
|
| 115 |
+
>
|
| 116 |
+
<X size={14} />
|
| 117 |
+
</button>
|
| 118 |
+
</div>
|
| 119 |
+
|
| 120 |
+
{/* Definition */}
|
| 121 |
+
<p className="text-slate-100 leading-relaxed">{definition}</p>
|
| 122 |
+
|
| 123 |
+
{/* Arrow pointer (subtle) */}
|
| 124 |
+
<div className="absolute w-2 h-2 bg-slate-800 border-r border-t border-slate-700 transform -translate-x-1 -top-1 left-4 rotate-45" />
|
| 125 |
+
</div>
|
| 126 |
+
)}
|
| 127 |
+
|
| 128 |
+
{/* Hint text - visible on hover/click for mobile users */}
|
| 129 |
+
{!isOpen && (
|
| 130 |
+
<div className="absolute bottom-full right-0 mb-1 text-xs text-gray-400 whitespace-nowrap pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity">
|
| 131 |
+
<span className="text-gray-400 text-[10px]">Click for info</span>
|
| 132 |
+
</div>
|
| 133 |
+
)}
|
| 134 |
+
</div>
|
| 135 |
+
)
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
export default FeatureTooltip
|
frontend/src/components/InputBar.jsx
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useRef, useEffect } from 'react'
|
| 2 |
+
import { Send } from 'lucide-react'
|
| 3 |
+
|
| 4 |
+
const InputBar = ({ onSend, onReset, disabled }) => {
|
| 5 |
+
const [input, setInput] = useState('')
|
| 6 |
+
const textareaRef = useRef(null)
|
| 7 |
+
|
| 8 |
+
// Auto-resize textarea
|
| 9 |
+
useEffect(() => {
|
| 10 |
+
if (textareaRef.current) {
|
| 11 |
+
textareaRef.current.style.height = 'auto'
|
| 12 |
+
textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 120) + 'px'
|
| 13 |
+
}
|
| 14 |
+
}, [input])
|
| 15 |
+
|
| 16 |
+
const handleSend = async () => {
|
| 17 |
+
if (input.trim() && !disabled) {
|
| 18 |
+
await onSend(input)
|
| 19 |
+
setInput('')
|
| 20 |
+
if (textareaRef.current) {
|
| 21 |
+
textareaRef.current.style.height = 'auto'
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
const handleKeyDown = (e) => {
|
| 27 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 28 |
+
e.preventDefault()
|
| 29 |
+
handleSend()
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
return (
|
| 34 |
+
<div className="border-t border-gray-200 bg-white px-6 py-4 shadow-lg">
|
| 35 |
+
<div className="max-w-2xl mx-auto">
|
| 36 |
+
<div className="flex gap-3 items-end">
|
| 37 |
+
{/* New button */}
|
| 38 |
+
<button
|
| 39 |
+
onClick={onReset}
|
| 40 |
+
disabled={disabled}
|
| 41 |
+
className="px-4 py-2 rounded-lg border border-gray-300 text-gray-600 hover:bg-gray-50 hover:border-gray-400 text-sm font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
| 42 |
+
title="Start a new assessment"
|
| 43 |
+
>
|
| 44 |
+
New
|
| 45 |
+
</button>
|
| 46 |
+
|
| 47 |
+
{/* Input box */}
|
| 48 |
+
<div className="flex-1 border border-gray-300 rounded-xl px-4 py-2 bg-white shadow-sm hover:shadow-md focus-within:shadow-md focus-within:border-purple-400 transition-all duration-200">
|
| 49 |
+
<textarea
|
| 50 |
+
ref={textareaRef}
|
| 51 |
+
value={input}
|
| 52 |
+
onChange={(e) => setInput(e.target.value)}
|
| 53 |
+
onKeyDown={handleKeyDown}
|
| 54 |
+
placeholder="Tell me about your health..."
|
| 55 |
+
disabled={disabled}
|
| 56 |
+
className="w-full resize-none outline-none text-sm border-none p-0 bg-transparent text-gray-900 placeholder-gray-400 disabled:bg-gray-50 disabled:cursor-not-allowed"
|
| 57 |
+
rows="1"
|
| 58 |
+
/>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
{/* Send button */}
|
| 62 |
+
<button
|
| 63 |
+
onClick={handleSend}
|
| 64 |
+
disabled={disabled || !input.trim()}
|
| 65 |
+
className="px-4 py-2 rounded-lg bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 shadow-sm hover:shadow-md"
|
| 66 |
+
title="Send message (Enter)"
|
| 67 |
+
>
|
| 68 |
+
<Send size={16} />
|
| 69 |
+
</button>
|
| 70 |
+
</div>
|
| 71 |
+
<p className="text-xs text-gray-400 mt-2">
|
| 72 |
+
π‘ Tip: Press Shift+Enter for new line
|
| 73 |
+
</p>
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
)
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
export default InputBar
|
frontend/src/components/MessageBubble.jsx
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const MessageBubble = ({ role, content }) => {
|
| 2 |
+
if (role === 'user') {
|
| 3 |
+
return (
|
| 4 |
+
<div className="flex justify-end mb-4">
|
| 5 |
+
<div className="bg-white border border-gray-200 rounded-xl shadow-sm px-4 py-3 max-w-prose hover:shadow-md transition-shadow">
|
| 6 |
+
<p className="text-gray-900 text-sm leading-relaxed whitespace-pre-wrap break-words">
|
| 7 |
+
{content}
|
| 8 |
+
</p>
|
| 9 |
+
</div>
|
| 10 |
+
</div>
|
| 11 |
+
)
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
return (
|
| 15 |
+
<div className="flex justify-start mb-4">
|
| 16 |
+
<div className="bg-[#FFD1DC] rounded-xl px-4 py-3 max-w-prose shadow-sm">
|
| 17 |
+
<p className="text-gray-900 text-sm leading-relaxed whitespace-pre-wrap break-words">
|
| 18 |
+
{content}
|
| 19 |
+
</p>
|
| 20 |
+
</div>
|
| 21 |
+
</div>
|
| 22 |
+
)
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export default MessageBubble
|
frontend/src/components/MetricItem.jsx
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useRef, useEffect } from 'react'
|
| 2 |
+
import { createPortal } from 'react-dom'
|
| 3 |
+
import { CheckCircle, Circle, Info, X } from 'lucide-react'
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* MetricItem Component
|
| 7 |
+
*
|
| 8 |
+
* Handles individual health metric with isolated hover state
|
| 9 |
+
* Uses React Portal to render popover outside the scrollable sidebar
|
| 10 |
+
* Ensures popover floats over the chat area without being clipped
|
| 11 |
+
*/
|
| 12 |
+
const MetricItem = ({ feature, displayLabel, definition, isCollected }) => {
|
| 13 |
+
const [showHint, setShowHint] = useState(false)
|
| 14 |
+
const [popoverPosition, setPopoverPosition] = useState({ top: 0, left: 0 })
|
| 15 |
+
const iconRef = useRef(null)
|
| 16 |
+
|
| 17 |
+
// Calculate popover position based on icon location
|
| 18 |
+
useEffect(() => {
|
| 19 |
+
if (!showHint || !iconRef.current) return
|
| 20 |
+
|
| 21 |
+
const iconRect = iconRef.current.getBoundingClientRect()
|
| 22 |
+
|
| 23 |
+
// Position popover to the right of the icon, aligned with top
|
| 24 |
+
// Add some spacing (16px) to the right
|
| 25 |
+
const left = iconRect.right + 16
|
| 26 |
+
const top = iconRect.top - 8 // Slight vertical centering
|
| 27 |
+
|
| 28 |
+
setPopoverPosition({ top, left })
|
| 29 |
+
}, [showHint])
|
| 30 |
+
|
| 31 |
+
// Handle mouse leaving the metric item
|
| 32 |
+
const handleMouseLeave = () => {
|
| 33 |
+
setShowHint(false)
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Handle mouse entering the metric item
|
| 37 |
+
const handleMouseEnter = () => {
|
| 38 |
+
setShowHint(true)
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// Close hint when clicking X button
|
| 42 |
+
const handleCloseHint = (e) => {
|
| 43 |
+
e.stopPropagation()
|
| 44 |
+
setShowHint(false)
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
return (
|
| 48 |
+
<>
|
| 49 |
+
{/* Metric Item Row */}
|
| 50 |
+
<div
|
| 51 |
+
className={`flex items-center justify-between gap-2 text-xs p-2 rounded transition-all duration-150 ${
|
| 52 |
+
isCollected
|
| 53 |
+
? 'text-green-700 bg-green-50 hover:bg-green-100'
|
| 54 |
+
: 'text-gray-400 hover:bg-gray-100'
|
| 55 |
+
}`}
|
| 56 |
+
onMouseEnter={handleMouseEnter}
|
| 57 |
+
onMouseLeave={handleMouseLeave}
|
| 58 |
+
>
|
| 59 |
+
{/* Status icon + Label */}
|
| 60 |
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
| 61 |
+
{isCollected ? (
|
| 62 |
+
<CheckCircle size={16} className="flex-shrink-0" />
|
| 63 |
+
) : (
|
| 64 |
+
<Circle size={16} className="flex-shrink-0" />
|
| 65 |
+
)}
|
| 66 |
+
<span className="truncate">{displayLabel}</span>
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
{/* Info Icon Button */}
|
| 70 |
+
<button
|
| 71 |
+
ref={iconRef}
|
| 72 |
+
onClick={() => setShowHint(!showHint)}
|
| 73 |
+
className="p-1 rounded-full text-gray-400 hover:text-gray-600 hover:bg-gray-200 transition-all duration-150 cursor-help flex-shrink-0"
|
| 74 |
+
title="Click for definition"
|
| 75 |
+
aria-label={`Information about ${displayLabel}`}
|
| 76 |
+
>
|
| 77 |
+
<Info size={14} />
|
| 78 |
+
</button>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
{/* Popover - Rendered via Portal (outside sidebar) with Glassmorphism */}
|
| 82 |
+
{showHint &&
|
| 83 |
+
createPortal(
|
| 84 |
+
<div
|
| 85 |
+
className="fixed z-9999 w-80 rounded-2xl shadow-lg p-4 text-xs leading-relaxed animate-in fade-in duration-200 pointer-events-auto backdrop-blur-md"
|
| 86 |
+
style={{
|
| 87 |
+
top: `${popoverPosition.top}px`,
|
| 88 |
+
left: `${popoverPosition.left}px`,
|
| 89 |
+
backgroundColor: 'rgba(220, 252, 231, 0.75)',
|
| 90 |
+
borderColor: 'rgba(134, 239, 172, 0.5)',
|
| 91 |
+
borderWidth: '1px',
|
| 92 |
+
WebkitBackdropFilter: 'blur(12px)',
|
| 93 |
+
}}
|
| 94 |
+
onMouseEnter={() => setShowHint(true)}
|
| 95 |
+
onMouseLeave={() => setShowHint(false)}
|
| 96 |
+
>
|
| 97 |
+
{/* Header with feature name and close button */}
|
| 98 |
+
<div className="flex items-start justify-between gap-3 mb-3">
|
| 99 |
+
<span className="font-semibold text-green-900 text-sm">{displayLabel}</span>
|
| 100 |
+
<button
|
| 101 |
+
onClick={handleCloseHint}
|
| 102 |
+
className="flex-shrink-0 text-green-700 hover:text-green-900 transition-colors p-0.5 hover:bg-green-200/40 rounded-lg"
|
| 103 |
+
aria-label="Close popover"
|
| 104 |
+
>
|
| 105 |
+
<X size={16} />
|
| 106 |
+
</button>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
{/* Definition text */}
|
| 110 |
+
<p className="text-green-900 leading-relaxed mb-2 font-medium">{definition}</p>
|
| 111 |
+
|
| 112 |
+
{/* Arrow pointer - points back to info icon (matches popover styling) */}
|
| 113 |
+
<div
|
| 114 |
+
className="absolute w-3 h-3 transform rotate-45"
|
| 115 |
+
style={{
|
| 116 |
+
right: '-6px',
|
| 117 |
+
top: `${8}px`,
|
| 118 |
+
backgroundColor: 'rgba(220, 252, 231, 0.85)',
|
| 119 |
+
borderTop: '1px solid rgba(134, 239, 172, 0.4)',
|
| 120 |
+
borderLeft: '1px solid rgba(134, 239, 172, 0.4)',
|
| 121 |
+
}}
|
| 122 |
+
/>
|
| 123 |
+
</div>,
|
| 124 |
+
document.body
|
| 125 |
+
)}
|
| 126 |
+
</>
|
| 127 |
+
)
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
export default MetricItem
|
frontend/src/hooks/useChat.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react'
|
| 2 |
+
import axios from 'axios'
|
| 3 |
+
|
| 4 |
+
const useChat = () => {
|
| 5 |
+
const [messages, setMessages] = useState([])
|
| 6 |
+
const [features, setFeatures] = useState({})
|
| 7 |
+
const [sessionId, setSessionId] = useState(null)
|
| 8 |
+
const [loading, setLoading] = useState(false)
|
| 9 |
+
const [prediction, setPrediction] = useState(null)
|
| 10 |
+
|
| 11 |
+
const sendMessage = async (text) => {
|
| 12 |
+
if (!text.trim()) return
|
| 13 |
+
|
| 14 |
+
// Add user message immediately
|
| 15 |
+
setMessages(prev => [...prev, { role: 'user', content: text }])
|
| 16 |
+
setLoading(true)
|
| 17 |
+
|
| 18 |
+
try {
|
| 19 |
+
// Call POST /api/chat
|
| 20 |
+
const res = await axios.post('/api/chat', {
|
| 21 |
+
session_id: sessionId,
|
| 22 |
+
message: text,
|
| 23 |
+
history: messages,
|
| 24 |
+
})
|
| 25 |
+
|
| 26 |
+
// Update state from response
|
| 27 |
+
setSessionId(res.data.session_id)
|
| 28 |
+
setFeatures(res.data.features)
|
| 29 |
+
setMessages(prev => [...prev, { role: 'assistant', content: res.data.message }])
|
| 30 |
+
|
| 31 |
+
// Store prediction if complete
|
| 32 |
+
if (res.data.is_complete && res.data.prediction) {
|
| 33 |
+
setPrediction(res.data.prediction)
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
return res.data
|
| 37 |
+
} catch (error) {
|
| 38 |
+
console.error('Error sending message:', error)
|
| 39 |
+
setMessages(prev => [...prev, {
|
| 40 |
+
role: 'assistant',
|
| 41 |
+
content: 'β Error: Could not process your message. Please try again.'
|
| 42 |
+
}])
|
| 43 |
+
} finally {
|
| 44 |
+
setLoading(false)
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
const resetChat = async () => {
|
| 49 |
+
if (!sessionId) {
|
| 50 |
+
// Reset UI without API call if no session yet
|
| 51 |
+
setMessages([])
|
| 52 |
+
setFeatures({})
|
| 53 |
+
setPrediction(null)
|
| 54 |
+
return
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
try {
|
| 58 |
+
await axios.post('/api/reset', { session_id: sessionId })
|
| 59 |
+
setMessages([])
|
| 60 |
+
setFeatures({})
|
| 61 |
+
setSessionId(null)
|
| 62 |
+
setPrediction(null)
|
| 63 |
+
} catch (error) {
|
| 64 |
+
console.error('Error resetting session:', error)
|
| 65 |
+
// Still reset UI even if API call fails
|
| 66 |
+
setMessages([])
|
| 67 |
+
setFeatures({})
|
| 68 |
+
setSessionId(null)
|
| 69 |
+
setPrediction(null)
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
return {
|
| 74 |
+
messages,
|
| 75 |
+
features,
|
| 76 |
+
loading,
|
| 77 |
+
sessionId,
|
| 78 |
+
prediction,
|
| 79 |
+
sendMessage,
|
| 80 |
+
resetChat,
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
export default useChat
|
frontend/src/index.css
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
* {
|
| 6 |
+
margin: 0;
|
| 7 |
+
padding: 0;
|
| 8 |
+
box-sizing: border-box;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
body {
|
| 12 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
| 13 |
+
background: #ffffff;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
html, body, #root {
|
| 17 |
+
height: 100%;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/* Scrollbar styling */
|
| 21 |
+
::-webkit-scrollbar {
|
| 22 |
+
width: 8px;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
::-webkit-scrollbar-track {
|
| 26 |
+
background: #f1f1f1;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
::-webkit-scrollbar-thumb {
|
| 30 |
+
background: #cbd5e1;
|
| 31 |
+
border-radius: 4px;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
::-webkit-scrollbar-thumb:hover {
|
| 35 |
+
background: #94a3b8;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
/* Custom z-index for popovers - ensures they float above everything */
|
| 39 |
+
.z-9999 {
|
| 40 |
+
z-index: 9999;
|
| 41 |
+
}
|
frontend/src/main.jsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
import ReactDOM from 'react-dom/client'
|
| 3 |
+
import App from './App.jsx'
|
| 4 |
+
import './index.css'
|
| 5 |
+
|
| 6 |
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
| 7 |
+
<React.StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</React.StrictMode>,
|
| 10 |
+
)
|
frontend/tailwind.config.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
export default {
|
| 3 |
+
content: [
|
| 4 |
+
"./index.html",
|
| 5 |
+
"./src/**/*.{js,jsx}",
|
| 6 |
+
],
|
| 7 |
+
theme: {
|
| 8 |
+
extend: {
|
| 9 |
+
colors: {
|
| 10 |
+
purple: {
|
| 11 |
+
600: '#9333ea',
|
| 12 |
+
700: '#7e22ce',
|
| 13 |
+
},
|
| 14 |
+
},
|
| 15 |
+
},
|
| 16 |
+
},
|
| 17 |
+
plugins: [],
|
| 18 |
+
}
|
frontend/vite.config.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
|
| 4 |
+
export default defineConfig({
|
| 5 |
+
plugins: [react()],
|
| 6 |
+
server: {
|
| 7 |
+
proxy: {
|
| 8 |
+
'/api': {
|
| 9 |
+
target: 'http://localhost:8000',
|
| 10 |
+
changeOrigin: true,
|
| 11 |
+
},
|
| 12 |
+
},
|
| 13 |
+
},
|
| 14 |
+
})
|
models/GradientBoosting_model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:394bcda0ead5e154434e56d53bc1f8536585c9d5a69db4a93345e2413daf99b6
|
| 3 |
+
size 2062313
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.44.1
|
| 2 |
+
groq
|
| 3 |
+
python-dotenv
|
| 4 |
+
pydantic
|
| 5 |
+
scikit-learn
|
| 6 |
+
joblib
|
| 7 |
+
numpy
|
| 8 |
+
huggingface_hub>=0.23.4
|
| 9 |
+
fastapi>=0.100.0
|
| 10 |
+
uvicorn[standard]>=0.23.0
|
| 11 |
+
python-multipart>=0.0.6
|
server.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
FastAPI server entry point for Medical Diagnosis AI
|
| 4 |
+
|
| 5 |
+
Local Development:
|
| 6 |
+
python server.py
|
| 7 |
+
Server will start on http://localhost:8000
|
| 8 |
+
API docs: http://localhost:8000/docs
|
| 9 |
+
|
| 10 |
+
Hugging Face Spaces / Production:
|
| 11 |
+
python server.py
|
| 12 |
+
Server will start on http://0.0.0.0:7860
|
| 13 |
+
Serves both API and static React frontend
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import uvicorn
|
| 17 |
+
import logging
|
| 18 |
+
import os
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from fastapi.staticfiles import StaticFiles
|
| 21 |
+
|
| 22 |
+
# Setup logging
|
| 23 |
+
logging.basicConfig(
|
| 24 |
+
level=logging.INFO,
|
| 25 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 26 |
+
)
|
| 27 |
+
logger = logging.getLogger(__name__)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def get_server_config():
|
| 31 |
+
"""Determine server configuration based on environment"""
|
| 32 |
+
# Check if running in Hugging Face Spaces
|
| 33 |
+
in_space = os.getenv("SPACE_ID") is not None
|
| 34 |
+
|
| 35 |
+
host = "0.0.0.0"
|
| 36 |
+
port = 7860 if in_space else 8000
|
| 37 |
+
reload = not in_space # Disable reload in production (HF Spaces)
|
| 38 |
+
|
| 39 |
+
return host, port, reload, in_space
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
host, port, reload, in_space = get_server_config()
|
| 44 |
+
|
| 45 |
+
# Import FastAPI app AFTER configuration
|
| 46 |
+
from app.api import app
|
| 47 |
+
|
| 48 |
+
# Configure static file serving for the React frontend
|
| 49 |
+
frontend_dist = Path(__file__).parent / "frontend" / "dist"
|
| 50 |
+
|
| 51 |
+
if frontend_dist.exists():
|
| 52 |
+
logger.info(f"π Mounting static files from: {frontend_dist}")
|
| 53 |
+
# Mount static files at root, but BEFORE API routes are checked
|
| 54 |
+
# This way /api/* routes are handled by FastAPI, everything else by static files
|
| 55 |
+
app.mount("/", StaticFiles(directory=frontend_dist, html=True), name="static")
|
| 56 |
+
else:
|
| 57 |
+
logger.warning(f"β οΈ Frontend dist directory not found: {frontend_dist}")
|
| 58 |
+
logger.warning(" Run 'cd frontend && npm run build' to build the frontend")
|
| 59 |
+
|
| 60 |
+
# Log startup info
|
| 61 |
+
environment = "π Hugging Face Spaces" if in_space else "π» Local Development"
|
| 62 |
+
logger.info(f"Starting Medical Diagnosis AI Server ({environment})")
|
| 63 |
+
logger.info(f"π Server: http://{host}:{port}")
|
| 64 |
+
logger.info(f"π Frontend: http://{host}:{port}")
|
| 65 |
+
logger.info(f"π API Base: http://{host}:{port}/api")
|
| 66 |
+
logger.info(f"π API Docs: http://{host}:{port}/docs")
|
| 67 |
+
|
| 68 |
+
uvicorn.run(
|
| 69 |
+
"app.api:app",
|
| 70 |
+
host=host,
|
| 71 |
+
port=port,
|
| 72 |
+
reload=reload,
|
| 73 |
+
log_level="info"
|
| 74 |
+
)
|