Himanshu Gangwar commited on
Commit
eff8aa5
·
1 Parent(s): 85c810b

initial commit

Browse files
.env.example ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment Variables for InsightPilot
2
+
3
+ # Required: Groq API Key for LLM access
4
+ # Get your key from: https://console.groq.com/
5
+ GROQ_API_KEY=your_groq_api_key_here
6
+
7
+ # Optional: Database URL (defaults to SQLite)
8
+ # For SQLite (default): sqlite:///./test.db
9
+ # For PostgreSQL: postgresql://user:password@host:port/dbname
10
+ DATABASE_URL=sqlite:///./test.db
11
+
12
+ # Optional: Configure other settings
13
+ # DEBUG=false
14
+ # LOG_LEVEL=info
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ __pycache__/
2
+ .venv/
3
+ backend/.env
4
+ backend/backend/static
HF_DEPLOYMENT_CHECKLIST.md ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 HF Spaces Deployment Checklist
2
+
3
+ Use this checklist to ensure smooth deployment to Hugging Face Spaces.
4
+
5
+ ## Pre-Deployment
6
+
7
+ - [ ] **Get Groq API Key**
8
+ - Sign up at https://console.groq.com/
9
+ - Generate API key
10
+ - Keep it safe for Step 5
11
+
12
+ - [ ] **Create HF Account**
13
+ - Sign up at https://huggingface.co/join
14
+ - Verify email
15
+
16
+ - [ ] **Build Frontend (Optional)**
17
+ - Windows: Run `build_frontend.bat`
18
+ - Mac/Linux: Run `./build_frontend.sh`
19
+ - Verify `frontend/dist/` folder exists
20
+
21
+ ## Deployment Steps
22
+
23
+ ### 1. Create New Space
24
+ - [ ] Go to https://huggingface.co/new-space
25
+ - [ ] Set Space name (e.g., `insightpilot`)
26
+ - [ ] Choose SDK: **Gradio**
27
+ - [ ] Choose License: **MIT**
28
+ - [ ] Choose Hardware: **CPU basic** (free)
29
+ - [ ] Set Visibility: Public or Private
30
+ - [ ] Click **Create Space**
31
+
32
+ ### 2. Prepare Files
33
+ - [ ] Copy/create these files:
34
+ ```
35
+ ✅ app.py
36
+ ✅ requirements.txt
37
+ ✅ README_HF.md → rename to README.md
38
+ ✅ backend/ (entire folder)
39
+ ✅ data/ (optional - sample data)
40
+ ✅ frontend/dist/ (optional - if built)
41
+ ```
42
+
43
+ ### 3. Upload to Space
44
+
45
+ **Option A: Git (Recommended)**
46
+ - [ ] Clone your Space:
47
+ ```bash
48
+ git clone https://huggingface.co/spaces/YOUR_USERNAME/SPACE_NAME
49
+ cd SPACE_NAME
50
+ ```
51
+ - [ ] Copy files into the cloned directory
52
+ - [ ] Add files:
53
+ ```bash
54
+ git add .
55
+ git commit -m "Initial deployment of InsightPilot"
56
+ git push
57
+ ```
58
+
59
+ **Option B: Web Upload**
60
+ - [ ] Go to your Space page
61
+ - [ ] Click **Files** → **Add file** → **Upload files**
62
+ - [ ] Drag and drop all files
63
+ - [ ] Commit changes
64
+
65
+ ### 4. Configure Space
66
+
67
+ - [ ] Go to **Settings** → **Repository secrets**
68
+ - [ ] Click **New secret**
69
+ - [ ] Add:
70
+ - Name: `GROQ_API_KEY`
71
+ - Value: Your Groq API key
72
+ - [ ] Click **Add**
73
+
74
+ ### 5. Monitor Build
75
+
76
+ - [ ] Go to **Logs** tab
77
+ - [ ] Wait for build to complete (3-5 minutes)
78
+ - [ ] Check for errors
79
+ - [ ] Wait for "Running" status
80
+
81
+ ### 6. Test Deployment
82
+
83
+ - [ ] Visit your Space URL
84
+ - [ ] Test **API Access** tab:
85
+ - [ ] Enter a question
86
+ - [ ] Click Analyze
87
+ - [ ] Verify response
88
+
89
+ - [ ] Test **Upload Dataset** tab:
90
+ - [ ] Upload a CSV
91
+ - [ ] Verify success message
92
+
93
+ - [ ] Test **Analytics Dashboard** (if frontend built):
94
+ - [ ] Check UI loads
95
+ - [ ] Try sample questions
96
+ - [ ] Verify visualizations
97
+ - [ ] Download PDF report
98
+
99
+ ## Post-Deployment
100
+
101
+ ### Optional Enhancements
102
+
103
+ - [ ] **Add README badge** to your GitHub repo:
104
+ ```markdown
105
+ [![Hugging Face Spaces](https://img.shields.io/badge/🤗%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces/YOUR_USERNAME/SPACE_NAME)
106
+ ```
107
+
108
+ - [ ] **Enable Analytics**
109
+ - Settings → Enable analytics
110
+ - Track visitor count
111
+
112
+ - [ ] **Customize Space**
113
+ - Edit README.md for better description
114
+ - Add screenshots
115
+ - Update emoji/colors in metadata
116
+
117
+ - [ ] **Set up monitoring**
118
+ - Check Logs regularly
119
+ - Monitor Usage in Settings
120
+
121
+ ### Troubleshooting Common Issues
122
+
123
+ - [ ] **Build failed**: Check `requirements.txt` syntax
124
+ - [ ] **Import errors**: Verify all backend files uploaded
125
+ - [ ] **GROQ_API_KEY error**: Check secret name matches exactly
126
+ - [ ] **Frontend not loading**: Verify `frontend/dist/` uploaded
127
+ - [ ] **Port errors**: App uses 7860 (HF default)
128
+ - [ ] **Database errors**: Check file paths in backend
129
+
130
+ ## Success Indicators
131
+
132
+ ✅ Space shows "Running" status
133
+ ✅ No errors in Logs
134
+ ✅ Can ask questions and get responses
135
+ ✅ Can upload CSV files
136
+ ✅ Can download PDF reports
137
+ ✅ Visualizations appear correctly
138
+
139
+ ## Share Your Space
140
+
141
+ Once deployed, share at:
142
+ ```
143
+ https://huggingface.co/spaces/YOUR_USERNAME/SPACE_NAME
144
+ ```
145
+
146
+ Add to:
147
+ - [ ] GitHub README
148
+ - [ ] LinkedIn
149
+ - [ ] Twitter
150
+ - [ ] Portfolio
151
+
152
+ ---
153
+
154
+ **🎉 Congratulations on deploying InsightPilot to Hugging Face Spaces!**
155
+
156
+ Need help? See `DEPLOYMENT.md` for detailed instructions.
README.md CHANGED
@@ -1,13 +1,114 @@
1
- ---
2
- title: Insightpilot
3
- emoji: 🚀
4
- colorFrom: indigo
5
- colorTo: red
6
- sdk: gradio
7
- sdk_version: 6.0.0
8
- app_file: app.py
9
- pinned: false
10
- license: mit
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # InsightPilot – Autonomous Analytics Agent
2
+
3
+ InsightPilot is a production-style AI analyst that turns natural-language business questions into validated SQL, visualizations, insights, and executive-ready PDF reports. The stack combines LangGraph (deterministic agent workflow), FastAPI, SQLAlchemy, ReportLab, and a modern React dashboard.
4
+
5
+ ## Project Highlights
6
+
7
+ - **Agentic LangGraph pipeline** – deterministic tool-calling chain (intent → schema → NL2SQL → execution → diagnostics → visualization → PDF) keeps results auditable.
8
+ - **Advanced analytics automation** – dedicated trend and anomaly modules mine every result set and stream context-aware summaries to the UI and PDF.
9
+ - **ReportLab title-page exports** – branded title page, SQL appendix, visuals, and analytics sections ready for exec briefings.
10
+ - **Draw.io workflow assets** – editable diagram (`docs/agent-workflow.drawio`) plus lightweight SVG for README embeds.
11
+ - **Reusable data ingestion** – dataset catalog + multi-table CSV upload lets you retarget the agent to any warehouse quickly.
12
+ - **Groq Llama‑3 tooling** – low-latency NL→SQL and narrative insight generation.
13
+
14
+ ## Workflow Diagram (Draw.io)
15
+
16
+ ![Agent Workflow](docs/agent-workflow.svg)
17
+
18
+ > Edit the source diagram at `docs/agent-workflow.drawio` in Draw.io/Lucidchart and export a fresh SVG/PNG if the workflow changes.
19
+
20
+ ## Advanced Analytics Modules
21
+
22
+ | Module | What it does | Output surfaces |
23
+ | --- | --- | --- |
24
+ | **Trend detection** | Converts the query result into a monthly time series, fits a regression, and quantifies direction, slope, and % change. | Chat insights, Latest Analysis cards, PDF “Trend Diagnostics”. |
25
+ | **Anomaly detection** | Computes z-scores across the same period and flags statistically significant outliers. | Latest Analysis cards, PDF “Anomaly Highlights”. |
26
+
27
+ The insight LLM receives both diagnostics, so follow-up prompts remain grounded in prior conclusions.
28
+
29
+ ## Architecture
30
+
31
+ | Layer | Tech | Notes |
32
+ | --- | --- | --- |
33
+ | API & Orchestration | FastAPI + LangGraph | Defines graph nodes for schema, NL2SQL, execution, viz, insight, and report building. |
34
+ | LLM | Groq `llama3-70b-8192` | Deterministic, low-latency NL2SQL + insight generation. |
35
+ | Data | SQLite via SQLAlchemy | Auto-generated `sales` dataset for local testing. |
36
+ | Visualization & Reports | Matplotlib, ReportLab/Platypus | Charts saved to `backend/static`, referenced in UI/PDF. |
37
+ | Frontend | React + Vite | Modern dashboard with prompt chips, chat, result stack, PDF download. |
38
+
39
+ ## Prerequisites
40
+
41
+ - Python 3.9+
42
+ - Node.js 16+
43
+ - Groq API Key (for NL2SQL + insight generation)
44
+
45
+ ## Backend Setup
46
+
47
+ ```bash
48
+ cd backend
49
+ python -m venv venv
50
+ source venv/bin/activate # Windows: venv\Scripts\activate
51
+ pip install -r requirements.txt
52
+ cp .env.example .env # add GROQ_API_KEY and optional DATABASE_URL
53
+ python create_db.py # optional; startup also seeds the sales table
54
+ python -m app.main # runs on http://localhost:8000
55
+ ```
56
+
57
+ `DATABASE_URL` defaults to `sqlite:///./test.db`. Ensure the `.env` file includes your `GROQ_API_KEY` for Groq access.
58
+
59
+ ## Frontend Setup
60
+
61
+ ```bash
62
+ cd frontend
63
+ npm install
64
+ npm run dev # http://localhost:5173
65
+ ```
66
+
67
+ ## Usage Flow
68
+
69
+ 1. Start backend and frontend dev servers.
70
+ 1. Upload CSVs via the “Dataset Control” card (or call `POST /api/upload-csv` with `file` + `table_name`). The agent now validates table names, swaps in new tables on demand, and refreshes the dataset catalog for quick reuse.
71
+ 1. Open the React app, pick a quick prompt (e.g., “What were total sales by category?”) or type your own.
72
+ 1. InsightPilot:
73
+
74
+ - Reads DB schema & crafts SQL via Groq Llama‑3.
75
+ - Executes SQL with SQLAlchemy/Pandas.
76
+ - Generates Matplotlib charts and Groq insight summaries.
77
+ - Streams insights to the chat, shows SQL/data preview, and produces a PDF.
78
+
79
+ 1. Download the PDF to share with stakeholders.
80
+
81
+ ## Dataset Upload & Multi-table Ingestion
82
+
83
+ - **Upload endpoint:** `POST /api/upload-csv` accepts `file` + `table_name`. Table names are validated (`[A-Za-z_][A-Za-z0-9_]*`) to keep SQL safe, and uploads replace/create tables atomically.
84
+ - **Catalog endpoint:** `GET /api/datasets` returns discovered tables along with row counts and column names so the frontend can surface a live catalog.
85
+ - **Frontend controls:** the Dataset Control card now includes a target-table input, catalog refresh button, and a list of available tables—making it trivial to pivot the agent to another fact table without redeploying anything.
86
+
87
+ ## Project Structure
88
+
89
+ ```text
90
+ backend/
91
+ app/
92
+ agents/graph.py # LangGraph workflow
93
+ api/routes.py # FastAPI routes
94
+ core/config.py # Settings + env
95
+ db/database.py # Engine + auto seed
96
+ services/analytics.py # Trend & anomaly detection modules
97
+ services/pdf_generator.py# ReportLab report builder
98
+ services/csv_loader.py # CSV ingestion + dataset catalog helpers
99
+ static/ # Generated charts & PDFs
100
+ create_db.py # Manual seed script
101
+ requirements.txt
102
+
103
+ frontend/
104
+ src/App.jsx, App.css # React dashboard
105
+ package.json
106
+ ```
107
+
108
+ ## Future Ideas
109
+
110
+ - Intent-specific LangGraph branches (comparison vs. forecasting).
111
+ - Supabase/Postgres adapters with connection pooling.
112
+ - Auth + team workspaces for insights and PDF history.
113
+
114
+ Enjoy exploring data with InsightPilot! 🚀
README_HF.md ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: InsightPilot - Autonomous Analytics Agent
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 4.16.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ python_version: 3.10
12
+ ---
13
+
14
+ # InsightPilot – Autonomous Analytics Agent
15
+
16
+ <div align="center">
17
+
18
+ [![Powered by LangGraph](https://img.shields.io/badge/Powered%20by-LangGraph-blue)](https://github.com/langchain-ai/langgraph)
19
+ [![FastAPI](https://img.shields.io/badge/FastAPI-0.109.0-green)](https://fastapi.tiangolo.com/)
20
+ [![Groq](https://img.shields.io/badge/LLM-Groq%20Llama--3-orange)](https://groq.com/)
21
+
22
+ </div>
23
+
24
+ InsightPilot is a production-ready AI analyst that transforms natural language questions into validated SQL queries, interactive visualizations, comprehensive insights, and executive-ready PDF reports.
25
+
26
+ ## 🌟 Features
27
+
28
+ - **🤖 Agentic LangGraph Pipeline** – Deterministic tool-calling workflow (intent → schema → NL2SQL → execution → diagnostics → visualization → PDF)
29
+ - **📊 Advanced Analytics** – Automated trend detection and anomaly analysis with statistical insights
30
+ - **📄 PDF Report Generation** – Executive-ready reports with branded title pages, charts, and SQL appendix
31
+ - **📁 Multi-table Support** – Easy CSV upload and dataset catalog management
32
+ - **⚡ Real-time Streaming** – Live insights streamed to the UI as they're generated
33
+ - **🔍 Groq Llama-3 Powered** – Low-latency NL→SQL and narrative insight generation
34
+
35
+ ## 🚀 Quick Start on Hugging Face Spaces
36
+
37
+ 1. **Set Environment Variables** (Required)
38
+ - Go to Settings → Repository Secrets
39
+ - Add `GROQ_API_KEY` with your Groq API key ([Get one here](https://console.groq.com/))
40
+
41
+ 2. **Upload Your Data** (Optional)
42
+ - Use the "Upload Dataset" tab to add your CSV files
43
+ - Or work with the pre-loaded sample sales dataset
44
+
45
+ 3. **Ask Questions**
46
+ - Use the Analytics Dashboard to ask natural language questions
47
+ - Example: "What were the total sales by category last quarter?"
48
+ - Get SQL, visualizations, insights, and downloadable PDF reports
49
+
50
+ ## 🏗️ Architecture
51
+
52
+ | Component | Technology | Purpose |
53
+ |-----------|-----------|---------|
54
+ | **LLM Orchestration** | LangGraph + Groq Llama-3 70B | Deterministic agent workflow with tool calling |
55
+ | **API & Backend** | FastAPI + SQLAlchemy | RESTful API, database management |
56
+ | **Analytics** | Pandas, NumPy, SciPy | Trend detection, anomaly analysis |
57
+ | **Visualization** | Matplotlib, ReportLab | Charts and PDF report generation |
58
+ | **Database** | SQLite | Lightweight, persistent data storage |
59
+ | **Frontend** | React + Vite (optional) | Modern interactive dashboard |
60
+ | **Interface** | Gradio | HF Spaces integration |
61
+
62
+ ## 📊 Advanced Analytics Modules
63
+
64
+ - **Trend Detection**: Time series regression analysis with slope quantification and % change metrics
65
+ - **Anomaly Detection**: Z-score based statistical outlier identification
66
+ - **Insight Generation**: Context-aware narrative summaries powered by Groq LLM
67
+
68
+ ## 🛠️ Tech Stack
69
+
70
+ ```
71
+ Backend: FastAPI + LangGraph + LangChain + Groq
72
+ Data: SQLite + SQLAlchemy + Pandas
73
+ Viz: Matplotlib + ReportLab/Platypus
74
+ Frontend: React + Vite (embedded in Gradio)
75
+ Deploy: Hugging Face Spaces (Gradio SDK)
76
+ ```
77
+
78
+ ## 📁 Project Structure
79
+
80
+ ```
81
+ .
82
+ ├── app.py # Gradio wrapper for HF Spaces
83
+ ├── requirements.txt # Python dependencies
84
+ ├── backend/
85
+ │ ├── app/
86
+ │ │ ├── main.py # FastAPI application
87
+ │ │ ├── agents/graph.py # LangGraph workflow
88
+ │ │ ├── api/routes.py # API endpoints
89
+ │ │ ├── core/config.py # Settings & environment
90
+ │ │ ├── db/database.py # Database engine & seeding
91
+ │ │ └── services/ # Analytics, PDF, CSV modules
92
+ │ ├── static/ # Generated charts & PDFs
93
+ │ └── requirements.txt # Backend-specific deps
94
+ ├── frontend/ # React dashboard (optional)
95
+ └── data/ # Sample datasets
96
+
97
+ ```
98
+
99
+ ## 🔑 Environment Variables
100
+
101
+ | Variable | Description | Required |
102
+ |----------|-------------|----------|
103
+ | `GROQ_API_KEY` | Groq API key for LLM access | ✅ Yes |
104
+ | `DATABASE_URL` | Database connection string | ⚪ Optional (defaults to SQLite) |
105
+
106
+ ## 📖 Usage Examples
107
+
108
+ **Question:** "What were the top 5 products by revenue last year?"
109
+
110
+ **InsightPilot will:**
111
+ 1. ✅ Analyze your database schema
112
+ 2. ✅ Generate optimized SQL query
113
+ 3. ✅ Execute query and validate results
114
+ 4. ✅ Create visualizations (bar charts, trends)
115
+ 5. ✅ Perform trend & anomaly analysis
116
+ 6. ✅ Generate narrative insights
117
+ 7. ✅ Build downloadable PDF report
118
+
119
+ ## 🎯 Use Cases
120
+
121
+ - **Business Analytics**: Ad-hoc reporting without SQL knowledge
122
+ - **Executive Briefings**: Automated PDF reports with insights
123
+ - **Data Exploration**: Quick analysis of uploaded CSV datasets
124
+ - **Trend Analysis**: Automated time-series analytics
125
+ - **Anomaly Detection**: Statistical outlier identification
126
+
127
+ ## 🚧 Limitations & Notes
128
+
129
+ - **Free HF Spaces**: CPU-only tier; suitable for moderate traffic
130
+ - **Database**: SQLite with persistent storage (50GB limit)
131
+ - **File Cleanup**: Old PDFs/charts should be periodically removed
132
+ - **Concurrent Users**: May need rate limiting for production use
133
+
134
+ ## 🔮 Future Enhancements
135
+
136
+ - Multi-tenant workspaces with authentication
137
+ - Postgres/Supabase adapter for production databases
138
+ - Real-time collaborative dashboards
139
+ - Forecast & prediction modules
140
+ - Custom visualization templates
141
+
142
+ ## 📝 License
143
+
144
+ MIT License - see LICENSE file for details
145
+
146
+ ## 🤝 Contributing
147
+
148
+ Contributions welcome! Please open an issue or submit a PR.
149
+
150
+ ## 🔗 Links
151
+
152
+ - **Repository**: [GitHub](https://github.com/zenitsu0509/InsightPilot)
153
+ - **Documentation**: See original README in repo
154
+ - **Groq Platform**: [Get API Key](https://console.groq.com/)
155
+
156
+ ---
157
+
158
+ **Built with ❤️ using LangGraph, FastAPI, and Groq**
app.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import sys
4
+ import uvicorn
5
+ from threading import Thread
6
+ import time
7
+ import shutil
8
+
9
+ # Add backend to path
10
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))
11
+
12
+ from app.main import app as fastapi_app
13
+
14
+ # Set up paths for HF Spaces
15
+ STATIC_DIR = "backend/static"
16
+ FRONTEND_BUILD_DIR = "frontend/dist"
17
+ DB_PATH = "test.db"
18
+
19
+ os.makedirs(STATIC_DIR, exist_ok=True)
20
+
21
+ def start_fastapi():
22
+ """Start FastAPI server in background thread"""
23
+ uvicorn.run(fastapi_app, host="0.0.0.0", port=7860, log_level="info")
24
+
25
+ # Start FastAPI in a separate thread
26
+ fastapi_thread = Thread(target=start_fastapi, daemon=True)
27
+ fastapi_thread.start()
28
+
29
+ # Give FastAPI time to start
30
+ time.sleep(2)
31
+
32
+ # Create Gradio interface
33
+ with gr.Blocks(title="InsightPilot - Autonomous Analytics Agent", theme=gr.themes.Soft()) as demo:
34
+ gr.Markdown("""
35
+ # 🚀 InsightPilot – Autonomous Analytics Agent
36
+
37
+ An AI-powered analyst that turns natural language questions into SQL queries, visualizations, and executive-ready PDF reports.
38
+
39
+ **Powered by:** LangGraph + FastAPI + Groq Llama-3
40
+ """)
41
+
42
+ with gr.Tab("📊 Analytics Dashboard"):
43
+ gr.Markdown("""
44
+ ### Access the full React dashboard below
45
+
46
+ The dashboard provides:
47
+ - Natural language query interface
48
+ - Real-time SQL generation and execution
49
+ - Interactive visualizations
50
+ - PDF report generation
51
+ - Dataset management and CSV upload
52
+ """)
53
+
54
+ # Embed the React app if built, otherwise show API info
55
+ if os.path.exists(FRONTEND_BUILD_DIR):
56
+ gr.HTML(f"""
57
+ <iframe src="/frontend" width="100%" height="800px" frameborder="0"></iframe>
58
+ """)
59
+ else:
60
+ gr.Markdown("""
61
+ ⚠️ **Frontend not built yet.**
62
+
63
+ To build the frontend:
64
+ ```bash
65
+ cd frontend
66
+ npm install
67
+ npm run build
68
+ ```
69
+
70
+ For now, you can interact with the API directly at the endpoints below.
71
+ """)
72
+
73
+ with gr.Tab("🔧 API Access"):
74
+ gr.Markdown("""
75
+ ### Direct API Endpoints
76
+
77
+ - **Base URL:** `http://localhost:7860/api`
78
+ - **Query Analysis:** `POST /api/analyze` - Send natural language questions
79
+ - **Upload CSV:** `POST /api/upload-csv` - Upload datasets
80
+ - **List Datasets:** `GET /api/datasets` - View available tables
81
+ - **API Docs:** [Interactive Swagger UI](http://localhost:7860/docs)
82
+
83
+ ### Quick Test
84
+ """)
85
+
86
+ with gr.Row():
87
+ question_input = gr.Textbox(
88
+ label="Ask a Question",
89
+ placeholder="What were the total sales by category?",
90
+ lines=2
91
+ )
92
+ submit_btn = gr.Button("Analyze", variant="primary")
93
+
94
+ result_output = gr.JSON(label="Analysis Result")
95
+
96
+ def analyze_question(question):
97
+ """Call the FastAPI analyze endpoint"""
98
+ import requests
99
+ try:
100
+ response = requests.post(
101
+ "http://localhost:7860/api/analyze",
102
+ json={"question": question},
103
+ timeout=60
104
+ )
105
+ return response.json()
106
+ except Exception as e:
107
+ return {"error": str(e)}
108
+
109
+ submit_btn.click(
110
+ fn=analyze_question,
111
+ inputs=[question_input],
112
+ outputs=[result_output]
113
+ )
114
+
115
+ with gr.Tab("📤 Upload Dataset"):
116
+ gr.Markdown("""
117
+ ### Upload CSV Dataset
118
+
119
+ Upload your own CSV files to analyze custom data.
120
+ """)
121
+
122
+ with gr.Row():
123
+ csv_file = gr.File(label="Select CSV File", file_types=[".csv"])
124
+ table_name = gr.Textbox(
125
+ label="Table Name",
126
+ placeholder="sales_data",
127
+ value="uploaded_data"
128
+ )
129
+
130
+ upload_btn = gr.Button("Upload", variant="primary")
131
+ upload_result = gr.Textbox(label="Upload Status", lines=3)
132
+
133
+ def upload_csv(file, table):
134
+ """Upload CSV to the database"""
135
+ import requests
136
+ try:
137
+ if file is None:
138
+ return "Please select a CSV file"
139
+
140
+ with open(file.name, 'rb') as f:
141
+ files = {'file': (os.path.basename(file.name), f, 'text/csv')}
142
+ data = {'table_name': table}
143
+ response = requests.post(
144
+ "http://localhost:7860/api/upload-csv",
145
+ files=files,
146
+ data=data,
147
+ timeout=30
148
+ )
149
+ if response.status_code == 200:
150
+ result = response.json()
151
+ return f"✅ Success! Uploaded {result.get('rows', 0)} rows to table '{table}'"
152
+ else:
153
+ return f"❌ Error: {response.text}"
154
+ except Exception as e:
155
+ return f"❌ Error: {str(e)}"
156
+
157
+ upload_btn.click(
158
+ fn=upload_csv,
159
+ inputs=[csv_file, table_name],
160
+ outputs=[upload_result]
161
+ )
162
+
163
+ with gr.Tab("ℹ️ About"):
164
+ gr.Markdown("""
165
+ ## About InsightPilot
166
+
167
+ InsightPilot is a production-style AI analyst that combines:
168
+
169
+ - **Agentic LangGraph Pipeline:** Deterministic tool-calling workflow
170
+ - **Advanced Analytics:** Automated trend detection and anomaly analysis
171
+ - **PDF Reports:** Executive-ready reports with visualizations
172
+ - **Multi-table Support:** Easy CSV upload and dataset management
173
+
174
+ ### Tech Stack
175
+ - **Backend:** FastAPI + LangGraph + SQLAlchemy
176
+ - **LLM:** Groq Llama-3 (70B)
177
+ - **Frontend:** React + Vite
178
+ - **Database:** SQLite
179
+ - **Visualization:** Matplotlib + ReportLab
180
+
181
+ ### Environment Variables
182
+ Make sure to set your `GROQ_API_KEY` in the Hugging Face Space secrets!
183
+
184
+ ### Repository
185
+ [View on GitHub](https://github.com/zenitsu0509/InsightPilot)
186
+ """)
187
+
188
+ # Mount frontend if built
189
+ if os.path.exists(FRONTEND_BUILD_DIR):
190
+ from fastapi.staticfiles import StaticFiles
191
+ fastapi_app.mount("/frontend", StaticFiles(directory=FRONTEND_BUILD_DIR, html=True), name="frontend")
192
+
193
+ if __name__ == "__main__":
194
+ demo.launch(server_name="0.0.0.0", server_port=7860)
backend/.env.example ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ GROQ_API_KEY="your_groq_api_key_here"
2
+ DATABASE_URL=sqlite:///./test.db
backend/app/agents/graph.py ADDED
@@ -0,0 +1,424 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import TypedDict, Annotated, List, Union, Any
3
+
4
+ import matplotlib
5
+ import matplotlib.pyplot as plt
6
+ import os
7
+ import pandas as pd
8
+ import uuid
9
+ from langchain_groq import ChatGroq
10
+ from langchain_core.output_parsers import StrOutputParser
11
+ from langchain_core.prompts import ChatPromptTemplate
12
+ from langgraph.graph import StateGraph, END
13
+
14
+ from app.core.config import settings
15
+ from app.db.database import get_db_schema, engine
16
+ from app.services.pdf_generator import generate_pdf_report
17
+ from app.services.analytics import run_advanced_analytics
18
+
19
+ matplotlib.use('Agg') # Use non-interactive backend
20
+
21
+ # Define State
22
+ class AgentState(TypedDict, total=False):
23
+ query: str
24
+ history: List[dict]
25
+ schema: str
26
+ sql_query: str
27
+ data: Any # Pandas DataFrame as dict or list
28
+ visualization_path: str
29
+ visualization_summary: str
30
+ trend_analysis: dict
31
+ anomaly_analysis: dict
32
+ forecast_analysis: dict
33
+ statistical_tests: dict
34
+ insights: str
35
+ report_path: str
36
+ error: str
37
+ def _format_history(history: List[dict]) -> str:
38
+ if not history:
39
+ return "None"
40
+ rendered = []
41
+ for turn in history[-5:]:
42
+ question = turn.get("question", "")
43
+ answer = turn.get("insights", "")
44
+ rendered.append(f"User: {question}\nAgent: {answer}")
45
+ return "\n---\n".join(rendered)
46
+
47
+
48
+ # LLM Setup
49
+ def get_llm():
50
+ if not settings.GROQ_API_KEY:
51
+ # Fallback or mock if needed, but for now assume key is present or will fail
52
+ return None
53
+ return ChatGroq(
54
+ temperature=0,
55
+ model_name="openai/gpt-oss-120b",
56
+ api_key=settings.GROQ_API_KEY,
57
+ )
58
+
59
+
60
+ def _summarize_dataframe(df: pd.DataFrame) -> List[dict]:
61
+ summary = []
62
+ for col in df.columns:
63
+ series = df[col]
64
+ summary.append(
65
+ {
66
+ "name": col,
67
+ "dtype": str(series.dtype),
68
+ "numeric": pd.api.types.is_numeric_dtype(series),
69
+ "unique_count": int(series.nunique()),
70
+ "sample_values": series.dropna().astype(str).unique().tolist()[:3],
71
+ }
72
+ )
73
+ return summary
74
+
75
+
76
+ def _fallback_chart_plan(df: pd.DataFrame) -> dict:
77
+ numeric_cols = list(df.select_dtypes(include=["number", "bool"]).columns)
78
+ categorical_cols = list(df.select_dtypes(include=["object", "category"]).columns)
79
+
80
+ if categorical_cols and numeric_cols:
81
+ return {
82
+ "chart_type": "bar",
83
+ "x_field": categorical_cols[0],
84
+ "y_field": numeric_cols[0],
85
+ "aggregation": "sum",
86
+ "top_n": 10,
87
+ "explanation": f"Bar chart of {numeric_cols[0]} by {categorical_cols[0]}"
88
+ }
89
+
90
+ if len(numeric_cols) >= 2:
91
+ return {
92
+ "chart_type": "line",
93
+ "x_field": numeric_cols[0],
94
+ "y_field": numeric_cols[1],
95
+ "aggregation": "none",
96
+ "top_n": 50,
97
+ "explanation": f"Line plot of {numeric_cols[1]} vs {numeric_cols[0]}"
98
+ }
99
+
100
+ return {
101
+ "chart_type": "table",
102
+ "x_field": None,
103
+ "y_field": None,
104
+ "aggregation": "none",
105
+ "top_n": 0,
106
+ "explanation": "No suitable numeric/categorical combination for chart"
107
+ }
108
+
109
+
110
+ def _parse_json_response(text: str) -> dict:
111
+ text = text.strip()
112
+ if text.startswith("```"):
113
+ text = text.strip("`")
114
+ if text.startswith("json"):
115
+ text = text[4:]
116
+ start = text.find("{")
117
+ end = text.rfind("}")
118
+ if start != -1 and end != -1:
119
+ candidate = text[start:end + 1]
120
+ return json.loads(candidate)
121
+ return json.loads(text)
122
+
123
+
124
+ def _suggest_chart_plan(df: pd.DataFrame, query: str) -> dict:
125
+ plan = _fallback_chart_plan(df)
126
+ llm = get_llm()
127
+ if not llm:
128
+ return plan
129
+
130
+ columns_meta = _summarize_dataframe(df)
131
+ sample_rows = df.head(5).to_dict(orient="records")
132
+
133
+ template = """
134
+ You are an analytics visualization planner. Based on the user's question, the column metadata, and sample rows, choose the most appropriate chart to highlight the insight.
135
+
136
+ Allowed chart_type values: bar, line, area, scatter, pie, table.
137
+ aggregation can be sum, mean, avg, average, count, or none. Use count when only frequency matters.
138
+ Return ONLY valid JSON with keys: chart_type, x_field, y_field (nullable), aggregation, top_n (int), explanation.
139
+ Make sure fields exist in the dataset and chart type matches their dtypes (categorical for x axis on bar/pie, numeric for y).
140
+ Pick at most top 12 categories when using bar/pie.
141
+
142
+ Columns: {columns}
143
+ Sample rows: {sample}
144
+ User question: {query}
145
+ """
146
+
147
+ prompt = ChatPromptTemplate.from_template(template)
148
+ chain = prompt | llm | StrOutputParser()
149
+
150
+ try:
151
+ response = chain.invoke({
152
+ "columns": json.dumps(columns_meta, ensure_ascii=False),
153
+ "sample": json.dumps(sample_rows, ensure_ascii=False),
154
+ "query": query,
155
+ })
156
+ plan = _parse_json_response(response)
157
+ except Exception:
158
+ # keep fallback plan
159
+ plan.setdefault("explanation", "Heuristic visualization applied")
160
+ return plan
161
+
162
+
163
+ def _aggregate_for_chart(df: pd.DataFrame, x_field: str, y_field: str, aggregation: str) -> pd.DataFrame:
164
+ if not x_field or x_field not in df.columns:
165
+ return pd.DataFrame()
166
+
167
+ agg = (aggregation or "sum").lower()
168
+
169
+ if agg in ("sum", "total", "mean", "avg", "average"):
170
+ target_col = y_field if y_field in df.columns else None
171
+ if not target_col:
172
+ numeric_cols = df.select_dtypes(include=["number", "bool"]).columns
173
+ target_col = numeric_cols[0] if len(numeric_cols) else None
174
+ if not target_col or not pd.api.types.is_numeric_dtype(df[target_col]):
175
+ return pd.DataFrame()
176
+ agg_fn = "mean" if agg in ("mean", "avg", "average") else "sum"
177
+ grouped = df.groupby(x_field)[target_col].agg(agg_fn).reset_index()
178
+ return grouped.rename(columns={target_col: "value"})
179
+
180
+ if agg == "count":
181
+ grouped = df.groupby(x_field).size().reset_index(name="value")
182
+ return grouped
183
+
184
+ if y_field and y_field in df.columns and pd.api.types.is_numeric_dtype(df[y_field]):
185
+ subset = df[[x_field, y_field]].copy()
186
+ subset = subset.rename(columns={y_field: "value"})
187
+ return subset
188
+
189
+ return pd.DataFrame()
190
+
191
+
192
+ def _render_chart(path: str, df: pd.DataFrame, plan: dict) -> str:
193
+ chart_type = (plan.get("chart_type") or "bar").lower()
194
+ x_field = plan.get("x_field")
195
+ y_field = plan.get("y_field")
196
+ agg = plan.get("aggregation")
197
+ top_n = int(plan.get("top_n") or 12)
198
+
199
+ plt.figure(figsize=(10, 6))
200
+
201
+ if chart_type == "scatter" and x_field and y_field:
202
+ if x_field in df.columns and y_field in df.columns and \
203
+ pd.api.types.is_numeric_dtype(df[x_field]) and pd.api.types.is_numeric_dtype(df[y_field]):
204
+ plot_df = df[[x_field, y_field]].dropna().head(top_n)
205
+ if plot_df.empty:
206
+ return ""
207
+ plt.scatter(plot_df[x_field], plot_df[y_field], color="#5cd4f4")
208
+ plt.xlabel(x_field)
209
+ plt.ylabel(y_field)
210
+ plt.title(plan.get("explanation", f"{y_field} vs {x_field}"))
211
+ plt.tight_layout()
212
+ plt.savefig(path, bbox_inches="tight")
213
+ plt.close()
214
+ return path
215
+ return ""
216
+
217
+ if not x_field:
218
+ return ""
219
+
220
+ plot_df = _aggregate_for_chart(df, x_field, y_field, agg)
221
+ if plot_df.empty:
222
+ return ""
223
+
224
+ plot_df = plot_df.sort_values("value", ascending=False)
225
+ if top_n > 0:
226
+ plot_df = plot_df.head(top_n)
227
+
228
+ if chart_type == "pie":
229
+ plot_df.set_index(x_field)["value"].plot(kind="pie", autopct="%1.1f%%")
230
+ plt.ylabel("")
231
+ elif chart_type == "line":
232
+ plt.plot(plot_df[x_field], plot_df["value"], marker="o")
233
+ elif chart_type == "area":
234
+ plt.fill_between(plot_df[x_field], plot_df["value"], alpha=0.4)
235
+ plt.plot(plot_df[x_field], plot_df["value"], color="#7a83ff")
236
+ else:
237
+ plt.bar(plot_df[x_field], plot_df["value"], color="#7a83ff")
238
+
239
+ plt.xticks(rotation=45, ha="right")
240
+ plt.xlabel(x_field)
241
+ plt.ylabel(plan.get("y_field") or "Value")
242
+ plt.title(plan.get("explanation", "Visualization"))
243
+ plt.tight_layout()
244
+ plt.savefig(path, bbox_inches="tight")
245
+ plt.close()
246
+ return path
247
+
248
+ # Nodes
249
+ def get_schema_node(state: AgentState):
250
+ schema = get_db_schema()
251
+ return {"schema": schema}
252
+
253
+ def generate_sql_node(state: AgentState):
254
+ llm = get_llm()
255
+ if not llm:
256
+ return {"error": "LLM not configured"}
257
+
258
+ template = """
259
+ You are a SQL expert. Convert the following natural language query into a SQL query for SQLite.
260
+
261
+ Schema:
262
+ {schema}
263
+
264
+ Recent conversation:
265
+ {history}
266
+
267
+ Current Query: {query}
268
+
269
+ Return ONLY the SQL query, nothing else. Do not wrap in markdown code blocks.
270
+ """
271
+ prompt = ChatPromptTemplate.from_template(template)
272
+ chain = prompt | llm | StrOutputParser()
273
+
274
+ try:
275
+ sql_query = chain.invoke({
276
+ "schema": state["schema"],
277
+ "history": _format_history(state.get("history", [])),
278
+ "query": state["query"],
279
+ })
280
+ # Clean up sql if needed
281
+ sql_query = sql_query.replace("```sql", "").replace("```", "").strip()
282
+ return {"sql_query": sql_query}
283
+ except Exception as e:
284
+ return {"error": str(e)}
285
+
286
+ def execute_sql_node(state: AgentState):
287
+ if state.get("error"):
288
+ return state
289
+
290
+ try:
291
+ df = pd.read_sql(state["sql_query"], engine)
292
+ return {"data": df.to_dict(orient="records")}
293
+ except Exception as e:
294
+ return {"error": f"SQL Execution failed: {str(e)}"}
295
+
296
+ def generate_visualization_node(state: AgentState):
297
+ if state.get("error") or not state.get("data"):
298
+ return state
299
+
300
+ df = pd.DataFrame(state["data"])
301
+ if df.empty:
302
+ return {"visualization_path": None, "visualization_summary": "No data to visualize."}
303
+
304
+ plan = _suggest_chart_plan(df, state.get("query", ""))
305
+
306
+ filename = f"chart_{uuid.uuid4()}.png"
307
+ path = os.path.join("backend", "static", filename)
308
+ os.makedirs(os.path.dirname(path), exist_ok=True)
309
+
310
+ image_path = _render_chart(path, df, plan)
311
+
312
+ if not image_path:
313
+ return {"visualization_path": None, "visualization_summary": plan.get("explanation")}
314
+
315
+ return {"visualization_path": image_path, "visualization_summary": plan.get("explanation")}
316
+
317
+
318
+ def advanced_analytics_node(state: AgentState):
319
+ if state.get("error") or not state.get("data"):
320
+ return state
321
+
322
+ df = pd.DataFrame(state["data"])
323
+ if df.empty:
324
+ return {"trend_analysis": None, "anomaly_analysis": None, "forecast_analysis": None, "statistical_tests": None}
325
+
326
+ analytics = run_advanced_analytics(df)
327
+ return {
328
+ "trend_analysis": analytics.get("trend"),
329
+ "anomaly_analysis": analytics.get("anomaly"),
330
+ "forecast_analysis": analytics.get("forecast"),
331
+ "statistical_tests": analytics.get("statistical_tests"),
332
+ }
333
+
334
+ def generate_insights_node(state: AgentState):
335
+ if state.get("error"):
336
+ return state
337
+
338
+ llm = get_llm()
339
+ if not llm:
340
+ return {"insights": "LLM not configured"}
341
+
342
+ data_summary = str(state["data"])[:2000] # Truncate if too long
343
+
344
+ template = """
345
+ You are an analytics copilot. Use the latest query, the conversation history, the data sample, and the derived diagnostics (trends, anomalies, forecasts, and statistical tests) to provide incremental insights. If the user repeats a question, reference earlier answers instead of restating everything.
346
+
347
+ History:
348
+ {history}
349
+
350
+ Current Query: {query}
351
+ Data Sample: {data}
352
+ Trend Analysis: {trend}
353
+ Anomaly Analysis: {anomaly}
354
+ Forecast Analysis: {forecast}
355
+ Statistical Tests: {stats}
356
+
357
+ Provide 3-5 concise bullet insights plus a short summary paragraph. Mention forecasts and statistical significance when available.
358
+ """
359
+ prompt = ChatPromptTemplate.from_template(template)
360
+ chain = prompt | llm | StrOutputParser()
361
+
362
+ try:
363
+ insights = chain.invoke({
364
+ "query": state["query"],
365
+ "history": _format_history(state.get("history", [])),
366
+ "data": data_summary,
367
+ "trend": json.dumps(state.get("trend_analysis") or {}, ensure_ascii=False),
368
+ "anomaly": json.dumps(state.get("anomaly_analysis") or {}, ensure_ascii=False),
369
+ "forecast": json.dumps(state.get("forecast_analysis") or {}, ensure_ascii=False),
370
+ "stats": json.dumps(state.get("statistical_tests") or {}, ensure_ascii=False),
371
+ })
372
+ return {"insights": insights}
373
+ except Exception as e:
374
+ return {"insights": f"Failed to generate insights: {str(e)}"}
375
+
376
+ def build_report_node(state: AgentState):
377
+ if state.get("error"):
378
+ return state
379
+
380
+ filename = f"report_{uuid.uuid4()}.pdf"
381
+ path = os.path.join("backend", "static", filename)
382
+
383
+ try:
384
+ generate_pdf_report(
385
+ report_path=path,
386
+ title="Autonomous Data Analyst Report",
387
+ query=state.get("query", ""),
388
+ sql_query=state.get("sql_query", ""),
389
+ insights=state.get("insights", "No insights generated."),
390
+ chart_image_path=state.get("visualization_path"),
391
+ chart_summary=state.get("visualization_summary"),
392
+ trend_analysis=state.get("trend_analysis"),
393
+ anomaly_analysis=state.get("anomaly_analysis"),
394
+ forecast_analysis=state.get("forecast_analysis"),
395
+ statistical_tests=state.get("statistical_tests"),
396
+ data_sample=state.get("data"),
397
+ )
398
+ return {"report_path": path}
399
+ except Exception as e:
400
+ return {"error": f"Report generation failed: {str(e)}"}
401
+
402
+ def create_agent_graph():
403
+ workflow = StateGraph(AgentState)
404
+
405
+ # Add nodes
406
+ workflow.add_node("get_schema", get_schema_node)
407
+ workflow.add_node("generate_sql", generate_sql_node)
408
+ workflow.add_node("execute_sql", execute_sql_node)
409
+ workflow.add_node("visualize", generate_visualization_node)
410
+ workflow.add_node("advanced_analytics", advanced_analytics_node)
411
+ workflow.add_node("generate_insights", generate_insights_node)
412
+ workflow.add_node("build_report", build_report_node)
413
+
414
+ # Define edges
415
+ workflow.set_entry_point("get_schema")
416
+ workflow.add_edge("get_schema", "generate_sql")
417
+ workflow.add_edge("generate_sql", "execute_sql")
418
+ workflow.add_edge("execute_sql", "visualize")
419
+ workflow.add_edge("visualize", "advanced_analytics")
420
+ workflow.add_edge("advanced_analytics", "generate_insights")
421
+ workflow.add_edge("generate_insights", "build_report")
422
+ workflow.add_edge("build_report", END)
423
+
424
+ return workflow.compile()
backend/app/api/routes.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import defaultdict
2
+ from decimal import Decimal
3
+ import math
4
+ from typing import Optional, List, Dict, Any
5
+
6
+ from fastapi import APIRouter, HTTPException, UploadFile, File, Form
7
+ from fastapi.encoders import jsonable_encoder
8
+ from fastapi.responses import JSONResponse
9
+ from pydantic import BaseModel
10
+
11
+ from app.agents.graph import create_agent_graph
12
+ from app.services.csv_loader import ingest_csv_dataset, list_dataset_tables
13
+
14
+ try:
15
+ import numpy as np # type: ignore
16
+ except Exception: # pragma: no cover - numpy optional at runtime
17
+ np = None
18
+
19
+ router = APIRouter()
20
+ agent_graph = create_agent_graph()
21
+ session_histories: Dict[str, List[dict]] = defaultdict(list)
22
+
23
+
24
+ def _sanitize_for_json(value: Any):
25
+ """Recursively coerce values so FastAPI's JSON encoding never sees NaN/Inf."""
26
+ if value is None:
27
+ return None
28
+ if isinstance(value, bool):
29
+ return value
30
+ if isinstance(value, (int, str)):
31
+ return value
32
+ if isinstance(value, float):
33
+ return value if math.isfinite(value) else None
34
+ if isinstance(value, Decimal):
35
+ as_float = float(value)
36
+ return as_float if math.isfinite(as_float) else None
37
+ if np is not None:
38
+ if isinstance(value, (np.floating, np.integer)):
39
+ coerced = value.item()
40
+ if isinstance(coerced, float) and not math.isfinite(coerced):
41
+ return None
42
+ return coerced
43
+ if isinstance(value, np.ndarray):
44
+ return [_sanitize_for_json(item) for item in value.tolist()]
45
+ if isinstance(value, dict):
46
+ return {key: _sanitize_for_json(val) for key, val in value.items()}
47
+ if isinstance(value, (list, tuple, set)):
48
+ return [_sanitize_for_json(item) for item in value]
49
+ # Fallback to string representation for unsupported objects (e.g., Timestamp)
50
+ return str(value)
51
+
52
+
53
+ def _safe_json(payload: Any) -> JSONResponse:
54
+ sanitized = _sanitize_for_json(payload)
55
+ encoded = jsonable_encoder(
56
+ sanitized,
57
+ custom_encoder={
58
+ float: lambda v: v if math.isfinite(v) else None,
59
+ Decimal: lambda v: float(v) if math.isfinite(float(v)) else None,
60
+ },
61
+ )
62
+ return JSONResponse(content=encoded)
63
+
64
+ class QueryRequest(BaseModel):
65
+ query: str
66
+ session_id: Optional[str] = "default"
67
+
68
+
69
+ class SessionResetRequest(BaseModel):
70
+ session_id: str
71
+
72
+ @router.post("/analyze")
73
+ async def analyze_data(request: QueryRequest):
74
+ try:
75
+ history = list(session_histories.get(request.session_id, []))
76
+ initial_state = {"query": request.query, "history": history}
77
+ result = agent_graph.invoke(initial_state)
78
+
79
+ # Construct response
80
+ response = {
81
+ "status": "success",
82
+ "query": result.get("query"),
83
+ "sql_query": result.get("sql_query"),
84
+ "data": result.get("data"),
85
+ "insights": result.get("insights"),
86
+ "visualization_url": None,
87
+ "visualization_summary": result.get("visualization_summary"),
88
+ "trend_analysis": result.get("trend_analysis"),
89
+ "anomaly_analysis": result.get("anomaly_analysis"),
90
+ "forecast_analysis": result.get("forecast_analysis"),
91
+ "statistical_tests": result.get("statistical_tests"),
92
+ "report_url": None,
93
+ "error": result.get("error")
94
+ }
95
+
96
+ if result.get("visualization_path"):
97
+ # Convert path to URL (assuming static mount)
98
+ filename = result["visualization_path"].split("\\")[-1].split("/")[-1]
99
+ response["visualization_url"] = f"/static/{filename}"
100
+
101
+ if result.get("report_path"):
102
+ filename = result["report_path"].split("\\")[-1].split("/")[-1]
103
+ response["report_url"] = f"/static/{filename}"
104
+
105
+ if not result.get("error"):
106
+ session_histories[request.session_id].append(
107
+ {
108
+ "question": request.query,
109
+ "insights": result.get("insights", ""),
110
+ "sql": result.get("sql_query", ""),
111
+ }
112
+ )
113
+ # limit memory
114
+ session_histories[request.session_id] = session_histories[request.session_id][-10:]
115
+
116
+ return _safe_json(response)
117
+
118
+ except Exception as e:
119
+ raise HTTPException(status_code=500, detail=str(e))
120
+
121
+ @router.get("/health")
122
+ async def health_check():
123
+ return {"status": "healthy"}
124
+
125
+
126
+ @router.post("/upload-csv")
127
+ async def upload_csv_dataset(
128
+ file: UploadFile = File(...),
129
+ table_name: str = Form("sales")
130
+ ):
131
+ try:
132
+ payload = ingest_csv_dataset(file, table_name)
133
+ return _safe_json({"status": "success", **payload})
134
+ except ValueError as exc:
135
+ raise HTTPException(status_code=400, detail=str(exc))
136
+ except Exception as exc:
137
+ raise HTTPException(status_code=500, detail=str(exc))
138
+
139
+
140
+ @router.get("/datasets")
141
+ async def get_dataset_catalog():
142
+ try:
143
+ catalog = list_dataset_tables()
144
+ return _safe_json({"tables": catalog})
145
+ except Exception as exc:
146
+ raise HTTPException(status_code=500, detail=str(exc))
147
+
148
+
149
+ @router.post("/session/reset")
150
+ async def reset_session(req: SessionResetRequest):
151
+ session_histories.pop(req.session_id, None)
152
+ return _safe_json({"status": "cleared", "session_id": req.session_id})
backend/app/core/config.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ class Settings:
7
+ PROJECT_NAME: str = "Autonomous Data Analyst Agent"
8
+ GROQ_API_KEY: str = os.getenv("GROQ_API_KEY", "")
9
+ DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./test.db")
10
+
11
+ settings = Settings()
backend/app/db/database.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timedelta
2
+ import random
3
+
4
+ from sqlalchemy import create_engine, MetaData, inspect, text
5
+
6
+ from app.core.config import settings
7
+
8
+ engine = create_engine(settings.DATABASE_URL)
9
+ metadata = MetaData()
10
+
11
+
12
+ def ensure_sales_dataset():
13
+ inspector = inspect(engine)
14
+ if inspector.has_table("sales"):
15
+ return
16
+
17
+ with engine.begin() as conn:
18
+ conn.exec_driver_sql(
19
+ """
20
+ CREATE TABLE IF NOT EXISTS sales (
21
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
22
+ date DATE,
23
+ product_category TEXT,
24
+ product_name TEXT,
25
+ quantity INTEGER,
26
+ unit_price REAL,
27
+ total_amount REAL,
28
+ region TEXT
29
+ )
30
+ """
31
+ )
32
+
33
+ categories = ["Electronics", "Clothing", "Home", "Books"]
34
+ products = {
35
+ "Electronics": ["Laptop", "Smartphone", "Headphones", "Monitor"],
36
+ "Clothing": ["T-Shirt", "Jeans", "Jacket", "Sneakers"],
37
+ "Home": ["Sofa", "Table", "Lamp", "Rug"],
38
+ "Books": ["Fiction", "Non-Fiction", "Sci-Fi", "Biography"],
39
+ }
40
+ regions = ["North", "South", "East", "West"]
41
+
42
+ rows = []
43
+ start_date = datetime(2023, 1, 1)
44
+ for _ in range(365):
45
+ date = start_date + timedelta(days=random.randint(0, 364))
46
+ category = random.choice(categories)
47
+ product = random.choice(products[category])
48
+ quantity = random.randint(1, 10)
49
+ unit_price = round(random.uniform(10.0, 1200.0), 2)
50
+ total_amount = round(quantity * unit_price, 2)
51
+ region = random.choice(regions)
52
+
53
+ rows.append(
54
+ {
55
+ "date": date.strftime("%Y-%m-%d"),
56
+ "product_category": category,
57
+ "product_name": product,
58
+ "quantity": quantity,
59
+ "unit_price": unit_price,
60
+ "total_amount": total_amount,
61
+ "region": region,
62
+ }
63
+ )
64
+
65
+ conn.execute(
66
+ text(
67
+ """
68
+ INSERT INTO sales (date, product_category, product_name, quantity, unit_price, total_amount, region)
69
+ VALUES (:date, :product_category, :product_name, :quantity, :unit_price, :total_amount, :region)
70
+ """
71
+ ),
72
+ rows,
73
+ )
74
+
75
+
76
+ ensure_sales_dataset()
77
+
78
+
79
+ def get_db_schema():
80
+ metadata.reflect(bind=engine)
81
+ schema_info = []
82
+ for table in metadata.tables.values():
83
+ columns = [f"{col.name} ({col.type})" for col in table.columns]
84
+ schema_info.append(f"Table: {table.name}\nColumns: {', '.join(columns)}")
85
+ return "\n\n".join(schema_info)
backend/app/main.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.staticfiles import StaticFiles
4
+ from app.api import routes
5
+ import uvicorn
6
+ import os
7
+
8
+ app = FastAPI(title="Autonomous Data Analyst Agent")
9
+
10
+ # CORS
11
+ app.add_middleware(
12
+ CORSMiddleware,
13
+ allow_origins=["*"], # In production, replace with specific frontend origin
14
+ allow_credentials=True,
15
+ allow_methods=["*"],
16
+ allow_headers=["*"],
17
+ )
18
+
19
+ # Mount static files
20
+ static_dir = os.path.join("backend", "static")
21
+ os.makedirs(static_dir, exist_ok=True)
22
+ app.mount("/static", StaticFiles(directory=static_dir), name="static")
23
+
24
+ app.include_router(routes.router, prefix="/api")
25
+
26
+ @app.get("/")
27
+ def read_root():
28
+ return {"message": "Autonomous Data Analyst Agent API is running"}
29
+
30
+ if __name__ == "__main__":
31
+ uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
backend/app/services/analytics.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from typing import Dict, List, Optional, Tuple
3
+ import warnings
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from scipy import stats
8
+
9
+ try:
10
+ from statsmodels.tsa.holtwinters import ExponentialSmoothing
11
+ from statsmodels.tsa.stattools import adfuller
12
+ HAS_STATSMODELS = True
13
+ except ImportError:
14
+ HAS_STATSMODELS = False
15
+
16
+ DATE_HINTS = ("date", "time", "month", "year", "period")
17
+ METRIC_HINTS = ("sale", "amount", "revenue", "profit", "price", "total")
18
+ MAX_POINTS = 60
19
+
20
+
21
+ def _detect_datetime_column(df: pd.DataFrame) -> Optional[str]:
22
+ # Prefer columns already datetime typed
23
+ for col in df.columns:
24
+ if pd.api.types.is_datetime64_any_dtype(df[col]):
25
+ return col
26
+ # Fallback to columns whose names hint at date/time
27
+ for col in df.columns:
28
+ low = col.lower()
29
+ if any(hint in low for hint in DATE_HINTS):
30
+ try:
31
+ pd.to_datetime(df[col])
32
+ return col
33
+ except Exception:
34
+ continue
35
+ return None
36
+
37
+
38
+ def _detect_metric_column(df: pd.DataFrame) -> Optional[str]:
39
+ numeric_cols = [c for c in df.columns if pd.api.types.is_numeric_dtype(df[c])]
40
+ if not numeric_cols:
41
+ return None
42
+ for col in numeric_cols:
43
+ low = col.lower()
44
+ if any(hint in low for hint in METRIC_HINTS):
45
+ return col
46
+ return numeric_cols[0]
47
+
48
+
49
+ def _build_time_series(df: pd.DataFrame) -> Optional[pd.Series]:
50
+ if df is None or df.empty:
51
+ return None
52
+
53
+ date_col = _detect_datetime_column(df)
54
+ metric_col = _detect_metric_column(df)
55
+ if not date_col or not metric_col:
56
+ return None
57
+
58
+ ts = df[[date_col, metric_col]].copy()
59
+ ts[date_col] = pd.to_datetime(ts[date_col], errors="coerce")
60
+ ts = ts.dropna(subset=[date_col, metric_col])
61
+ if ts.empty:
62
+ return None
63
+
64
+ # Aggregate by month for smoother signals
65
+ ts["period"] = ts[date_col].dt.to_period("M").dt.to_timestamp()
66
+ grouped = ts.groupby("period")[metric_col].sum().sort_index()
67
+ if len(grouped) > MAX_POINTS:
68
+ grouped = grouped[-MAX_POINTS:]
69
+ return grouped
70
+
71
+
72
+ def _linear_trend(series: pd.Series) -> Optional[Dict[str, object]]:
73
+ if series is None or len(series) < 3:
74
+ return None
75
+
76
+ x = np.arange(len(series))
77
+ y = series.values.astype(float)
78
+
79
+ # Linear regression with confidence intervals
80
+ slope, intercept = np.polyfit(x, y, 1)
81
+ y_pred = slope * x + intercept
82
+
83
+ # Calculate standard error and confidence intervals
84
+ residuals = y - y_pred
85
+ n = len(y)
86
+ degrees_freedom = n - 2
87
+ residual_std_error = np.sqrt(np.sum(residuals**2) / degrees_freedom) if degrees_freedom > 0 else 0
88
+
89
+ # Standard error of slope
90
+ x_mean = np.mean(x)
91
+ se_slope = residual_std_error / np.sqrt(np.sum((x - x_mean)**2)) if np.sum((x - x_mean)**2) > 0 else 0
92
+
93
+ # 95% confidence interval for slope
94
+ t_val = stats.t.ppf(0.975, degrees_freedom) if degrees_freedom > 0 else 1.96
95
+ slope_ci_lower = slope - t_val * se_slope
96
+ slope_ci_upper = slope + t_val * se_slope
97
+
98
+ # Prediction intervals for the trend line
99
+ prediction_intervals = []
100
+ for i in range(len(x)):
101
+ se_pred = residual_std_error * np.sqrt(1 + 1/n + (x[i] - x_mean)**2 / np.sum((x - x_mean)**2))
102
+ pi_lower = y_pred[i] - t_val * se_pred
103
+ pi_upper = y_pred[i] + t_val * se_pred
104
+ prediction_intervals.append({
105
+ "lower": float(pi_lower),
106
+ "upper": float(pi_upper)
107
+ })
108
+
109
+ start_val = float(y[0])
110
+ end_val = float(y[-1])
111
+ change_pct = None
112
+ if not math.isclose(start_val, 0.0):
113
+ change_pct = ((end_val - start_val) / abs(start_val)) * 100
114
+
115
+ direction = "flat"
116
+ if slope > 0.02 * np.mean(y):
117
+ direction = "upward"
118
+ elif slope < -0.02 * np.mean(y):
119
+ direction = "downward"
120
+
121
+ summary = f"{direction.capitalize()} trend detected" if direction != "flat" else "Minimal trend detected"
122
+ if change_pct is not None:
123
+ summary += f" ({change_pct:+.1f}% over period)"
124
+ summary += f" with 95% confidence [slope: {slope_ci_lower:.2f} to {slope_ci_upper:.2f}]"
125
+
126
+ return {
127
+ "summary": summary,
128
+ "start": start_val,
129
+ "end": end_val,
130
+ "slope": float(slope),
131
+ "slope_ci_lower": float(slope_ci_lower),
132
+ "slope_ci_upper": float(slope_ci_upper),
133
+ "std_error": float(residual_std_error),
134
+ "r_squared": float(1 - np.sum(residuals**2) / np.sum((y - np.mean(y))**2)) if np.sum((y - np.mean(y))**2) > 0 else 0,
135
+ "change_pct": change_pct,
136
+ "points": [
137
+ {"period": period.strftime("%Y-%m"), "value": float(value)}
138
+ for period, value in series.items()
139
+ ],
140
+ "prediction_intervals": prediction_intervals,
141
+ }
142
+
143
+
144
+ def _anomaly_scan(series: pd.Series) -> Optional[Dict[str, object]]:
145
+ if series is None or len(series) < 4:
146
+ return None
147
+
148
+ values = series.values.astype(float)
149
+ mean = float(np.mean(values))
150
+ std = float(np.std(values))
151
+ if math.isclose(std, 0.0):
152
+ return None
153
+
154
+ z_scores = (values - mean) / std
155
+ anomalies: List[Dict[str, object]] = []
156
+ for idx, z in enumerate(z_scores):
157
+ if abs(z) >= 2.0:
158
+ period = series.index[idx]
159
+ anomalies.append(
160
+ {
161
+ "period": period.strftime("%Y-%m"),
162
+ "value": float(values[idx]),
163
+ "z_score": float(z),
164
+ }
165
+ )
166
+
167
+ if not anomalies:
168
+ return None
169
+
170
+ top = sorted(anomalies, key=lambda a: abs(a["z_score"]), reverse=True)[:3]
171
+ summary = "Anomalies detected at " + ", ".join(
172
+ [f"{a['period']} (z={a['z_score']:+.1f})" for a in top]
173
+ )
174
+
175
+ return {"summary": summary, "anomalies": anomalies, "mean": mean, "std": std}
176
+
177
+
178
+ def run_advanced_analytics(df: pd.DataFrame) -> Dict[str, Optional[Dict[str, object]]]:
179
+ series = _build_time_series(df)
180
+ trend = _linear_trend(series)
181
+ anomaly = _anomaly_scan(series)
182
+ forecast = _forecast_next_periods(series)
183
+ statistical_tests = _run_statistical_tests(series)
184
+ return {
185
+ "trend": trend,
186
+ "anomaly": anomaly,
187
+ "forecast": forecast,
188
+ "statistical_tests": statistical_tests,
189
+ }
190
+
191
+
192
+ def _forecast_next_periods(series: pd.Series, periods: int = 3) -> Optional[Dict[str, object]]:
193
+ """Generate forecasts using exponential smoothing with prediction intervals."""
194
+ if series is None or len(series) < 6:
195
+ return None
196
+
197
+ if not HAS_STATSMODELS:
198
+ return {
199
+ "summary": "Forecasting unavailable (statsmodels not installed)",
200
+ "forecasts": [],
201
+ }
202
+
203
+ try:
204
+ with warnings.catch_warnings():
205
+ warnings.simplefilter("ignore")
206
+
207
+ # Try Holt's exponential smoothing (trend method)
208
+ model = ExponentialSmoothing(
209
+ series.values,
210
+ trend='add',
211
+ seasonal=None,
212
+ initialization_method="estimated"
213
+ )
214
+ fitted = model.fit(optimized=True, remove_bias=False)
215
+
216
+ # Generate forecasts
217
+ forecast_values = fitted.forecast(steps=periods)
218
+
219
+ # Calculate prediction intervals using residual std
220
+ residuals = series.values - fitted.fittedvalues
221
+ residual_std = np.std(residuals)
222
+
223
+ # Generate future periods
224
+ last_period = series.index[-1]
225
+ freq = pd.infer_freq(series.index) or 'MS'
226
+ future_periods = pd.date_range(start=last_period, periods=periods + 1, freq=freq)[1:]
227
+
228
+ forecasts = []
229
+ for i, (period, value) in enumerate(zip(future_periods, forecast_values)):
230
+ # Prediction interval widens with forecast horizon
231
+ interval_width = residual_std * np.sqrt(i + 1) * 1.96
232
+ forecasts.append({
233
+ "period": period.strftime("%Y-%m"),
234
+ "value": float(value),
235
+ "lower_bound": float(value - interval_width),
236
+ "upper_bound": float(value + interval_width),
237
+ })
238
+
239
+ summary = f"Forecast for next {periods} periods using exponential smoothing"
240
+ if len(forecasts) > 0:
241
+ first_forecast = forecasts[0]["value"]
242
+ last_actual = float(series.values[-1])
243
+ change = ((first_forecast - last_actual) / abs(last_actual)) * 100 if last_actual != 0 else 0
244
+ summary += f" (next period: {first_forecast:.1f}, {change:+.1f}% vs current)"
245
+
246
+ return {
247
+ "summary": summary,
248
+ "method": "Exponential Smoothing (Holt)",
249
+ "forecasts": forecasts,
250
+ "model_params": {
251
+ "alpha": float(fitted.params.get('smoothing_level', 0)),
252
+ "beta": float(fitted.params.get('smoothing_trend', 0)) if fitted.params.get('smoothing_trend') else None,
253
+ }
254
+ }
255
+
256
+ except Exception as e:
257
+ return {
258
+ "summary": f"Forecasting failed: {str(e)[:100]}",
259
+ "forecasts": [],
260
+ }
261
+
262
+
263
+ def _run_statistical_tests(series: pd.Series) -> Optional[Dict[str, object]]:
264
+ """Run statistical comparison tests on time series data."""
265
+ if series is None or len(series) < 6:
266
+ return None
267
+
268
+ results = {}
269
+
270
+ # Split into two halves for comparison (e.g., first half vs second half)
271
+ mid = len(series) // 2
272
+ first_half = series.values[:mid]
273
+ second_half = series.values[mid:]
274
+
275
+ # T-test: Are the two periods significantly different?
276
+ try:
277
+ t_stat, p_value = stats.ttest_ind(first_half, second_half)
278
+ results["period_comparison"] = {
279
+ "test": "Independent t-test",
280
+ "comparison": "First half vs Second half",
281
+ "t_statistic": float(t_stat),
282
+ "p_value": float(p_value),
283
+ "significant": p_value < 0.05,
284
+ "summary": f"{'Significant' if p_value < 0.05 else 'No significant'} difference between periods (p={p_value:.4f})"
285
+ }
286
+ except Exception:
287
+ pass
288
+
289
+ # Test for stationarity (Augmented Dickey-Fuller test)
290
+ if HAS_STATSMODELS:
291
+ try:
292
+ adf_result = adfuller(series.values, autolag='AIC')
293
+ results["stationarity"] = {
294
+ "test": "Augmented Dickey-Fuller",
295
+ "adf_statistic": float(adf_result[0]),
296
+ "p_value": float(adf_result[1]),
297
+ "is_stationary": adf_result[1] < 0.05,
298
+ "summary": f"Series is {'stationary' if adf_result[1] < 0.05 else 'non-stationary'} (p={adf_result[1]:.4f})"
299
+ }
300
+ except Exception:
301
+ pass
302
+
303
+ # Quartile-based comparison (ANOVA-like for segments)
304
+ try:
305
+ quartiles = pd.qcut(series.index.to_series(), q=4, labels=False, duplicates='drop')
306
+ groups = [series.values[quartiles == i] for i in range(4) if len(series.values[quartiles == i]) > 0]
307
+
308
+ if len(groups) >= 2:
309
+ f_stat, p_value = stats.f_oneway(*groups)
310
+ results["quarterly_variance"] = {
311
+ "test": "One-way ANOVA",
312
+ "comparison": "Across quarters",
313
+ "f_statistic": float(f_stat),
314
+ "p_value": float(p_value),
315
+ "significant": p_value < 0.05,
316
+ "summary": f"{'Significant' if p_value < 0.05 else 'No significant'} variance across quarters (p={p_value:.4f})"
317
+ }
318
+ except Exception:
319
+ pass
320
+
321
+ if not results:
322
+ return None
323
+
324
+ # Overall summary
325
+ sig_tests = [v["summary"] for v in results.values() if "significant" in v.get("summary", "").lower()]
326
+ overall = f"{len(sig_tests)} significant finding(s): " + "; ".join(sig_tests[:2]) if sig_tests else "No significant patterns detected"
327
+
328
+ return {
329
+ "summary": overall,
330
+ "tests": results,
331
+ }
332
+
backend/app/services/csv_loader.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import re
3
+ from typing import Any, Dict, List
4
+
5
+ import pandas as pd
6
+ from fastapi import UploadFile
7
+ from sqlalchemy import inspect, text
8
+
9
+ from app.db.database import engine
10
+
11
+ SUPPORTED_ENCODINGS = ("utf-8", "latin1", "cp1252")
12
+
13
+ SAFE_TABLE_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
14
+
15
+
16
+ def _validate_table_name(name: str) -> str:
17
+ if not SAFE_TABLE_PATTERN.match(name):
18
+ raise ValueError("Table name must start with a letter/underscore and contain only alphanumerics or underscores.")
19
+ return name
20
+
21
+
22
+ def ingest_csv_dataset(
23
+ upload_file: UploadFile,
24
+ table_name: str = "sales",
25
+ ) -> Dict[str, Any]:
26
+ """Load an uploaded CSV file into the configured database table."""
27
+ raw_bytes = upload_file.file.read()
28
+ upload_file.file.seek(0)
29
+
30
+ dataframe = None
31
+ chosen_encoding = None
32
+ errors = []
33
+
34
+ for encoding in SUPPORTED_ENCODINGS:
35
+ buffer = io.BytesIO(raw_bytes)
36
+ try:
37
+ dataframe = pd.read_csv(buffer, encoding=encoding)
38
+ chosen_encoding = encoding
39
+ break
40
+ except UnicodeDecodeError as exc:
41
+ errors.append(f"{encoding}: {exc}")
42
+ continue
43
+ except Exception as exc:
44
+ raise ValueError(f"Unable to parse CSV: {exc}")
45
+
46
+ if dataframe is None:
47
+ raise ValueError(
48
+ "Unable to decode CSV. Please upload UTF-8 or Latin-1 encoded files." +
49
+ (f" Details: {'; '.join(errors)}" if errors else "")
50
+ )
51
+
52
+ if dataframe.empty:
53
+ raise ValueError("Uploaded CSV is empty.")
54
+
55
+ safe_table_name = _validate_table_name(table_name.strip() or "sales")
56
+ dataframe.to_sql(safe_table_name, engine, if_exists="replace", index=False)
57
+
58
+ # Convert first 5 rows to dict for preview
59
+ preview_rows = dataframe.head(5).to_dict(orient="records")
60
+
61
+ return {
62
+ "table": safe_table_name,
63
+ "rows": len(dataframe),
64
+ "columns": list(dataframe.columns),
65
+ "encoding": chosen_encoding,
66
+ "preview": preview_rows
67
+ }
68
+
69
+
70
+ def list_dataset_tables(limit: int = 15) -> List[Dict[str, Any]]:
71
+ inspector = inspect(engine)
72
+ tables = [t for t in inspector.get_table_names() if not t.startswith("sqlite_")][:limit]
73
+ catalog: List[Dict[str, Any]] = []
74
+
75
+ with engine.connect() as conn:
76
+ for table_name in tables:
77
+ column_info = inspector.get_columns(table_name)
78
+ try:
79
+ result = conn.execute(text(f"SELECT COUNT(*) as cnt FROM {table_name}"))
80
+ row_count = result.scalar() or 0
81
+ except Exception:
82
+ row_count = None
83
+
84
+ catalog.append(
85
+ {
86
+ "table": table_name,
87
+ "rows": int(row_count) if row_count is not None else None,
88
+ "columns": [col.get("name") for col in column_info],
89
+ }
90
+ )
91
+ return catalog
backend/app/services/pdf_generator.py ADDED
@@ -0,0 +1,451 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from datetime import datetime
3
+ from typing import Optional
4
+
5
+ from reportlab.lib import colors
6
+ from reportlab.lib.pagesizes import letter
7
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
8
+ from reportlab.lib.units import inch
9
+ from reportlab.platypus import (
10
+ SimpleDocTemplate,
11
+ Paragraph,
12
+ Spacer,
13
+ Table,
14
+ TableStyle,
15
+ Image,
16
+ Preformatted,
17
+ HRFlowable,
18
+ PageBreak,
19
+ )
20
+
21
+
22
+ def _build_styles():
23
+ styles = getSampleStyleSheet()
24
+ styles.add(
25
+ ParagraphStyle(
26
+ name="HeroTitle",
27
+ parent=styles["Title"],
28
+ fontName="Helvetica-Bold",
29
+ fontSize=20,
30
+ leading=26,
31
+ textColor=colors.HexColor("#111827"),
32
+ )
33
+ )
34
+ styles.add(
35
+ ParagraphStyle(
36
+ name="SectionHeader",
37
+ parent=styles["Heading2"],
38
+ fontName="Helvetica-Bold",
39
+ fontSize=14,
40
+ spaceAfter=6,
41
+ textColor=colors.HexColor("#111827"),
42
+ )
43
+ )
44
+ styles.add(
45
+ ParagraphStyle(
46
+ name="ReportBody",
47
+ parent=styles["BodyText"],
48
+ fontSize=11,
49
+ leading=16,
50
+ )
51
+ )
52
+ styles.add(
53
+ ParagraphStyle(
54
+ name="MetaLabel",
55
+ parent=styles["BodyText"],
56
+ fontSize=8,
57
+ textColor=colors.HexColor("#6b7280"),
58
+ leading=10,
59
+ )
60
+ )
61
+ styles.add(
62
+ ParagraphStyle(
63
+ name="QueryText",
64
+ parent=styles["BodyText"],
65
+ fontSize=12,
66
+ leading=16,
67
+ textColor=colors.HexColor("#111827"),
68
+ )
69
+ )
70
+ styles.add(
71
+ ParagraphStyle(
72
+ name="MetricValue",
73
+ parent=styles["BodyText"],
74
+ fontName="Helvetica-Bold",
75
+ fontSize=13,
76
+ textColor=colors.HexColor("#111827"),
77
+ )
78
+ )
79
+ styles.add(
80
+ ParagraphStyle(
81
+ name="MetricLabel",
82
+ parent=styles["BodyText"],
83
+ fontSize=9,
84
+ textColor=colors.HexColor("#6b7280"),
85
+ )
86
+ )
87
+ styles.add(
88
+ ParagraphStyle(
89
+ name="CaptionSmall",
90
+ parent=styles["BodyText"],
91
+ fontSize=9,
92
+ textColor=colors.HexColor("#6b7280"),
93
+ )
94
+ )
95
+ styles.add(
96
+ ParagraphStyle(
97
+ name="InsightBullet",
98
+ parent=styles["BodyText"],
99
+ leftIndent=14,
100
+ bulletIndent=6,
101
+ bulletFontName="Helvetica",
102
+ bulletFontSize=10,
103
+ leading=16,
104
+ )
105
+ )
106
+ styles.add(
107
+ ParagraphStyle(
108
+ name="CodeBlock",
109
+ parent=styles["BodyText"],
110
+ fontName="Courier",
111
+ fontSize=9,
112
+ leading=14,
113
+ backColor=colors.whitesmoke,
114
+ )
115
+ )
116
+ return styles
117
+
118
+
119
+ def _format_insights(insights: str, styles) -> list:
120
+ blocks = []
121
+ lines = [line.strip() for line in (insights or "").splitlines() if line.strip()]
122
+ if not lines:
123
+ return [Paragraph("No insights provided.", styles["ReportBody"])]
124
+
125
+ for line in lines:
126
+ if line[0] in {"-", "*", "•"}:
127
+ text = line.lstrip("-•* ").strip()
128
+ blocks.append(Paragraph(text, styles["InsightBullet"], bulletText="•"))
129
+ else:
130
+ blocks.append(Paragraph(line, styles["ReportBody"]))
131
+ blocks.append(Spacer(1, 4))
132
+ return blocks
133
+
134
+
135
+ def _build_data_table(data_sample):
136
+ if not data_sample:
137
+ return None
138
+
139
+ columns = list(data_sample[0].keys())
140
+ rows = [columns]
141
+ for row in data_sample[:10]:
142
+ rows.append([str(row.get(col, "")) for col in columns])
143
+
144
+ table = Table(rows, repeatRows=1)
145
+ table.setStyle(
146
+ TableStyle(
147
+ [
148
+ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#111827")),
149
+ ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
150
+ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
151
+ ("FONTSIZE", (0, 0), (-1, 0), 10),
152
+ ("BOTTOMPADDING", (0, 0), (-1, 0), 6),
153
+ ("BACKGROUND", (0, 1), (-1, -1), colors.HexColor("#f9fafb")),
154
+ ("TEXTCOLOR", (0, 1), (-1, -1), colors.HexColor("#111827")),
155
+ ("GRID", (0, 0), (-1, -1), 0.25, colors.HexColor("#e5e7eb")),
156
+ ]
157
+ )
158
+ )
159
+ return table
160
+
161
+
162
+ def _build_query_callout(query: str, styles):
163
+ display = query or "No question provided"
164
+ content = [
165
+ [Paragraph("BUSINESS QUESTION", styles["MetaLabel"])],
166
+ [Paragraph(display, styles["QueryText"])]
167
+ ]
168
+ table = Table(content, colWidths=[6.5 * inch])
169
+ table.setStyle(
170
+ TableStyle(
171
+ [
172
+ ("BACKGROUND", (0, 0), (-1, -1), colors.HexColor("#f3f4ff")),
173
+ ("BOX", (0, 0), (-1, -1), 0.6, colors.HexColor("#c7d2fe")),
174
+ ("INNERPADDING", (0, 0), (-1, -1), 8),
175
+ ]
176
+ )
177
+ )
178
+ return table
179
+
180
+
181
+ def _build_metric_cards(stats: dict, styles):
182
+ if not stats:
183
+ return None
184
+
185
+ data = []
186
+ row = []
187
+ for label, value in stats.items():
188
+ cell = [
189
+ Paragraph(label, styles["MetricLabel"]),
190
+ Paragraph(value, styles["MetricValue"]),
191
+ ]
192
+ row.append(cell)
193
+
194
+ data.append(row)
195
+
196
+ col_width = (6.5 * inch) / len(stats)
197
+ table = Table(data, colWidths=[col_width] * len(stats))
198
+ table.setStyle(
199
+ TableStyle(
200
+ [
201
+ ("BACKGROUND", (0, 0), (-1, -1), colors.HexColor("#f9fafb")),
202
+ ("BOX", (0, 0), (-1, -1), 0.5, colors.HexColor("#e5e7eb")),
203
+ ("INNERPADDING", (0, 0), (-1, -1), 8),
204
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
205
+ ]
206
+ )
207
+ )
208
+ return table
209
+
210
+
211
+ def _format_value(value):
212
+ if value is None:
213
+ return "—"
214
+ if isinstance(value, float):
215
+ return f"{value:,.2f}"
216
+ if isinstance(value, int):
217
+ return f"{value:,}"
218
+ return str(value)
219
+
220
+
221
+ def _render_trend_section(trend_analysis, styles):
222
+ blocks = [Paragraph("Trend Diagnostics", styles["SectionHeader"])]
223
+ if not trend_analysis:
224
+ blocks.append(Paragraph("Not enough chronological data to compute a trend.", styles["ReportBody"]))
225
+ return blocks
226
+
227
+ blocks.append(Paragraph(trend_analysis.get("summary", ""), styles["ReportBody"]))
228
+ stats = [
229
+ ["Starting value", _format_value(trend_analysis.get("start"))],
230
+ ["Ending value", _format_value(trend_analysis.get("end"))],
231
+ ["Slope", _format_value(trend_analysis.get("slope"))],
232
+ ]
233
+ if trend_analysis.get("change_pct") is not None:
234
+ stats.append(["% Change", f"{trend_analysis['change_pct']:+.1f}%"])
235
+
236
+ table = Table(stats, colWidths=[2.3 * inch, 4 * inch])
237
+ table.setStyle(
238
+ TableStyle(
239
+ [
240
+ ("BACKGROUND", (0, 0), (-1, -1), colors.HexColor("#f9fafb")),
241
+ ("BOX", (0, 0), (-1, -1), 0.4, colors.HexColor("#e5e7eb")),
242
+ ("INNERPADDING", (0, 0), (-1, -1), 6),
243
+ ("FONTNAME", (0, 0), (-1, -1), "Helvetica"),
244
+ ]
245
+ )
246
+ )
247
+ blocks.extend([Spacer(1, 6), table])
248
+ return blocks
249
+
250
+
251
+ def _render_anomaly_section(anomaly_analysis, styles):
252
+ blocks = [Paragraph("Anomaly Highlights", styles["SectionHeader"])]
253
+ if not anomaly_analysis:
254
+ blocks.append(Paragraph("No statistical anomalies detected across the observed period.", styles["ReportBody"]))
255
+ return blocks
256
+
257
+ blocks.append(Paragraph(anomaly_analysis.get("summary", ""), styles["ReportBody"]))
258
+ table_rows = [["Period", "Value", "z-score"]]
259
+ for entry in anomaly_analysis.get("anomalies", [])[:8]:
260
+ table_rows.append([
261
+ entry.get("period", "?"),
262
+ _format_value(entry.get("value")),
263
+ f"{entry.get('z_score', 0):+.2f}",
264
+ ])
265
+ table = Table(table_rows, repeatRows=1, colWidths=[2 * inch, 2.5 * inch, 1.5 * inch])
266
+ table.setStyle(
267
+ TableStyle(
268
+ [
269
+ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#111827")),
270
+ ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
271
+ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
272
+ ("BACKGROUND", (0, 1), (-1, -1), colors.HexColor("#f3f4f6")),
273
+ ("GRID", (0, 0), (-1, -1), 0.25, colors.HexColor("#d1d5db")),
274
+ ]
275
+ )
276
+ )
277
+ blocks.extend([Spacer(1, 6), table])
278
+ return blocks
279
+
280
+
281
+ def _render_forecast_section(forecast_analysis, styles):
282
+ blocks = [Paragraph("Forecast", styles["SectionHeader"])]
283
+ if not forecast_analysis or not forecast_analysis.get("forecasts"):
284
+ blocks.append(Paragraph("Forecasting unavailable for this dataset.", styles["ReportBody"]))
285
+ return blocks
286
+
287
+ blocks.append(Paragraph(forecast_analysis.get("summary", ""), styles["ReportBody"]))
288
+
289
+ method = forecast_analysis.get("method", "N/A")
290
+ blocks.append(Spacer(1, 4))
291
+ blocks.append(Paragraph(f"<i>Method: {method}</i>", styles["CaptionSmall"]))
292
+
293
+ table_rows = [["Period", "Forecast", "Lower Bound", "Upper Bound"]]
294
+ for entry in forecast_analysis.get("forecasts", []):
295
+ table_rows.append([
296
+ entry.get("period", "?"),
297
+ _format_value(entry.get("value")),
298
+ _format_value(entry.get("lower_bound")),
299
+ _format_value(entry.get("upper_bound")),
300
+ ])
301
+
302
+ table = Table(table_rows, repeatRows=1, colWidths=[1.5 * inch, 1.5 * inch, 1.5 * inch, 1.5 * inch])
303
+ table.setStyle(
304
+ TableStyle(
305
+ [
306
+ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#111827")),
307
+ ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
308
+ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
309
+ ("BACKGROUND", (0, 1), (-1, -1), colors.HexColor("#f3f4f6")),
310
+ ("GRID", (0, 0), (-1, -1), 0.25, colors.HexColor("#d1d5db")),
311
+ ]
312
+ )
313
+ )
314
+ blocks.extend([Spacer(1, 6), table])
315
+ return blocks
316
+
317
+
318
+ def _render_statistical_tests_section(statistical_tests, styles):
319
+ blocks = [Paragraph("Statistical Tests", styles["SectionHeader"])]
320
+ if not statistical_tests or not statistical_tests.get("tests"):
321
+ blocks.append(Paragraph("No statistical tests performed.", styles["ReportBody"]))
322
+ return blocks
323
+
324
+ blocks.append(Paragraph(statistical_tests.get("summary", ""), styles["ReportBody"]))
325
+ blocks.append(Spacer(1, 8))
326
+
327
+ for test_name, test_data in statistical_tests.get("tests", {}).items():
328
+ test_title = test_data.get("test", test_name)
329
+ blocks.append(Paragraph(f"<b>{test_title}</b>", styles["ReportBody"]))
330
+ blocks.append(Spacer(1, 2))
331
+
332
+ details = []
333
+ if "comparison" in test_data:
334
+ details.append(f"Comparison: {test_data['comparison']}")
335
+ if "p_value" in test_data:
336
+ p_val = test_data["p_value"]
337
+ sig = "Yes" if test_data.get("significant", False) else "No"
338
+ details.append(f"p-value: {p_val:.4f} (Significant: {sig})")
339
+ if "summary" in test_data:
340
+ details.append(test_data["summary"])
341
+
342
+ for detail in details:
343
+ blocks.append(Paragraph(f"• {detail}", styles["ReportBody"]))
344
+
345
+ blocks.append(Spacer(1, 6))
346
+
347
+ return blocks
348
+
349
+
350
+ def generate_pdf_report(
351
+ report_path: str,
352
+ title: str,
353
+ query: str,
354
+ sql_query: str,
355
+ insights: str,
356
+ chart_image_path: str = None,
357
+ chart_summary: str = None,
358
+ trend_analysis: Optional[dict] = None,
359
+ anomaly_analysis: Optional[dict] = None,
360
+ forecast_analysis: Optional[dict] = None,
361
+ statistical_tests: Optional[dict] = None,
362
+ data_sample=None,
363
+ ):
364
+ styles = _build_styles()
365
+ doc = SimpleDocTemplate(
366
+ report_path,
367
+ pagesize=letter,
368
+ leftMargin=0.75 * inch,
369
+ rightMargin=0.75 * inch,
370
+ topMargin=0.75 * inch,
371
+ bottomMargin=0.75 * inch,
372
+ )
373
+
374
+ story = []
375
+
376
+ generated_at = datetime.utcnow().strftime("%B %d, %Y %H:%M UTC")
377
+ row_count = len(data_sample or [])
378
+ column_count = len(data_sample[0]) if data_sample else 0
379
+
380
+ story.append(Paragraph(title, styles["HeroTitle"]))
381
+ story.append(Paragraph("Autonomous insight report", styles["CaptionSmall"]))
382
+ story.append(Spacer(1, 10))
383
+
384
+ story.append(_build_query_callout(query, styles))
385
+ story.append(Spacer(1, 12))
386
+
387
+ metrics = {
388
+ "Rows Returned": f"{row_count:,}" if row_count else "0",
389
+ "Columns": str(column_count),
390
+ "Generated": generated_at,
391
+ }
392
+ metric_table = _build_metric_cards(metrics, styles)
393
+ if metric_table:
394
+ story.append(metric_table)
395
+ story.append(Spacer(1, 18))
396
+
397
+ story.append(Paragraph("This auto-generated briefing captures the freshest SQL results, advanced analytics, and executive visuals from InsightPilot.", styles["ReportBody"]))
398
+ story.append(Spacer(1, 14))
399
+ story.append(PageBreak())
400
+
401
+ story.append(HRFlowable(width="100%", thickness=0.6, color=colors.HexColor("#e5e7eb")))
402
+ story.append(Spacer(1, 14))
403
+
404
+ story.append(Paragraph("SQL Used", styles["SectionHeader"]))
405
+ story.append(
406
+ Preformatted(sql_query.strip() or "No SQL generated", styles["CodeBlock"], maxLineLength=80)
407
+ )
408
+ story.append(Spacer(1, 12))
409
+
410
+ story.append(Paragraph("Insights", styles["SectionHeader"]))
411
+ for block in _format_insights(insights, styles):
412
+ story.append(block)
413
+ story.append(Spacer(1, 10))
414
+
415
+ table = _build_data_table(data_sample or [])
416
+ if table:
417
+ story.append(Paragraph("Data Preview", styles["SectionHeader"]))
418
+ story.append(table)
419
+ story.append(Spacer(1, 12))
420
+
421
+ for block in _render_trend_section(trend_analysis, styles):
422
+ story.append(block)
423
+ story.append(Spacer(1, 12))
424
+
425
+ for block in _render_anomaly_section(anomaly_analysis, styles):
426
+ story.append(block)
427
+ story.append(Spacer(1, 12))
428
+
429
+ for block in _render_forecast_section(forecast_analysis, styles):
430
+ story.append(block)
431
+ story.append(Spacer(1, 12))
432
+
433
+ for block in _render_statistical_tests_section(statistical_tests, styles):
434
+ story.append(block)
435
+ story.append(Spacer(1, 12))
436
+
437
+ if chart_image_path and os.path.exists(chart_image_path):
438
+ story.append(Paragraph("Visualization", styles["SectionHeader"]))
439
+ img = Image(chart_image_path)
440
+ img._restrictSize(6.5 * inch, 4.5 * inch)
441
+ img.hAlign = "CENTER"
442
+ story.append(img)
443
+ if chart_summary:
444
+ story.append(Spacer(1, 6))
445
+ story.append(Paragraph(chart_summary, styles["CaptionSmall"]))
446
+ elif chart_summary:
447
+ story.append(Paragraph("Visualization Summary", styles["SectionHeader"]))
448
+ story.append(Paragraph(chart_summary, styles["ReportBody"]))
449
+
450
+ doc.build(story)
451
+ return report_path
backend/create_db.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import pandas as pd
3
+ import random
4
+ from datetime import datetime, timedelta
5
+
6
+ def create_dummy_db():
7
+ conn = sqlite3.connect('test.db')
8
+ cursor = conn.cursor()
9
+
10
+ # Create Sales Table
11
+ cursor.execute('''
12
+ CREATE TABLE IF NOT EXISTS sales (
13
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
14
+ date DATE,
15
+ product_category TEXT,
16
+ product_name TEXT,
17
+ quantity INTEGER,
18
+ unit_price REAL,
19
+ total_amount REAL,
20
+ region TEXT
21
+ )
22
+ ''')
23
+
24
+ # Generate sample data
25
+ categories = ['Electronics', 'Clothing', 'Home', 'Books']
26
+ products = {
27
+ 'Electronics': ['Laptop', 'Smartphone', 'Headphones', 'Monitor'],
28
+ 'Clothing': ['T-Shirt', 'Jeans', 'Jacket', 'Sneakers'],
29
+ 'Home': ['Sofa', 'Table', 'Lamp', 'Rug'],
30
+ 'Books': ['Fiction', 'Non-Fiction', 'Sci-Fi', 'Biography']
31
+ }
32
+ regions = ['North', 'South', 'East', 'West']
33
+
34
+ data = []
35
+ start_date = datetime(2023, 1, 1)
36
+ for i in range(100):
37
+ date = start_date + timedelta(days=random.randint(0, 365))
38
+ category = random.choice(categories)
39
+ product = random.choice(products[category])
40
+ quantity = random.randint(1, 5)
41
+ unit_price = round(random.uniform(10.0, 1000.0), 2)
42
+ total_amount = round(quantity * unit_price, 2)
43
+ region = random.choice(regions)
44
+
45
+ data.append((date.strftime('%Y-%m-%d'), category, product, quantity, unit_price, total_amount, region))
46
+
47
+ cursor.executemany('''
48
+ INSERT INTO sales (date, product_category, product_name, quantity, unit_price, total_amount, region)
49
+ VALUES (?, ?, ?, ?, ?, ?, ?)
50
+ ''', data)
51
+
52
+ conn.commit()
53
+ conn.close()
54
+ print("Dummy database created successfully.")
55
+
56
+ if __name__ == "__main__":
57
+ create_dummy_db()
backend/requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ langgraph
4
+ langchain
5
+ langchain-groq
6
+ langchain-community
7
+ sqlalchemy
8
+ pandas
9
+ numpy
10
+ scipy
11
+ statsmodels
12
+ matplotlib
13
+ reportlab
14
+ python-dotenv
15
+ pydantic
16
+ requests
17
+ python-multipart
build_frontend.bat ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ REM Build script for React frontend (Windows) - run this before deploying to HF Spaces
3
+
4
+ echo Building React frontend for production...
5
+
6
+ cd frontend
7
+
8
+ REM Install dependencies
9
+ echo Installing dependencies...
10
+ call npm install
11
+
12
+ REM Build for production
13
+ echo Building production bundle...
14
+ call npm run build
15
+
16
+ echo Frontend build complete! Output in frontend/dist/
17
+ echo Files ready to be served statically by the Gradio app
18
+
19
+ cd ..
build_frontend.sh ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Build script for React frontend - run this before deploying to HF Spaces
3
+
4
+ echo "🔨 Building React frontend for production..."
5
+
6
+ cd frontend
7
+
8
+ # Install dependencies
9
+ echo "📦 Installing dependencies..."
10
+ npm install
11
+
12
+ # Build for production
13
+ echo "🏗️ Building production bundle..."
14
+ npm run build
15
+
16
+ echo "✅ Frontend build complete! Output in frontend/dist/"
17
+ echo "📁 Files ready to be served statically by the Gradio app"
18
+
19
+ cd ..
data/archive.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:06179cdd0ddf1e4df27d19b80c31bb4ad873453f41a26f00d332d3c720c8e2fd
3
+ size 79402
data/report_f86d7e18-bf33-4d6f-a4fc-e57eb10590f7.pdf ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ %PDF-1.3
2
+ %���� ReportLab Generated PDF document http://www.reportlab.com
3
+ 1 0 obj
4
+ <<
5
+ /F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 5 0 R
6
+ >>
7
+ endobj
8
+ 2 0 obj
9
+ <<
10
+ /BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
11
+ >>
12
+ endobj
13
+ 3 0 obj
14
+ <<
15
+ /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
16
+ >>
17
+ endobj
18
+ 4 0 obj
19
+ <<
20
+ /BaseFont /Symbol /Name /F3 /Subtype /Type1 /Type /Font
21
+ >>
22
+ endobj
23
+ 5 0 obj
24
+ <<
25
+ /BaseFont /ZapfDingbats /Name /F4 /Subtype /Type1 /Type /Font
26
+ >>
27
+ endobj
28
+ 6 0 obj
29
+ <<
30
+ /BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 480 /Length 22647 /Subtype /Image
31
+ /Type /XObject /Width 640
32
+ >>
33
+ stream
34
+ Gb"/l&"H@:rr7iNCQ'-i_$hMWAX9\K-+,gs@]:.A&>:DHn?!rTVE\GYVl27Y+-a'JQptFt>@IuJ&Rt\Z1]FJ*M=bF;72V!Z]t\V0cJpg,mU`lq4eMI/cf_&nO)rXXpV+r=FjBrumcVm+=%N\uzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz!!!.4s8Ms3.TCZkp")S'r2Wl#P3823s0>FiR@,L"qoq(Wr3B,a'J?]o!2,l?CY$SJr7uSS?2?poZEjJ318TK<chR%j(GB+]]101K!!)aJA]b])QDGcYr?9FS27r\HRkOa>oPaN(;PhNapYG#E7^X6:!;AeZ#"!H@dD#Y]-=`%W^0!gOp?^IFT7+.FUILB.OUW3c(d33;l-lQ4LWE3IqccKo:f-Z6@umIaVou;O[;+5eSN:W@ZGfkTo`"f]s7r:fYJ*%E-70lZiRbjDot5SAXRuf.*-gth#Lh,LU8+Ka=0LJoH["@+%0$4;DV_nYDS%Ja5$jh\IJ``==0K%krU@]8_hSa=(LesF.I#DfDuJi`ru!)Pr;?I64aMo5d2b,]fVqfIXKD'#c'pXFo+9TmNZC3IqeW"Z\T;E:8b")Z,V*Ycs7_$C=-fVUU9;3'g.e%pq*&?.MA>O4[>U$(X]DQ-f</ENo('rigR^QTUh"&;3:5V<s,TDDBPf>B9W]KKrX]&0#U`icDXGuTQOM'dVG6q\.47l\OS(@sb=\bEXK8LPrVFY,STiq)4_4*B`Moi9='g0KFoT5VrU7%4_hJV%>]I`H:HgdSegr9<P`uk"i=@"oCd$hlqfhc#fp8R!aic?PI/j0>9A`mA(+rkEU.'u[j2T2hrqOMu-71'3S_;grVLPGNUg)T^D>W=N08_Ds8\)B\rH`6pn(tb>k+du$qWb,b:7=$(GJF%2Rr?#Dmf$=BorDHu9("KF`f,95]iT+FmG$7HqT:=laNa,,\(gZWm'G#Kbo:T6;=:d4%QA7YZqd/Q:I"fbm$0sQ(DeIIp$:5%=GeBT\8gQgiaV6Q++3QDjiWjbcmMV\r6-tCe>ueO>he^.XF4m;ZK9ndh`Z&CbWl$a%s%8u-_@QNb&NP1^ZhVbXCLneVk7;f+TMKRB)FlCQDppdOn%+2oD%@,bF4\H='@Q]5C]!PZ!A=qV1gG[^D*r$o2>Fo>ISM@biN_'H1Id"c*_?=qDOm_s.2H)^.FM81AR1=qlpSL2XJeaK6:+-DWoH@_=tmb.P.!4g1qaUkTO(RDd$\^Ycps$Z(jdJZF@ug"e#?oDd`LJ4u]^G:ftGZ\_NG6q8%[gXYo[P_tRHT2:l)3X&c?SJcb88LgG8XAb'0KGM`T7*,%S:?$G+)UIG^iAt[JBK008:!5aSBs%\q86.Yg[gA0is?cnhX*dc,B)<C8$kK]Xqp=jN#q;rWaFkTdV9C-$OF&A>o#.`'J1M<fP:In"n0k,gDd^O]bD;$8YJ,]&3^[XVH9??u2lQ(TL@&m>1m(%BQXMU-p[+FC%kKB=WT7>_/H-\YJaR%G,H82"]SD:#<T0U^L\lW6e[=)!=>Fqj9kK`c>lYXt<3HJa,aH7_HP>ca*+gYS6IIk]ulI;f=k54V^[r)E8h]NF1!.]%JhnK[an:43?V!39q-@OLZHZs8Y^ON*6A2%rW\8e?;Q0^^sroQ/SZ*mtJs%;=LUnl'pj`tC6l2*tTRDY%*4#SrR[:7+4p@Ae30YlE-gV7[?oi?bH*'8>f\qA42`F/#_Xeh3SmbJgqJ#pN@[r7nP7P^)28j:hN,tjQF*BsJt@p*-HCmcitrrYERoP^X&SUB&1rJ(e!@c+OjHJsPo8hNg,XY.&EpD^EdRU9:rG4auqH`P(8CCV=D@,27@^HinB!!(t';#d+MHe.q/_>Eu;kFT"DgK^sjJP6nFptu83qcs!j=&L[(g"H?$pQrFd4Cm6WERt!`n%W4Ql-W!C?+=k4huE\"H2\^>C=rLEgV:#rZ7Mk:nAB]F+g]@nXK6<q]#D\/8f'-d]!U7`naBi,`4(5u3U`V,K4Of7Hge(B]=+dIABh^bb'/h-\aLp)<_B[\-(!E%^)&S0ASN=-9=b&4bX2@1+IpN+l_:"9bG<8SoB5c<oJ6C"U.#H:[VI$Bg&I^pD2O4.Uoe/cD-_'bX]dT/[O]Ekfi2kh=r0KLRPjD/<5/4o/(r5fDnN@1XV^s6=no3#qQIiU7%%2u`Por>&?UG_J'7fg=f0MV5(=&?VQrGY0*$OEr97",P!O?ak0<T<lOT,1#7n8p8.kW'J(:Dj)_#%L-b8s)CN$cF(Ah]=:/6KN?VN#\jSo,OgM_LYNZ:&W-C[6tn:N8nY?kK]`V-1NP:'GAn?n^JTD!2CEUacF>g]?=PN0]Eq``cd?[l?Z3b'`m/G*6*\om=>X]h"%5CouPo)3<gp#kHY4aP3L"+?6Pins%YP1&["A9)Pe0%oscg\n=j$<!bl=IJ+EgS')t!"?]40]$fi*H/U\`e"@%I(t0\ZY.UcX=S/?^6kpr["^&2XSCRlccQ,eXB6W]`f1obpQ:?'4aZlIO$"V/fr.cCJ%'p(`+:mIO!"DeX09ZN4";`FGJF$G[7P[7-G76O&!r-TW2-;f#5\8N\euM/?,(i:BcPeJq1&;8mCRK),+GCRX:Jp@:7Wl(`1T6^`>;l4GCP"TIj@$Vh`pD^]6B[TV5L67qQ$d1._**?[stOK+'j0RQ*TQ*B:hmj8s5%>rF,Iib>aW=Vd`A\dCOJd0'+5V!"`b<8Q/MbA9q7lkP$p,H01NL%3%c)7+A8=chI/m8kM=.Q7Ak;[AlC3Q*&K%Fm@J5M2Ae5`Z*O4?$gWAf)78,J#-AG.&C%BH"@I9o3_CC>j9K6!s[8V=YhZ_m^CkfgUVC'e6/ID'kB@TpYC$t:F5b"T!dE&'GVA)e&p`WG$aH*(1tj^)/+N$_TL'Fle]=eggB@8iJ/M`It,AURZ(%00Hms<J%'o]%mTtg_N[UOj5]5aJH,(V"=[9+8A^V3<qtqJDpZ6!!8n>nHKh8`'eA!FiroD_J5C=dr:s^ul;t]LqZ[$#0TDg$U+ZUFjqk2EkJW2joB4FA1h]^>$5MEGpKa\@#64`H+P%WLb9_913`:d<r`kqSORU:TrmBtY!<<**`j'$or-oL"ilI6,:@B2'zzKSe^Ji^8J2'c%R6%fcTuOc`ngF5MHm'MLt+<hN)s!!#8)19*EA/9"0_s$!D%0;m^;!<Bp]_O=8cq=/M[(-,n?pp[d7!;+0RXp^7+!.[.=4P'bW$NRs_0V7T^iroG@%N*N7&`e2-5C`[VR5:``oLR8jG5.+^!4W@3<u6FJ=&DiBJHH/J+C%IkP.;h#!!'M2qH"Kp*j=S_igi/Tmb+qEYd!WXacfKG%UG)?/3-AWOuZm>#CugkqH"Kp*\SrGR?@ICj,Zt&Pm<GY-$)Z7koBd)XjFho#h$J05r^$/!;Mu/^Ug(f%=u_(i+GCVlGA2PHf'8GFmIWG(OSOUE:Mr,5P%J%SN-\'!W#k6QeoId7fD_@rU1BnnE-3Y*^4uH06O`:qA0-:$N7"5qY0/?rrR]Wd(d.G]RQb.IK*TTrVQ>=o[-12>ITWON3$<sp8Ci`cCR1'mbQ0Z4#_-:^rnCG4?>Ma/M6@dQX962cJ\Z5E8e\+oQ5@dp$1'`1,;Up3$Be:qs:XnDnWPJN>h?Wna5)?s5uWHC#@gWIm<u2o,^&@<3'%O0eoqKf<2D,f(rCI<lo7"`l?/u)fE1E^juT!7-S%YpmKLD\hLdH$O[?s[V\*>d%E$bq;R;cUr"1!kb7qPI&XcI2/?f9B):CZb4j,J1ZQK7gTY,(^4#n8I.L'Xe6?k)ANa\qr]P;_8EJ!ZVbWd4e[]PO[]HR3j2[5(q@tM]r8fS]&R@ujrql.[:/:fILEEUQf_m=p=']s6oY*Famaf!on"rgK()7S+Rfo_H`ufg/?iTthXIhRum_OM\bNRBh!:Ve<Is#1Es-ETTKB5>snW2+3p.mP<3QB%W=he)Or:B*F6\k[?pV42'KD2QCCaBYmq5H$A=LDnopYL8C\nUoB'X[>s8b9s?`%590]V(o_mNXO^+O<XZ!ecakN*YCtrmc_*rk7X#]DD9jDf=@8AW,of24*t(h\e4^>^PkG1.SKFV+O-D7-iF+XnBO-=6@dG)&Z>Uh7Im'4aZn-A&k\D7un_i_]Sm)\8gR(piq)$MA7X6qcfQVrfn@=V+R#Qp`OdM3HM'6P22.UMX&Wdb=aM%,ZAN/?2>?[SC#%/3,\-@"Uc)67n63@DVDI3c^m:P#7k_"U4p#>U4<3.$1-s-X2g2=pT%[".Ocf\hnFNEo![:jCXoH8OsNbAF*$FLV:MP=RqCL6mb]0TL0SrYFaJR0@:>jP2qaB$'.1)'gK*J'CY,^@,>.t;KG41u*68!VmEhQE_[etJ/c1%s//)%Lo4Qsm$*sK`hu;3@.9"qnmp@$%$`Ber]_LKEZ7MjJ`Z&hulTbFIEs^++Iq??aR$`-GPQ1LFYCGVq8'7p/If[Cmr<nWqIfBlr1oPQ_f),41s5oehXR!'E%NV"1TE!1un(?6!Q+<A@o#i]$*$69;CtZ+"]69/O1IdZ`Z%(B->;q$/p^J0#aL!uU-RUZ)Ep;#KZG'nnQ;[5cMqm9gRr4+?M=L&l++O3n8WtL1giCS&rp^Me26U2k8p^3XTgQ-`jHEAgqWXm_%NJTbI!L\i267=b0:Fm$KF%Dj*68!VPld3WU\%iRmiVA_mI]YlrVQ>m<ioOl6Ge?c4t-)JM>\sEDnGDFUnjfbW2R)1[-?&;>O+4bg=3-&X6Ab\C"S+I(H,sR^qYAQ0>IFj?b_'\`]tU%W)4db=!60;4aQbG47B7LViL,P4E0)cEW6!`&cE(K:?Z&OnaXXAGNmOEL=ngVaRHtI&,:Tk.23I1o^:PIC=T?4'E\RM[(bg7Z$#B`G4"#*0V4`%3bd"fhrg,I=js6BAQ*o$qN<-tm.KPfT-$.<X;$lJ6q0YcmbPK.]$DGqrCJdgF+A3rs14/c9:%j8e5M8@/Ln5ZA\CdRlIN*'gV9:?Fcr$Ueuei1P><)UDVV9o05J6(R%0]%Vb`r?gtW&@(<Ek#euW$*VDW/?<2ro3o&RnJ>`TaY-6aXCqtBF.VMPs68rpaYc9'J1?+Y8sfWa='a)h+/&BGh6SC##g`P6o;pu73rPS>QFhp3lc]Tmh]fW`3'q>U9[=0,NonDV9j4EKZ/`5KTgU*=C%W)9=#_J:atm+J^dkK`cW[qX;\VUZuhP@'aR:73lV>$kB6-kg.Umbk_Bg&=-Zn6hOPnGd'YnDK#E!R-,oFXq&"\%&sC2o#0Job3.qrB"Z;S:lOuqeg&mSs>/79Ts%uDr8:CJ"p8ZEhtmroj-'np$2@uTZ<$2f=ZBM=kL?1YM<d^k0<Usp?gTF)`B8BY'RO+-1*kW@K/VRb]k:P!-=f_h+F+7SM4J=LCP?+DpA(Nn+cG5e'cZl+./:WA6g''pE)MBYe>M+NZI\\1-Wm%6q"ukq=EuJ=]nn8C=Od_rENOoj87R@4aQa^ag1s7SN_Juq[4B)FNBsMCDp^n&5MFr\)Q/W)`MY.:/85]<A$#$Ikrl:/,U+Xf5\kO+IpfnDT0)7V/7_`XB=]gZK\[</s@&'EUNG]9:Z$R4F-K6a9!t'8Xo\_#TNE>>Oh+<Dnl6o]+9&Mef?V_eBXUH9.jJ!,njhTT$*HR06@*BR58KW9qt4>p:^I*9:%9><nk'"IBUD3iIr)Lol:?;j6NnoSNA4r-@>a`@Xrq18R!s:jIP->!)Gl[\r>[iO)HX>&st_&W\!pP9^DTOF%PWjO'8u.\UOMReZ2b1C!ss!r_[*4@<-Cob^snWF0i3Z@q0"s^OK4nEb4Lgr;?K:bfhj"5EZ?G]C3JGD;-t:56&[\5:S,\BDAP8Q?h.4#UPY<<PISo]*nZN$Kti`Z"hgh14NB=n%S\Ago!jG().HRkJkK[`Q-"0(bhW<hqpsr8WdKTI?L+jS@#Z1aN2G7c'nJ.re;,r4=VR#A(Rkh(Z"7>a%N#HWaH)CP.(>$ec67rk[/;!i5b0=\T:Nl%Qsh73s1-RqA1-,iroE(K`2.qiHPBR#GHX`EB%8)i'HR6E#alS!-f(O(EA4j"TTTsrWU@$GQE="Tmsmu3*lUC60MZp*0(g8GS']f*/EPX!$6gm"ioSH"le^X4AC9Y!^I(j$O4E:$3^;8i*osel=N01"ii,)oIk`QETO6jnQZkMY<^IL!%u9VYAV@$?#[FX62q"r(g"G_):*HAnHoEt$fGK!q<DQSf*02F&G.W+MfIjZS+0`"*3VWS^Aon+Q*5)FF'Wat!^Zhf\/8;]\f&h:_=7j8r<qOm>l!Ys4P'bW.hM>+J;\F/X./Wa(EBVDd(]Q5`g*`F`cGF)]E1D!!k9(K^:G3.Fi!ST"T8_d@U/`oAj"ZKa"%_n07\t#jf=m@>SeB`5g[7G0#j0[DT6a'E4c6<rrT\I=2)6`cZR(o$3^;8i*osel=N01"ii,)oIk`QETO6jnQZkMY<^IL!%u9VYAV@$?#[FX62q"r(g"G_):*HAnHoEt$fGK!q<DQSf*02F&G.W+MfIjZS+0`"*3VWS^Aon+Q*5)FF'Wat!^Zhf\/8;]\f&h:_=7j8r<qOm>l!Ys4P'bW.hM>+J;\F/X./Wa(EBVDd(]Q5`g*`F`cGF)]E1D!!k9(K^:M!?Sq$\.a'H8uWDf^T,d;O`@Vb(n1M<;(ied="o[qeug!mKLhnt3[gCG_].-TL!GL`]nX&Z2YE8\Ola,euE[VjpTY$JZ<W2S4qe4GiNq$Q6Ej>:`-<&VkI/m5Ku?@VtPq6=GMeui:?3]];H,@gK9h&"m[0-@CUd(]Q5`g*`F09>9hQS*^OWM^L`>3+GQKnY5ihKnKlDnWQ"kK^JNf3W[WFhdqKYO.*elI;e%M\bX0Xo%1C:Du00goFL0CV9k<S=JB4:S6Ftg]\'$`5KU<RPi:BZt@RO8sLl#Sif"d05o(%0bG!0gUD+12GK]$;0r/2GM\JMmb5'a_hJUrSN7L5T=oWbJj]P>\/8;]\f&fthu54FB:jT!>7Vc;.*GM]ZFjV`#&D]bn%W57@RY'U4EKZ/`5KTgU*=C%W)9=#_J:atm+J^dkK`c7[V=4QJHm5E.@pCZb]h3o.Frh0I@23NG6d#*N`R@e?N:o&b#^O^GtHB^rSW;=Uh$TTk006WOH<jg3d^S%9Pe;:/B`J=p?gG5S%WOtb*DOfa,V1(ja-Ub9q+&chDB-*\E;&*bJN,LHf[G\2fH,`[;XrOfW\f#GEq2'#l5@Q`OPKhc$3P-\:7;F3d'htfCnXggU]Cor:mY+1uCA?,pg[W<u*i=kV.8N@!hj4*BW=BA\K_=&J5WHkKgUrAS#H'W`?*qoQ+`Wc]KGBlqKWhoT72#s)+>jIJWgK*'/1tj&I`(07\t#jf=mM/IBe=B?m%Ch=#gEZ=QZj*2iWl1GcAS:\"5M.Mqpt^r<hSeP6(0Ph3ZJ^3AFZBCk91^]!k1H_jZ55;W>Q'^/m((ZWY>2uQt?f=tA^2J^n1l`\(s]pj0Ce]*RfXddM#0/%DFPs!FI1j\e-hs7JWFQm7!8_^[jIBUD3iIr)Lol:>Jrp&Z3rrT\I=2)8Z4J!WZIXH2cP/,SH;e=qG_1U10?+Y8S9hjHLA]pDVYV0h\e>ZBiQ7^^l?b^p[l6]5of\\nC/1aIi:6o%%DJj?8G'7\<XRGnE.T`c(4e&(9T:)*Mk5.Y(TmlalGIMGD-IZVAJ+RfTIfWf5.pq+=*m[oi>#j_RWqY=nhu<TO/,1:^ZlW+A:`u;J(Ddl0+$X/KJ9$-h,:+X)MUbcV-RU8n/M/_5;WcL.^V:r2/W\&qaOmn9f1#GebJu+moIk`QETO6jnQZkMY<^IL!%u9VYAV@$?#[FX62q"r(g"G_):*HAnHoEt$XUWTkIE*K1S3oaitVjGN_A:Z4F<[3dP^'q/i[0gnEdXfT#.hs\LUrAT'M!g'aP&b--m'X&:Q`gZFSC@j1-d%?@ec!_$bM]A94S_`cGF)]E1D!Jf!qCalZ4IN9@e1GQi]u"Y\fePKfAq):*HAnHoEt)h'nbgd+&l\LUrAT'M!g'aXXpr-6f&K6T:q=1W!+f*02F&G0o9nZ6^8nSI?jFNo'5$]\:2khkmi=lU"Og!!adRdjW!*3VWS^Z!5>`Z<"(RCgm)j1-d%?@ec!_iJGi4,@N*GGT)pF'Wat!^Zj@+3tjL&C4tV9%T$b]&7f:K)Q3DSi,pYT$?&UXr5Q._=7j8r>U2JCYV3<S%1%T1S3oaitVjr2uIpO\:]#aF'#qb4P'bW.hM>kq!kG*#OuWmY&qp5Xp^7k+R%_Pj&tCPin;O]l'h-I(EBVDd(eP[Zc4$(Z^JGR2<25"3*q0/J&@=[NB;r/1O#[2`cGF)]E1D!L*+_[G7`)4mRl)ikITSr"bQ^_58XpGCGL*tCW:F$E4c6<s+VVSH`DRd4l!2&dhpDR_=7kcB?);'_76Wd;LP.Knk<OUf*02F&=so/*!G<5C.S#Cm`I>FnQZkMY<[>U>]LC7l`),YkIE+h1raTBE4c82fl>,"0WqI2<1i?V4OY.nFi!STKP/=O#JmpQ)H)!T]CJX*4P'bW.hJ9mQQ-.g\nXe/H1&g2NU^6i*3VVPG$K&!N9dJee`XIncSWX_?#[FXiRr4m5qoIBL/af.Xut/McZR(o$A<'4,mV8rXr:2dT"FPe@m&?Ha"%^m*L@`Kj18trom,uJ1QCp0(EBVDG]m>4&<,qT+k>bd/'g&lF'Wat!o_gPa[%V`X+jTGkH6qr(sd>EdGr3-kd>EP5qoIBL/af.Xut1GgqOq\r,J&U?0]q=,7,o-]\$P?rquSsA&m0'>rt).3[5kZp[6l2VLjDOhS"8;X]i,9[;#dK%Qo4<\@lVBeue>n[>Tp"8ki$N1hi,V:ejb\e##iN;gNcdOsNaR748`>D]h/(/ia3WFk)T=oA#UDR[<nql-cD(4n[=!7>o=\n&1%rV-`&)k8gPOY$JXtCtS7,WDhBF:7XGRB[L;F\ojWmnk%l]NI1?`AZ_:!PEV4-o]_R/S%E(EJs_pm=gM^CS(=ZD;lNZkace+8jlMI>1F=:i@f,ld(DR8[r>Q$gN_9k6EEtGtqE/u`RHEW1[INWkR?PdHFNTgsf$2(K6CG;TV96(XW8)I3L(2_2pYUJ8@`=4[%j.,!R58J&-70KC'j7"qN>a)V?>I)ac^p[hGIFp5Zt]-I!tPMN$3L/F#g\N(%YA\+25L(2GNG5^s-ESiJ+rBe.lTD*hJ@+IhnK&n@DcVacC?m$#mp>%ZY.V,`)`b]ZJ;p-heq8]fpc$F>U`Hhm+=$qY#hg`Zt\"U:#j6QCYl^LE1o%W5(3;)$QM3LM#JtBc]1X.in;P@8s?f"d(^kkmGBd:f#Z)\7ul5+Hh?MFF')Q^5CRu`>+4m^PEM)>jspf[3cl3$Gd+l1a,V22,Vi_mjbS_Kq=Ee(S$K?*C=T=!>c8(!R@+e=$N9:OB?);'nSI>[V9.LLkhd$5qj7-IV;U$Ha[&%Y\Pd,s2\#Vm`;%ZNIGsY0Jai$9j<1&=>cBQJRg/gq;Vq(J0>-e+F]4GPF8aY/N>nuobEjk8F8aXW1M<d`UIUC=^&G$)QS,^IgU:sIYJ2Pg-j]H`+?V_<3J_<(="6r,VW$Y94b!>(k#*IUWDf_;_SLj091lr7,kKf8hgK_'[V[e#/bAq9Q7Z=D?b_'_lIAUt1pOfF<*37iS=D.:i8!,Cm-O)kfN@&"+'#IK=aDtj5<gq[Zcr0)!XT$^rXFNna!Cu\D!"?dakS_cgqTmjXX\Btds^uH=*!840r.TV6CO%!Z"qrDq]Q+H"k'r$EH1O=nH/t(./3Hp;GpDg=FGmDEcTUVdNTn\Rl,!Ds8*bs$3c%\%>8B7,mWsVk";Jlgbc\!^D`eq"6%pQa[&%Y\Pd,s2\#Ub(\DVg4?G/ULhAMF.S]'&63$5%rXFNna!Cu\D!"?dakS`:?X;$nJai$9j<1&=>cBQJRg/hl$i2dnSfO.;`3Yj^PpUS$TRV)Mrs$?Hj-#%>2KP_CA8W=XY:f%r5^7HWEY-Q/Y#Lk69nWGq"oTAGc]1X.in;P@8s?f"`p$i9d(b/$m'u+!T$:N>1mshFSGq%Br>Q$gN_9lAg!#^RPIYDIl!I^5hS2[Z<KBc_in;P@8s?f"`p$i9d(]c'G;aLInSI>[V9.LLj7KN-k[+FO4.Ed5GW'W>dU[6aEV``QoWNhcS^#qU4I`lZBehWl3;olcqU\!mc=ZJeT$:N>1mshFSGq%BrG/'GB/=`CcXs=ZR`nJ^:B0U2r\`'4Z^En2B/<U=:$/;jVgihTru,S+futJTZ^E>/-?IXpe%`JeIgn7&Cuu5eg!#^RPIYDIl!I`C^BPU#2KK,nD!"?dakS`5oWKn2?NZ9LS%4/r2KP_CA8[iUqH"L*YCX]ac.kTIS%2F21,mH;rMp>&f0JBAB'pe5c!75)R@@fYrm_2NCS5Z[1NsDVB!,+%bY9lgs6>482:/k>R6-4;1KQ&#k,'#oIs"'W)JIpZbT4X.R4FOLF46)s^Ug(f%5dKhjppD(b`tea\S9(J?X;$n"r\k1105Vnk,'#oIs"'W)JGYba]sNVR@@fYrm_2NCS5Z&-Z^$NA8[iUqH"L*YCX^8SBOu(PIYDIl!I`C^BPT(F#$f?:$/;jVgihTru,R@bfO+D1mshFSGq%BrG/'G1+.,XdU[6aEV``QoWNhcadgRT8<^Su`p$i9d(]c'-Z_%E+\h*r)5ht,62qj:SBT)[Kf#3eAt8kN"oW50EAAn_("-W1RW81*(\Fn^`lXap<Ci=aBu]CEJ'OTH2eL:$b:gUm3]aiLUINQnqs=.MkH.Ia0Qug8g9k_>VMpImh7ImcO")P+khWjpeZ)WM<NA#si5"BnO5$/4DVa$L?+[&6W79>H8WtLsC?=O;1i:rG2Pm=L03PUAUAIF[)JGYba]sNVR@@fYrq0q%q!g4VeG.eUrjI=Vre1^r`l5p1r:8%^VMPs69"ht1c9"q'?+Y8sfWa>6;l?]kSN:n.dA"UkFmD6ejHITC)u&XJb??RP-7:28l-h"=2g<)i"Yh_dZY%If2Q^,gVGa6_P:'/ObKH*hO,/VE^3t@tIA;9$1f!1DY!ST$9f#QEqU;urB:6Qh2t+KXDJ@F[d`J]:I&BZ+@]_:7/<kF\<2qKBJP(k?2JfTU^&S*/'e<<II/'osc^[!O0eor3W6![LC=FZ"n64!uHhJ.*>Gt8c2@hq>5k'2sIgn50k%(V]R`nJ^:B1/8oA9`])XQ1c9:!&'fi"kOXZ,-b;jR1Gh()WFhRqOUBD_DiLCG1L$ZkIS8OuCVf:P3Q<[P85(+k^2^4#m%ldj>;ZuY8#H'2[XNlc=THM+.0Rq?RW2f;mlk0<l!kOe#fhd><gTgSIUBu\8bbZ.2t\S9(J?[mrkMe/2('A1Hm/'Ir?9UK4i?aF@(A9HtS&c8._HM!:ZCe8>PVkp6c5"SXMP0A;bTgOSFK*MpqfC.@s6_6^.bH`*"&M\e7I!\dm<gIj@s8HP@Ale[UoWM-3adgRT8<^Su`p$i[(+`RLR[P`+hmRCV)fNAd]mB@^F6AiKf>#OZ6S6QmUs:rfPIV%bi9Ks<5>o3OB$@Jt553iRemE6aqY]"_rr-]+EAAn_("-W1RW82U+.;V%;cek`'.,PMd\Ps@oiEfZn:J&S67[-Ue]*RfX\I!*0/%DFeX84+XA,H[O7p9dW_8[2[SY_eD7TYg&S]62FiMOnbZ.1==/UQH\aR90;GpDomiTXk[V!dQ:H\0#[;"X=M)*D*pYC&&D;-DDg+qPMC>#oSqXs/UY?if/D1DSaYcofA3E]t1\[f9(g"EX8Z+f*[Wa**?'RG4WIV*1=h`l-RRW1oiEAAn_("0qKl!Jphpu."e_hZjI?f1_YNMi@ob5V*rp%o]VV_[kgaN1TgOgZYd.$2`6Mi3LSNup&`5e4YE+gip3m]2V:PEV;u()\"CXB@?.J&<^L`[,cIadgRT8-`4oIs"'W)JGYba]sNVR@@fYrm_2NCS5Z&-Z^$NA8[iUqH"L*YCX^8SBOu(PIYDIl!I`C^BPT(F#$f?:$/;jVgihTru,R@bfO+D1mshFSGq%BrG/'G1+.,XdU[6aEV``QoWNhcadgRT8<^Su`p$i9d(]c'-Z_%E+\h*r)5ht,62qj:SBT)[Kf#3eAt8kN"oW50EAAn_("-W1RW81*(\Fn^`lXap<Ci=aBu]CE?-p=j)CKS]=!:<#VAZ:]F[?$GBUm;g>_P#&RqA2ieHPneTQ)_7Eu!,6DOPSA<u:M4J]>g%bZ.2t\S9(J?X;$n"r\k1105Vnk,'#oIs"'W)JGYba]sNVR@@fYrm_2NCS5Z&-Z^$NA8[iUqH"L*YCX^8SBOu(PIYDIl!I`C^BPT(F#$f?:$/;jVgihTru,R@bfO+D1mshFSGq%BrG/'G1+.,XdU[6aEV``QoWNhcadgRT8<^Su`p$i9d(]c'-Z_%E+\h*r)5ht,62qj:SBT)[Kf#3eAt8kN"oW50EAAn_("-W1RW81*(\Fn^`lXap<Ci=aBu]CE?-p=j)CKS]=!:<#VAZ:]F[?$GBUm;g>_P#&RqA2ieHPneTQ)_7Eu!,6DOPSA<u:M4J]>g%bZ.2t\S9(J?X;$n"r\k1105Vnk,'#oIs"'W)JGYba]sNVR@@fYrm_2NCS5Z&-Z^$NA8[iUqH"L*YCX^8SBOu(PIYDIl!I`C^BPT(F#$f?:$/;jVgihTru,R@bfO+D1mshFSGq%BrG/'G1+.,XdU[6aEVd.L^A-]G5(39,Y?j@Q3]]DeID^dKf)#"$c]9).[W_&lZ9BKXA,#EIp-T/o$@;I7?+"FtC=T<n*'!H_n`0>?X]r9CCY(0g.Om;5Fjd'W^-Xh?3DGu$Pte!YABC!mmMPn<TQ)_7Eu!,6DOPSAf:QQnN\tLsTR["@ldr>t[r:0pjid\sYCB]E-;L'Zp%lrqmms9umFnsV_hP'aS[(.rlI;e%M\bX0Xo%1C:Du00goFL0CV=8JS=D-ISsu4]@)7G^R$_$_Am(VSXBAK$;PhL!-g0UQN>XN!+sLLnp?f]jNW(f^315u@$>D7SbrG[fc_s^9A@h'@?7*EBo:7XJMKQ80YYaJa;Q6rQ"Y)7\e)^8\J%oI3MMD]aQK=MjB-nrP()7S;:G*;eCUn*(St.re[\(</Rl1d>n3HJkUON_0:?H)i6CQ+m1J[uHF4F8lN>OCEHhZr"8P.N=F3X()Z`WnIp[4<J2K.Gghm7b.p;;UoPD)#<js22tf^$kT4Y%3#7>hr'Eo4TZ]63FODd'\lV*'T.Z":IP]mA/)9sZd<)`OpFoB*m`NW(f^315u@$>D7SbrG[f:K/d<7uuN6C"'.dZ_F4ikK]W&;YkC<A9HtS&c8._HM!:ZX=T;P5<^YNc1af*]4L+T$QV9M(d*&I]me"^*V(I2?^Bf0LktlukM)K3ZEe5qWo^u[Vb`rarW7ib*MS$X1J[(CY!ST$9f#QEqBNE#[V]7Gjs,pLErG[_QT3U3R@0HXIL?$hP]8H+Eu!,6DOPSAf:Vp4led3lrUrsbdCOJdQ8nX71\9L#9/A3"1J[(CY!P1Z3rSA?Y?nniYJ1u1@Y?TZ[][AkiIr)Lob(Grl`W3agh:P.^3k3ol`\U(3FcnE4aOL(oiGG1FKpNfL$Po^O_.irB:R2f>_R9=Fo'^^cT_6-EokI(a,f,aB@"59o3u3>hgG#iCX^4Y'4md4m^qq*gU9gg[:R(%e?WZ/p$:55?G3pAP:$"tNsZ7&PEV2A(7^k`S"6*%cfY-',8$)*cd0q`bfo/F7nuW3/u^X%k%(V]3k.6L>;#BXpKi/l\kI%G]^e\lqWF"L^V:q?.[r1K13W'=AT'%H.$2`6Mi3LSNup&`5e4YEOckkO%IPa2PEV2rQ51>UfrS=4IXf6aBuVHA`lXap<Cd?rVgihTru,R@bfO+D1mshFSGq%BrG/'G1+.,XdU[6aEV``QoWNhcadgRT8<^Su`p$i9d(]c'-Z_%E+\h*r)5ht,62qj:SBT)[Kf#3eAt8kN"oW50EAAn_("-W1RW81*(\Fn^`lXap<Ci=aBu]CE?-p=j)CKS]=!:<#VAZ:]F[?$GBUm;g>_P#&RqA2ieHPneTQ)_7Eu!,6DOPSA<u:M4J]>g%bZ.2t\S9(J?X;$n"r\k1105Vnk,'#oIs"'W)JGYba]sNVR@@fYrm_2NCS5Z&-Z^$NA8[iUqH"L*YCX^8SBOu(PIYDIl!I`C^BPT(F#$f?:$/;jVgihTru,R@bfO+D1mshFSGq%BrG/'G1+.,XdU[6aEV``QoWNhcadgRT8<^Su`p$i9d(]c'-Z_%E+\h*r)5ht,62qj:SBT)[Kf#3eAt8kN"oW50EAAn_("-W1RW81*(\Fn^`lXap<Ci=aBu]CE?-p=j)CKS]=!:<#VAZ:]F[?$GBUm;g>_P#&RqA2ieHPneTQ)_7Eu!,6DOPSA<u:M4J]>g%bZ.2t\S9(J?X;$n"r\k1105Vnk,'#oIs"'W)JGYba]sNVR@@fYrm_2NCS5Z&-Z^$NA8[iUqH"L*YCX^8SBOu(PIYDIl!I`C^BPT(F#$f?:$/;jVgihTru,R@bfO+D1mshFSGq%BrG/'G1+.,XdU[6aEV``QoWNhcadgRT8<^Su`p$i9d(]c'-Z_%E+\h*r)5ht,62qj:SBT)[Kf#3eAt8kN"oW50EAAn_("-W1RW81*(\Fn^`lXap<Ci=aBu]Dpi&q&;o9.GhiPVdeVbWe/kb3d3=0Gr,8sR$W^AI-AU\6h]*^"\f*I#H?`$?WT4oY6:B@!0W2m$8kWb'A>Ocbc->]IaKYJ9N&r%hs/B[ED<<)h@if3Pk7rVDH?&,g::EAAn_("-W1RW80_#bQP=g#\Q-hRn,T]75SE),dYE]bN%uCtuPjqWk?"iJ&;Qp#jVk92A?up"!>+Dr/-Kl-c"XCQ9H!g=ioPhnIaqfLI$31bUM#qXj$;.TKZ\Cmh[[4$-Y=q6eg\gTb8/TL"2C&e_qTlDrjT7/n0=SBT)[Kf#3eAt8i0`:;m.Rl"jM_$;(;I.PT`rtZ@c7RTWq>e&.l6[jKu7(7@,>$70:^$N&Jm-4#3rq*,`q"USX,$BBSM'n`LCM#fu4nm`QgMOE&b;/6$3cq=.>[1N=[r,K>D?o&LqU\!mA5;8:UrhCKj7I74qXs/5@`/OTYkqM!]opoihgK.pVgeU@;l<BUKs>..[V4(*?8$A+\`]haRq2>lRr5O0c[sAaV4(s]g0*RTM\e&?<UAGIC[f$3Ign50k%(V]R`nJ^:B-X'JcuP1e'h*`3`Y1g^[pU4Mi<Ve]j'gRX^uf/$\JWmlI`A:^&H%oJ!<oD;Cp!HM4QD0K"Ym?OsREujN3U@%j.*k:I"g-hDB968=:(en'(%FVn^!^TgOV!?djst3LQq0-?IXpe%bm0rqY`bC=V%LFfTZ6nc$eWbcL;4k[1*DP<#&3O=#%tN75Q@/7,tk19#?r=ftl3U+C]eiJ3G@rlk:!eC':KlAUoeRB)DZY"o#no>;)'D"jW'X&euOH`nu&f3c5ip=n0SG4+-gk5$V*lK)0TAkC*:DjHVoJ!HUsDEiEM?G1`i^O#\6LTT[tio62e'X#2k7%/*eST-D4Eu!->e`ktL@XnA^;Yp>:RIdRd\T6`MNGV=nem%#rlI`B%Q7Z=,/R+O*PZ6fB7jCNabg4V]b/u4*rM-<iW4@!4k2k\6*]uD*+i3m\`T9Gd)JGYba]sNVR@@fYrk0_orG2B27Ufi$<9db5h=]lr?eT]cE8\P!\)/5?'2u5Lc,UlPXN^-O>]\.IOcbcc)Ds#9q]""^:R=D79emMk[r1"jGD?)Sco@GW\I*%+2pF>\WsedU5iZBMjsm1Kgbe'`Y:f$G"-&L)RB)DrF46)s^Ug(f%5^iBA?J7fbY9lgs6>482:+=NPIV$71,mH;rMp>&f0JA,:?F%%akS`5oWKn2?NZ:O3LQq0-?IXpe%`JeIgn50k%(V]R`nJ^:B0U2r\`%^R?P/gBehWl3;olcqU\!mA5;8:UrhCKj7KN-k[+FOP<#&3O=#%tN75PQTRRD-:?H)i6CQ+m1J[u8K)RXS315u@$>D7SbrG[&$i8I?j((pI.\t2A1un530'HXENKZAjX,nZLdu;bj\tFN^1f!1DY!ST$9f#QEl@T!mco@GW\I*%+2pF>\WsedU5iZBMjsm1Kgbe'`Y:f$G"-&L)RB)DrF46)s^Ug(f%5^iBA?J7fbY9lgs6>482:+=NPIV$71,mH;rMp>&f0JA,:?F%%akS`5oWKn2?NZ:O3LQq0-?IXpe%`JeIgn50k%(V]R`nJ^:B0U2r\`%^R?P/gBehWl3;olcqU\!mA5;8:UrhCKj7KN-k[+FOP<#&3O=#%tN75PQTRRD-:?H)i6CQ+m1J[u8K)RXS315u@$>D7SbrG[&$i8I?j((pI.\t2A1un530'HXENKZAjX,nZLdu;bj\tFN^1f!1DY!ST$9f#QEl@T!mco@GW\I*%+2pF>\WsedU5iZBMjsm1Kgbe'`Y:f$G"-&L)RB)DrF46)s^Ug(f%5^iBA?J7fbY9lgs6>482:+=NPIV$71,mH;rMp>&f0JA,:?F%%akS`5oWKn2?NZ:O3LQq0-?IXpe%`JeIgn50k%(V]R`nJ^:B-X'KF&;q[;$Em(RLANe?)raSND&LRnfM8p!?&:<Nf`B8sLlMhgP7HUIX]G#7hlgj,Q31@UeCqO$EX=fs>>Y9rh<sC>#n(A7Xa9='Y:ll`^2#c'U5\6%]A=;,PqNGMW,gl\"XCrG/'G1+.,XdU[6aEVd.LDS,9oC!#/cgaua=8>Xn@?iS'$'@s$(6\a*k=)Z'8'cT)L]`.cnDn+a1C]1ebkL.i.DnYhpc'pXF?+RCTc'kCtO1$t6gUAgo2YPNg+3i!PRm)7HV+R#=/R)h(b0J:u;L\#@f66N50keq,KS5#^^A9>#NmYTG)CKS]=!:<#VAZ:]0?VmRIk;7\r:.h5>Zb)F&J;9eiBh<YIX]"I>.\s[/5,/4G1gI#FSJX]qGVchHr3b'/3i[C'?ut'Z_-`YHg[#F\!CUN4*9[_CUpsb\8L-6cThGWOI`(p%5^iBA?J7fbY9lgs'CpQHhKuugUD*fAXW87noru>B5a'7n^OO:ARXrXYq9.dYh.'g=73V5djA53l)'r)%j,\-P1d!;ZReX/b?T;^il1VgIf9+cmFntl%\,_2qYLX?3cp2W29e+KPIV$71,mH;r>Q`EPBQBLq8O@lZu40Y2_QpAde=6R>ITp_XXjl!1oOp)NqW;kJ,E/qPtA532eg]N_Lr6hVBnm$aJP.K:ED]I=;F+!oM^+caH7]Y7U0OI1,C^:r"T"Z.-"i5jsm1Kgbe'`DfCF>):G0D".=:82]_-ik.dUUAmA5HSBT)[Klh9VqDS[ILCR$Q:#39sT=r7,c_#XEn%T&F-Z_%E+\h*r)5hu-jo"rG;SI;>f%n#d\*nY/qQ%*_IbVHEF6Ch(f3d340@-(E91i6^XsKpr?_sZugJ?7u="iIsY9&/f@;EE`\2Yc\L$PqZ0?q#&c8_5U\I.TZlL]#*4S[ieeZ)XHkbF3Q]YMXQ-`p*1L('2)n&gnG2f@F'[M3Mjp&1YG*BS/lZ=V^pAP#HbA\NXNEob=>r;<XtX>8ZtMMd:[4*PRK?JaVVK+ek1I(rME&,g::EAAn_("-W1RW825>UoMWs(I/[q\tBKM,L<sh0#&`Zi-g)#7fO<qs?*m0f<atI-e,Yb5D7Y2:l(H)&_,+;55n]^G105F=D=b,]hpeldc6s1&BKej((pI.\t2A1un530'HXENKZAjX,nZLdu;bj\tFN^1f!1DY!ST$9f#QEl@T!mco@GW\I*%+2pF>\WsedU5iZBMjsm1Kgbe'`Y:f$G"-&L)RB)DrF46)s^Ug(f%5^iBA?J7fbY9lgs6>482:+=NPIV$71,mH;rMp>&f0JA,:?F%%akS`5oWKn2?NZ:O3LQq0-?IXpe%`JeIgn50k%(V]R`nJ^:B0U2r\`%^R?P/gBehWl3;olcqU\!mA5;8:UrhCKj7KN-k[+FOP<#&3O=#%tN75PQTRRD-:?H)i6CQ+m1J[u8K)RXS315u@$>D7SbrG[&$i8I?j((pI.\t2A1un530'HXENKZAjX,nZLdu;bj\tFN^1f!1DY!ST$9f#QEl@T!mco@GW\I*%+2pF>\WsedU5iZBMjsm1Kgbe'`Y:f$G"-&L)RB)DrF46)s^Ug(f%5^iBA?J7fbY9lgs6>482:+=NPIV$71,mH;rMp>&f0JA,:?F%%akS`5oWKn2?NZ:O3LQq0-?IXpe%`JeIgn50k%(V]R`nJ^:B0U2r\`%^R?P/gBehWl3;olcqU\!mA5;8:UrhCKj7KN-k[+FOP<#&3O=#%tN75PQTRRD-:?H)i6CQ+m1J[u8K)RXS315u@$>D7SbrG[&$i8I?j((pI.\t2A1un530'HXENKZAjX,nZLdu;bj\tFN^1f!1DY!ST$9f#QEl@T!mco@GW\I*%+2pF>\WsedU5iZBMjsm1Kgbe'`Y:f$G"-&L)RB)DrF46)s^Ug(f%5^iBA?J7fbY9lgs6>482:+=NPIV$71,mH;rTcF!B$=r8TKuOVe90AK[sn;Pi-2&J%5^iBA?J7fbY9lgs#NerG'7D,jD1s4[U[@<6%]@f,U@d1eQ5'):]D>HSBT)[Kf#3eAt8jC(7Fb(*BM\R/hScZ>IX%lcT`N4rq*CF`lXap<Ci=aBu]CE#:V,PW`5tGil-iC2?!CEle8a^RCf"RIJWgKI/`s)EkDl&3qRh#lA`m\3WB8/g.Oi5oZpJaF#$f?:$/;jQ]YOmo>8A]n2:gT+,:@KQLI-;oM^+]aH7]Y7U5(nR@0HX!!'fo8.13^H/S_4`;]G#s*j^uGC0)PQ1mrOfc)s;m8f>5F6N+j['T&SP]g*`S'oT^l]mcu>TR*S>TDV_JcFaC5C`Y9q;/YH2gA9T=`nXT40.dDBkPu0.1JK`RI*39nAEh_]F.>'8I9kEe^)1'-S"/>Vkfk`pSP=(lJ@b[T20WRG#VGjgpqLto'utq*h8=RE:iAAd)gS)6tKWoPdId^hNW1ah4)j^M_KSUo_S7H7n,tj>^5)9FWo9rIJS%tAu>JjUaenSU@(`iB@X[EmB!lXV#^Przzzzz6Ht?#-U.P)A#K&F[%'4%c<ahdDOpPcF68E+Hd]ZYs20U&LL4-\mbYW=r4h\!h07c$SiqGEQnbeWnk+/V2f@Cf#*hH$p!m+o<iiNq*^+gkd\KI\5QCYiD;2q&^3B&&5QCQ*'.6PF4F-MPDnd=Tn"Qsl^]('r?@&mk=h*\1@."O&IkajP*'AJmOduMqCtZ,UZHFj.WIt]Z,=dd,ief6i[r+?Ac5_2E$rEX`5'#`*NAo]rWD]SdmFr^EG-#cJc4ruD91tIC+dEhOq@O;OZ$>eJCH83=1@h&"H*X7<]uOWD4VenQ.8a0L>^=,8!MOu2ES"K?/R5e^(34J;CY#STPa'[p9q15)jP%"WkN1fZ<*GB9.qdT2n`.Oe5%iEG<E6%uOW>"2o!1k^9l8=S8^jP2US=KK04,buBmCNN]mFthgm/$o4!<^6>[6'C))FUZCE`.-g9k]DdL@*O\?6NKOINO2pci4/3on74DacuB+`D:Fo]Ql-*F5^HXg)BVg2HQ"0W]h@#XeW(AnK3T4(ZR!Vk/47>.&*t#7n8obD@]%dZ@)qmG%Ai)fGH:?iPGu4nm%79fLo#`>;n&]2L.iqd3P;*S+V#nY]K)6CkYL^GYXTm"pRUYJ'Y`NZC4<I.L&M/#8V$.P5]:q>%=8qsClembPLRW2QNa4kT`-hqt\Ka,io[P\**mclfSBD`6\DQFtuBrU_>18DV?YFo;#SaZLjq`BM*ZJ,F:HpHg>]Y:2K.[r:0<B?qVJi[:=Bil<-jPkVboZ*Eo_bmUMXnGD,C^2;W54*U,b>q%_O[HpSce]T6I_qM>'_rP[I][_DkPum&FL7E)2W?-e&%FgLWJGAp7:nO\G)`N67^%\U>)'@pOs8MuCT0FEP2i;bCe60D,XXk/#'Zc3l_[eu=eur2\U/@[T+dEgHZtLVd8h$M;V<m*S?V9:0%mI48L/_@#>5d3]A]n5_B%cLHJt"W5(/jCaR$`C%lX(s7iG[GWa^2_tf.M@C'iO:Z$c^Ma4o=s-Dr5QT*AS(gpO:O\U)n&`Gpun979pDqk09Btot,G:%^khq2/Cnf]Z\f#-=tW?Z7PV)CTb2pUsaig[!`IlNU2gE$E8-?Ino3k:Hf*XfM]J5rDfn!=j,]Mo8UujjmD0oe#/W6Jj8g$pp%j\S*S*NcTDm+ZUG(/W?1r6bWt9<A"Pr5%NdkSC2"QA56(P%CUm'&.an=IZ:og\Y<j^E;Yn^C%3Ie3lDq+s7jf1?AKDb)h3-2)qYKN]AYDSQ)DWnMfC.J':X4WT9"/N`B(kTE-)K/\qbMP:rU56>?+Y8NV+Z3mGZ`&/p?]-3b'P^#dr)b]VN=DC$0LC4b(=`-aB<HZ8kT*l+e(eJSWJpU/T3553HIYb1[F8J8pL_r<4\mt*;erpO8RU&4isR_8m<A`q%JLDQt-2Z=-J((LECtYgU?KfJ,<GQ8cM*pDReY[QX5![Isi.tW+jngTfqqQOQMf8ma.Xp(uD%Af7Al!2+hJsf!;dqooNjnFQq6'eC4d<-5774LU2KNO!"Dmb:h$aeG&f#l%6jl`/mUkm;IKT?G64$V-JKFjfcDplC'SG?@2(IHM-Q_*'#dMdNq(!]U(q0^tubOLK(ih[r,K_FBQ;'p#*Z=>IA4r\T?pI-Vd2KGOOD%;l<A6Q'Rc"rGhRr.9#<#>KcTW0+@%>6Ou?0TL"3!M\auII*$uqPbW0sRr0An5!FGDUe)[(n]EAcKR.HKg9g+-,tiX;*'\mW._%p*(A'8E5t9n4T75LD]hAt6p[92%l20qaJ)J+nAQHslUiGG7Tfu>cG3rK$]8dabB#m'aO@oCjo]X]WDnfT??X`8-)%SC8^pIh)m.HO]p"*OmCA#'nITtZLq#9YbAmloRN!R=n@`uuF_hh&FRl"h'4)i_M=39\Yk?d7JP*2"-q9OPF<*Gl#hXf7"?/,2\>S\km@',de]O/F&+dFZ/rMKmT$lNM[itl!Y/BH'@++,clruT!.\(jmLoaLBq44Jb5IH6M\r;X>[Zf!`Yg<dTB%L3I`=LN%6s*C(:c^d-Vr;0DjqHXEb:M%FrbXV'6o?K^Y%Y\';QaLV\QM[Sk?Q?%K#rt(/HiEs=X&c?cE@D<jK$8YgSin#6X`F72gMcIjR<64<hKutP6H>357qU)=4am2!n@ZU/8*7H7=0A'#MCaGDgJ;9]?<7'kjW/9Mdu@^rA&jU4Ir6ZeSEf3b)@lfJ!!$H)W2Sp^\)&#X@`89s72BoaEia'=O`mA.2l3'FpSN$TA.na_1XA%j"-2;6f+=H8HhK:2e0m@=qW=8cmTGDuaN__a)r"Et,OHClDb<*lmS2pgK]1Y15WDkt%_;jS\b4NX]63F?k)V9<;c\YL<tW(omB!lXV4CW\VK>CT^[l%\o5U[s-k%D8TPH6IR$a7]Y;k7ED-NsR$0t<"B@X[EmB!lXV+Vg,SMpdHcp?9VN]M=JfKV^ab?t;G=8Ua(8@q@IS'oT^HW.MOo_1<,baEOfO!RGb\@;Y^F:ILP4*GFL@cGn:;ru9_d8FL@DH<rI4Ynl-BpYfG;qbAsDb:BX4*H#>\``nBLZ(n9%[Nc?Y!F9G-Vfmb_tGalab&:Vormn4=k[#$Db<*lmS2q7M\c9Q07WM!J*bgm:k-==_%Lk:2<\W/^3uW>iqqc48@qmXS'oT^HW/W1-pc5$lIDq&rR5^VS"oCGBkPu0.IFXV;g?bKr+B!2>'9;j]h!K<h4)j<Wd@[u=`jsBd+QW/(NFG7:7WunP]g*`S'oT^HW/X$FU(Rdfuu_uhAdLt4"beWg0&Q:8is*A9kdGE]Qs*DS2c),8(>4$mB!lXV4CV1YQUNA)uouS5q##QQ<.C<V-gudYJ'Xu/8&U.?[:au%3%2F2f9V`Y#P.m['T&SP]g<f.D4m`rWB#lRoZ@XQ+QM!]2"nXs8CjT?)'juq>W7s4;/eY5ASh$Ug9kc9ke7j]d`?Y\l0p=XD?1:S<id/c'*iURl49L>kuoO?a\J._QSIfCSY75mS.D.e\*F!XLa$1k\TCSMjTm!K*Mojb>D,6\[f9%nt&!k&eYh=G+#p#V4CW\VR3\p%[NcoR=d-QSRGi2T!b)WRX3o-!Gbi`QFOFaUg9kc9ke7j]d`?M\q;<mD!%CK)fNAD4$22HrHn"HQ_'e/[*-t4.IF4J2l3'FGH_4@boCi1HM-RJRlC5JccXD!5(,K?/lVMA['T&SP]g*`_%LiJErZ1?#GD8$!!!!A&FuDH,9rsRNufBlpn>ClRr?#TfWZOImSikW['T&SP]g<f.D4m`rW?JUq;p8]fV(ZfhL"_)Ocbb>-;:Yp+$FrBZHFjfg1BR+P]g*`S'm=t4&pmX\cuGFIJ).A(Zg<-CXsaW-&ip,[NmD:M`7K'<VNAtZg-D3G#mG8&p-$Qcp^W%bl7S<m@2k>Dbj,'Ie(T._f3&/;qbAsDb<*lmS.D.eK$2,B/9cue>l[>+92+kn!Y7YG/t!=Y@#%@T.SoaG#mFgda"k>;qbAs#><FsbX!&YHM&12@RQWL\[f9AN\T$rM`7K'<VNAtZg-D3G#mG8&p-$Qd)FS<>HNB!U^ZgSn`csD7bVG=hNW1ah4)j<W]O=7c=ROd!8nua+=L^tqXlMTo[8r"aF`28g;W@6P]g*`S'm=t4,#V!QcoC6G=C/s~>endstream
35
+ endobj
36
+ 7 0 obj
37
+ <<
38
+ /Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 10 0 R /Resources <<
39
+ /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /XObject <<
40
+ /FormXob.63b9415af58491d57051f0533776330f 6 0 R
41
+ >>
42
+ >> /Rotate 0 /Trans <<
43
+
44
+ >>
45
+ /Type /Page
46
+ >>
47
+ endobj
48
+ 8 0 obj
49
+ <<
50
+ /PageMode /UseNone /Pages 10 0 R /Type /Catalog
51
+ >>
52
+ endobj
53
+ 9 0 obj
54
+ <<
55
+ /Author (anonymous) /CreationDate (D:20251121162221+05'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20251121162221+05'00') /Producer (ReportLab PDF Library - www.reportlab.com)
56
+ /Subject (unspecified) /Title (untitled) /Trapped /False
57
+ >>
58
+ endobj
59
+ 10 0 obj
60
+ <<
61
+ /Count 1 /Kids [ 7 0 R ] /Type /Pages
62
+ >>
63
+ endobj
64
+ 11 0 obj
65
+ <<
66
+ /Filter [ /ASCII85Decode /FlateDecode ] /Length 1970
67
+ >>
68
+ stream
69
+ Gatm<9lo&I&A@sBlqA`^[`lVdrb/3b1laXhRjP8f:(P'(,U>)k#o4e_d7f":+$`_eS`CnCp=L!L!;j"tp?dGAg%]^6s0O'JAPP1.%QQ/dQfRqcVtofRAq"1V]B&hb^)OERE8MP!#],BGbU_rK02YBmr^cd!\YW2)mCLqI_[FjgX$i6#;CZ^(f=RdQ-^rBT(;=\^a?l9d(1o?cS_almhj_#<CkUO'@QU'=lfu7.ci#Qo4:HSHr;6Kei<n1KfZfu]bj+<WnDH<s9A24]5,oUla#.4QO&\Z^Q_/ckJ&]g<?%)SkmN,E'GJ#+$6H=7$g[SA[l)P'@%I*]7E;OnRSQM6aM.cWUbe/0b/@GU-?s[k1HgpgTSc=)@e[M'!-%hC/]q!?=J:,[C^I?JTJg=c%XKSdTgN%+cq=ZP[CYr^.IC@7!ojH2SXP!98'97N]+tF!%(:n/S"bC^Sn)0S(!>(#eBZD[rpX+$qbOkoPS;5N%QlDWVnm+1.eF>+P%%6LMn@=\C#8-R%-fOY:(4"fRA2,?`%E=&P3J7o2%hiO[kCq5jZ#`+N]%)<d7dA\Y2Q;$4^ORA^o-Eq1[AV<YGVA>*'VQ8X0:Kka(gBiWYUFa20kQGq[`&bk47!R666_-\80jK;3$pED$&j:][f7L04\4J>4>15o0/-^G`<dae,`/ZeT6]Vsj]81jnG62L6OXI(lR,`#%!STG"Xs=*;&Z5`?G)rg`/`\T9tjf?c$g$1"0?u__:uU6IJH+\>UrUA@LQ*c'UjrC=/c1/;q:`Ehe]o>7GBp<`Ql=J`jTYK"]m5%ZrG3qDtAs]:L.+%?Kon$Y0ebeZ>mIfcsL`K3nd!N$;kRXR7N7K')h)WX?B!q-V315'dMFfa49/E./79_:s!U_*aLW.gbkh*dJ!Rpncm!o#,5?.V8qigl#^&rJtJ)$d6aj-i$nc1dK1YEq;KJj9HgbX/U#&E%OJpo[%Sj#/=._[fa5#6Fk<^_EGcr/c[?0N9Qk1[Tg%;5g*<-C$1/NK.;N*:=mA1!091>>BjTU'hT1Qj<:dO5WZ$`eC5FGXc#T"Q3\B8,FRI.5&!uK</Y*RoRXSB$=U+DX964YK2)n9c%'Id\#F.i&OED>4M\%7S"nf#UPh*tH$B&t*dn"Op3iIXecVbO4Yrcn0WN6iG+/Z_[@IQl^8.JO3Y_P[4r"2"VVCr([3S%jierUQlb2ne$G[7bgX9e!D]E4^0M>a=US>52+'Sgr;%-[Qh[;/qU[E5?bA(&mr^2/j?k3JE!@hUL&a?eA@aF1%\F4oQ(3H=_i\=YKmOpkH1&>O?%bd1q<7^$B+F#A9@aibSgk8MZ]W#Q&-r8N+si(iJ@-_[ts/$\,sRtcoNmP]bOI@r<%+R11l\.'F[o2_7K>3j"Ob=(Ktkt;NK@*_XEU.KPKSNo^.=!b>e<p=\s(lu.!g7ka7;@Z\Xj"6%a:3T4sj&0bOHO'?+55V+$?a1LpS1a"E%=F"N(+>R3.6"W;,c),j!7IWW6*i.ER:^.[\!?dG.fN#8L9E^>Z>'#mr99Seg<N!ao1&HcR_UtHRcjTd"j_A!lJ48;%JukeGqH'RDZnm?A]`Kj:^%A?h?qe74_u]O8ut'ABP8!f::j,A!1*^8MC-hg+0iqqE/J7sEm9AkIHViOQ&R3m=T>hhFR]F7-5&+'XB-b@prN<KK8+2/iBjj7e="`Z=Skjr>q9p+G^h<c9JpSs:(tkqVtu?>AjHWF6XCdDHseuhkUEfQpGpa4(@nG`.tKPB9F6D4@Iut,SoW+0o:eLDJm<#Xlmqd9,Y`P+hV/LrY\5b-B@@qAnocsXeQs^@>luH>2rM0Ja@uP"a^+F&@uY6%#*jA&&&EQmbf:RcXD&$tjQi7uY&g:j)i0aHeG0.LBRE`=C=Z]khTRYP]*N6-]T;](.Lp:C8(W:>o&AdA.e^&NYg?T"^(^!I&Gq<J"d'fb90;XU-ji,`?IGsgq"'(irWb8rND0~>endstream
70
+ endobj
71
+ xref
72
+ 0 12
73
+ 0000000000 65535 f
74
+ 0000000073 00000 n
75
+ 0000000134 00000 n
76
+ 0000000241 00000 n
77
+ 0000000353 00000 n
78
+ 0000000430 00000 n
79
+ 0000000513 00000 n
80
+ 0000023352 00000 n
81
+ 0000023610 00000 n
82
+ 0000023679 00000 n
83
+ 0000023975 00000 n
84
+ 0000024035 00000 n
85
+ trailer
86
+ <<
87
+ /ID
88
+ [<fee8447b877a2a16b220e9231e30aa79><fee8447b877a2a16b220e9231e30aa79>]
89
+ % ReportLab generated PDF document -- digest (http://www.reportlab.com)
90
+
91
+ /Info 9 0 R
92
+ /Root 8 0 R
93
+ /Size 12
94
+ >>
95
+ startxref
96
+ 26097
97
+ %%EOF
data/sales_data_sample.csv ADDED
The diff for this file is too large to render. See raw diff
 
docs/agent-workflow.drawio ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <mxfile host="app.diagrams.net" modified="2025-11-22T00:00:00.000Z" agent="5.0 (Windows)" version="22.0.7" editor="diagramly">
2
+ <diagram id="agentFlow" name="Agent Workflow">
3
+ <mxGraphModel dx="1180" dy="740" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="850" math="0" shadow="0">
4
+ <root>
5
+ <mxCell id="0"/>
6
+ <mxCell id="1" parent="0"/>
7
+ <mxCell id="user" value="User Prompt" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E3F2FD;strokeColor=#1E88E5;fontSize=14;" vertex="1" parent="1">
8
+ <mxGeometry x="60" y="180" width="160" height="60" as="geometry"/>
9
+ </mxCell>
10
+ <mxCell id="orchestrator" value="LangGraph Orchestrator" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E8F5E9;strokeColor=#2E7D32;fontSize=14;" vertex="1" parent="1">
11
+ <mxGeometry x="260" y="180" width="190" height="70" as="geometry"/>
12
+ </mxCell>
13
+ <mxCell id="nl2sql" value="NL2SQL + DB Runner" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF3E0;strokeColor=#FB8C00;fontSize=14;" vertex="1" parent="1">
14
+ <mxGeometry x="500" y="120" width="200" height="70" as="geometry"/>
15
+ </mxCell>
16
+ <mxCell id="analytics" value="Trend &amp; Anomaly Modules" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#F3E5F5;strokeColor=#8E24AA;fontSize=14;" vertex="1" parent="1">
17
+ <mxGeometry x="500" y="240" width="200" height="70" as="geometry"/>
18
+ </mxCell>
19
+ <mxCell id="reports" value="Visualization + PDF Builder" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E0F7FA;strokeColor=#00838F;fontSize=14;" vertex="1" parent="1">
20
+ <mxGeometry x="750" y="180" width="210" height="80" as="geometry"/>
21
+ </mxCell>
22
+ <mxCell id="frontend" value="React Analyst Dashboard" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FCE4EC;strokeColor=#AD1457;fontSize=14;" vertex="1" parent="1">
23
+ <mxGeometry x="1010" y="180" width="210" height="80" as="geometry"/>
24
+ </mxCell>
25
+ <mxCell id="datasets" value="Dataset Upload &amp; Catalog" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFDE7;strokeColor=#FDD835;fontSize=14;" vertex="1" parent="1">
26
+ <mxGeometry x="260" y="60" width="210" height="70" as="geometry"/>
27
+ </mxCell>
28
+ <mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#1E88E5;" edge="1" parent="1" source="user" target="orchestrator">
29
+ <mxGeometry relative="1" as="geometry"/>
30
+ </mxCell>
31
+ <mxCell id="edge2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#2E7D32;" edge="1" parent="1" source="orchestrator" target="nl2sql">
32
+ <mxGeometry relative="1" as="geometry"/>
33
+ </mxCell>
34
+ <mxCell id="edge3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#2E7D32;" edge="1" parent="1" source="orchestrator" target="analytics">
35
+ <mxGeometry relative="1" as="geometry"/>
36
+ </mxCell>
37
+ <mxCell id="edge4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#00838F;" edge="1" parent="1" source="nl2sql" target="reports">
38
+ <mxGeometry relative="1" as="geometry"/>
39
+ </mxCell>
40
+ <mxCell id="edge5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#8E24AA;" edge="1" parent="1" source="analytics" target="reports">
41
+ <mxGeometry relative="1" as="geometry"/>
42
+ </mxCell>
43
+ <mxCell id="edge6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#AD1457;" edge="1" parent="1" source="reports" target="frontend">
44
+ <mxGeometry relative="1" as="geometry"/>
45
+ </mxCell>
46
+ <mxCell id="edge7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#FDD835;" edge="1" parent="1" source="datasets" target="orchestrator">
47
+ <mxGeometry relative="1" as="geometry"/>
48
+ </mxCell>
49
+ </root>
50
+ </mxGraphModel>
51
+ </diagram>
52
+ </mxfile>
docs/agent-workflow.svg ADDED
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
frontend/eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs.flat.recommended,
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
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>frontend</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,2675 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "version": "0.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "frontend",
9
+ "version": "0.0.0",
10
+ "dependencies": {
11
+ "react": "^19.2.0",
12
+ "react-dom": "^19.2.0"
13
+ },
14
+ "devDependencies": {
15
+ "@eslint/js": "^9.39.1",
16
+ "@types/react": "^19.2.5",
17
+ "@types/react-dom": "^19.2.3",
18
+ "@vitejs/plugin-react": "^5.1.1",
19
+ "eslint": "^9.39.1",
20
+ "eslint-plugin-react-hooks": "^7.0.1",
21
+ "eslint-plugin-react-refresh": "^0.4.24",
22
+ "globals": "^16.5.0",
23
+ "vite": "npm:rolldown-vite@7.2.5"
24
+ }
25
+ },
26
+ "node_modules/@babel/code-frame": {
27
+ "version": "7.27.1",
28
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
29
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
30
+ "dev": true,
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "@babel/helper-validator-identifier": "^7.27.1",
34
+ "js-tokens": "^4.0.0",
35
+ "picocolors": "^1.1.1"
36
+ },
37
+ "engines": {
38
+ "node": ">=6.9.0"
39
+ }
40
+ },
41
+ "node_modules/@babel/compat-data": {
42
+ "version": "7.28.5",
43
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
44
+ "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
45
+ "dev": true,
46
+ "license": "MIT",
47
+ "engines": {
48
+ "node": ">=6.9.0"
49
+ }
50
+ },
51
+ "node_modules/@babel/core": {
52
+ "version": "7.28.5",
53
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
54
+ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
55
+ "dev": true,
56
+ "license": "MIT",
57
+ "dependencies": {
58
+ "@babel/code-frame": "^7.27.1",
59
+ "@babel/generator": "^7.28.5",
60
+ "@babel/helper-compilation-targets": "^7.27.2",
61
+ "@babel/helper-module-transforms": "^7.28.3",
62
+ "@babel/helpers": "^7.28.4",
63
+ "@babel/parser": "^7.28.5",
64
+ "@babel/template": "^7.27.2",
65
+ "@babel/traverse": "^7.28.5",
66
+ "@babel/types": "^7.28.5",
67
+ "@jridgewell/remapping": "^2.3.5",
68
+ "convert-source-map": "^2.0.0",
69
+ "debug": "^4.1.0",
70
+ "gensync": "^1.0.0-beta.2",
71
+ "json5": "^2.2.3",
72
+ "semver": "^6.3.1"
73
+ },
74
+ "engines": {
75
+ "node": ">=6.9.0"
76
+ },
77
+ "funding": {
78
+ "type": "opencollective",
79
+ "url": "https://opencollective.com/babel"
80
+ }
81
+ },
82
+ "node_modules/@babel/generator": {
83
+ "version": "7.28.5",
84
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
85
+ "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
86
+ "dev": true,
87
+ "license": "MIT",
88
+ "dependencies": {
89
+ "@babel/parser": "^7.28.5",
90
+ "@babel/types": "^7.28.5",
91
+ "@jridgewell/gen-mapping": "^0.3.12",
92
+ "@jridgewell/trace-mapping": "^0.3.28",
93
+ "jsesc": "^3.0.2"
94
+ },
95
+ "engines": {
96
+ "node": ">=6.9.0"
97
+ }
98
+ },
99
+ "node_modules/@babel/helper-compilation-targets": {
100
+ "version": "7.27.2",
101
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
102
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
103
+ "dev": true,
104
+ "license": "MIT",
105
+ "dependencies": {
106
+ "@babel/compat-data": "^7.27.2",
107
+ "@babel/helper-validator-option": "^7.27.1",
108
+ "browserslist": "^4.24.0",
109
+ "lru-cache": "^5.1.1",
110
+ "semver": "^6.3.1"
111
+ },
112
+ "engines": {
113
+ "node": ">=6.9.0"
114
+ }
115
+ },
116
+ "node_modules/@babel/helper-globals": {
117
+ "version": "7.28.0",
118
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
119
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
120
+ "dev": true,
121
+ "license": "MIT",
122
+ "engines": {
123
+ "node": ">=6.9.0"
124
+ }
125
+ },
126
+ "node_modules/@babel/helper-module-imports": {
127
+ "version": "7.27.1",
128
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
129
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
130
+ "dev": true,
131
+ "license": "MIT",
132
+ "dependencies": {
133
+ "@babel/traverse": "^7.27.1",
134
+ "@babel/types": "^7.27.1"
135
+ },
136
+ "engines": {
137
+ "node": ">=6.9.0"
138
+ }
139
+ },
140
+ "node_modules/@babel/helper-module-transforms": {
141
+ "version": "7.28.3",
142
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
143
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
144
+ "dev": true,
145
+ "license": "MIT",
146
+ "dependencies": {
147
+ "@babel/helper-module-imports": "^7.27.1",
148
+ "@babel/helper-validator-identifier": "^7.27.1",
149
+ "@babel/traverse": "^7.28.3"
150
+ },
151
+ "engines": {
152
+ "node": ">=6.9.0"
153
+ },
154
+ "peerDependencies": {
155
+ "@babel/core": "^7.0.0"
156
+ }
157
+ },
158
+ "node_modules/@babel/helper-plugin-utils": {
159
+ "version": "7.27.1",
160
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
161
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
162
+ "dev": true,
163
+ "license": "MIT",
164
+ "engines": {
165
+ "node": ">=6.9.0"
166
+ }
167
+ },
168
+ "node_modules/@babel/helper-string-parser": {
169
+ "version": "7.27.1",
170
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
171
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
172
+ "dev": true,
173
+ "license": "MIT",
174
+ "engines": {
175
+ "node": ">=6.9.0"
176
+ }
177
+ },
178
+ "node_modules/@babel/helper-validator-identifier": {
179
+ "version": "7.28.5",
180
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
181
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
182
+ "dev": true,
183
+ "license": "MIT",
184
+ "engines": {
185
+ "node": ">=6.9.0"
186
+ }
187
+ },
188
+ "node_modules/@babel/helper-validator-option": {
189
+ "version": "7.27.1",
190
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
191
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
192
+ "dev": true,
193
+ "license": "MIT",
194
+ "engines": {
195
+ "node": ">=6.9.0"
196
+ }
197
+ },
198
+ "node_modules/@babel/helpers": {
199
+ "version": "7.28.4",
200
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
201
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
202
+ "dev": true,
203
+ "license": "MIT",
204
+ "dependencies": {
205
+ "@babel/template": "^7.27.2",
206
+ "@babel/types": "^7.28.4"
207
+ },
208
+ "engines": {
209
+ "node": ">=6.9.0"
210
+ }
211
+ },
212
+ "node_modules/@babel/parser": {
213
+ "version": "7.28.5",
214
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
215
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
216
+ "dev": true,
217
+ "license": "MIT",
218
+ "dependencies": {
219
+ "@babel/types": "^7.28.5"
220
+ },
221
+ "bin": {
222
+ "parser": "bin/babel-parser.js"
223
+ },
224
+ "engines": {
225
+ "node": ">=6.0.0"
226
+ }
227
+ },
228
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
229
+ "version": "7.27.1",
230
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
231
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
232
+ "dev": true,
233
+ "license": "MIT",
234
+ "dependencies": {
235
+ "@babel/helper-plugin-utils": "^7.27.1"
236
+ },
237
+ "engines": {
238
+ "node": ">=6.9.0"
239
+ },
240
+ "peerDependencies": {
241
+ "@babel/core": "^7.0.0-0"
242
+ }
243
+ },
244
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
245
+ "version": "7.27.1",
246
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
247
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
248
+ "dev": true,
249
+ "license": "MIT",
250
+ "dependencies": {
251
+ "@babel/helper-plugin-utils": "^7.27.1"
252
+ },
253
+ "engines": {
254
+ "node": ">=6.9.0"
255
+ },
256
+ "peerDependencies": {
257
+ "@babel/core": "^7.0.0-0"
258
+ }
259
+ },
260
+ "node_modules/@babel/template": {
261
+ "version": "7.27.2",
262
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
263
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
264
+ "dev": true,
265
+ "license": "MIT",
266
+ "dependencies": {
267
+ "@babel/code-frame": "^7.27.1",
268
+ "@babel/parser": "^7.27.2",
269
+ "@babel/types": "^7.27.1"
270
+ },
271
+ "engines": {
272
+ "node": ">=6.9.0"
273
+ }
274
+ },
275
+ "node_modules/@babel/traverse": {
276
+ "version": "7.28.5",
277
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
278
+ "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
279
+ "dev": true,
280
+ "license": "MIT",
281
+ "dependencies": {
282
+ "@babel/code-frame": "^7.27.1",
283
+ "@babel/generator": "^7.28.5",
284
+ "@babel/helper-globals": "^7.28.0",
285
+ "@babel/parser": "^7.28.5",
286
+ "@babel/template": "^7.27.2",
287
+ "@babel/types": "^7.28.5",
288
+ "debug": "^4.3.1"
289
+ },
290
+ "engines": {
291
+ "node": ">=6.9.0"
292
+ }
293
+ },
294
+ "node_modules/@babel/types": {
295
+ "version": "7.28.5",
296
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
297
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
298
+ "dev": true,
299
+ "license": "MIT",
300
+ "dependencies": {
301
+ "@babel/helper-string-parser": "^7.27.1",
302
+ "@babel/helper-validator-identifier": "^7.28.5"
303
+ },
304
+ "engines": {
305
+ "node": ">=6.9.0"
306
+ }
307
+ },
308
+ "node_modules/@emnapi/core": {
309
+ "version": "1.7.1",
310
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
311
+ "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==",
312
+ "dev": true,
313
+ "license": "MIT",
314
+ "optional": true,
315
+ "dependencies": {
316
+ "@emnapi/wasi-threads": "1.1.0",
317
+ "tslib": "^2.4.0"
318
+ }
319
+ },
320
+ "node_modules/@emnapi/runtime": {
321
+ "version": "1.7.1",
322
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
323
+ "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
324
+ "dev": true,
325
+ "license": "MIT",
326
+ "optional": true,
327
+ "dependencies": {
328
+ "tslib": "^2.4.0"
329
+ }
330
+ },
331
+ "node_modules/@emnapi/wasi-threads": {
332
+ "version": "1.1.0",
333
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
334
+ "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
335
+ "dev": true,
336
+ "license": "MIT",
337
+ "optional": true,
338
+ "dependencies": {
339
+ "tslib": "^2.4.0"
340
+ }
341
+ },
342
+ "node_modules/@eslint-community/eslint-utils": {
343
+ "version": "4.9.0",
344
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
345
+ "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
346
+ "dev": true,
347
+ "license": "MIT",
348
+ "dependencies": {
349
+ "eslint-visitor-keys": "^3.4.3"
350
+ },
351
+ "engines": {
352
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
353
+ },
354
+ "funding": {
355
+ "url": "https://opencollective.com/eslint"
356
+ },
357
+ "peerDependencies": {
358
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
359
+ }
360
+ },
361
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
362
+ "version": "3.4.3",
363
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
364
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
365
+ "dev": true,
366
+ "license": "Apache-2.0",
367
+ "engines": {
368
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
369
+ },
370
+ "funding": {
371
+ "url": "https://opencollective.com/eslint"
372
+ }
373
+ },
374
+ "node_modules/@eslint-community/regexpp": {
375
+ "version": "4.12.2",
376
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
377
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
378
+ "dev": true,
379
+ "license": "MIT",
380
+ "engines": {
381
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
382
+ }
383
+ },
384
+ "node_modules/@eslint/config-array": {
385
+ "version": "0.21.1",
386
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
387
+ "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
388
+ "dev": true,
389
+ "license": "Apache-2.0",
390
+ "dependencies": {
391
+ "@eslint/object-schema": "^2.1.7",
392
+ "debug": "^4.3.1",
393
+ "minimatch": "^3.1.2"
394
+ },
395
+ "engines": {
396
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
397
+ }
398
+ },
399
+ "node_modules/@eslint/config-helpers": {
400
+ "version": "0.4.2",
401
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
402
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
403
+ "dev": true,
404
+ "license": "Apache-2.0",
405
+ "dependencies": {
406
+ "@eslint/core": "^0.17.0"
407
+ },
408
+ "engines": {
409
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
410
+ }
411
+ },
412
+ "node_modules/@eslint/core": {
413
+ "version": "0.17.0",
414
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
415
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
416
+ "dev": true,
417
+ "license": "Apache-2.0",
418
+ "dependencies": {
419
+ "@types/json-schema": "^7.0.15"
420
+ },
421
+ "engines": {
422
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
423
+ }
424
+ },
425
+ "node_modules/@eslint/eslintrc": {
426
+ "version": "3.3.1",
427
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
428
+ "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
429
+ "dev": true,
430
+ "license": "MIT",
431
+ "dependencies": {
432
+ "ajv": "^6.12.4",
433
+ "debug": "^4.3.2",
434
+ "espree": "^10.0.1",
435
+ "globals": "^14.0.0",
436
+ "ignore": "^5.2.0",
437
+ "import-fresh": "^3.2.1",
438
+ "js-yaml": "^4.1.0",
439
+ "minimatch": "^3.1.2",
440
+ "strip-json-comments": "^3.1.1"
441
+ },
442
+ "engines": {
443
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
444
+ },
445
+ "funding": {
446
+ "url": "https://opencollective.com/eslint"
447
+ }
448
+ },
449
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
450
+ "version": "14.0.0",
451
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
452
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
453
+ "dev": true,
454
+ "license": "MIT",
455
+ "engines": {
456
+ "node": ">=18"
457
+ },
458
+ "funding": {
459
+ "url": "https://github.com/sponsors/sindresorhus"
460
+ }
461
+ },
462
+ "node_modules/@eslint/js": {
463
+ "version": "9.39.1",
464
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
465
+ "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
466
+ "dev": true,
467
+ "license": "MIT",
468
+ "engines": {
469
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
470
+ },
471
+ "funding": {
472
+ "url": "https://eslint.org/donate"
473
+ }
474
+ },
475
+ "node_modules/@eslint/object-schema": {
476
+ "version": "2.1.7",
477
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
478
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
479
+ "dev": true,
480
+ "license": "Apache-2.0",
481
+ "engines": {
482
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
483
+ }
484
+ },
485
+ "node_modules/@eslint/plugin-kit": {
486
+ "version": "0.4.1",
487
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
488
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
489
+ "dev": true,
490
+ "license": "Apache-2.0",
491
+ "dependencies": {
492
+ "@eslint/core": "^0.17.0",
493
+ "levn": "^0.4.1"
494
+ },
495
+ "engines": {
496
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
497
+ }
498
+ },
499
+ "node_modules/@humanfs/core": {
500
+ "version": "0.19.1",
501
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
502
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
503
+ "dev": true,
504
+ "license": "Apache-2.0",
505
+ "engines": {
506
+ "node": ">=18.18.0"
507
+ }
508
+ },
509
+ "node_modules/@humanfs/node": {
510
+ "version": "0.16.7",
511
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
512
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
513
+ "dev": true,
514
+ "license": "Apache-2.0",
515
+ "dependencies": {
516
+ "@humanfs/core": "^0.19.1",
517
+ "@humanwhocodes/retry": "^0.4.0"
518
+ },
519
+ "engines": {
520
+ "node": ">=18.18.0"
521
+ }
522
+ },
523
+ "node_modules/@humanwhocodes/module-importer": {
524
+ "version": "1.0.1",
525
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
526
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
527
+ "dev": true,
528
+ "license": "Apache-2.0",
529
+ "engines": {
530
+ "node": ">=12.22"
531
+ },
532
+ "funding": {
533
+ "type": "github",
534
+ "url": "https://github.com/sponsors/nzakas"
535
+ }
536
+ },
537
+ "node_modules/@humanwhocodes/retry": {
538
+ "version": "0.4.3",
539
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
540
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
541
+ "dev": true,
542
+ "license": "Apache-2.0",
543
+ "engines": {
544
+ "node": ">=18.18"
545
+ },
546
+ "funding": {
547
+ "type": "github",
548
+ "url": "https://github.com/sponsors/nzakas"
549
+ }
550
+ },
551
+ "node_modules/@jridgewell/gen-mapping": {
552
+ "version": "0.3.13",
553
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
554
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
555
+ "dev": true,
556
+ "license": "MIT",
557
+ "dependencies": {
558
+ "@jridgewell/sourcemap-codec": "^1.5.0",
559
+ "@jridgewell/trace-mapping": "^0.3.24"
560
+ }
561
+ },
562
+ "node_modules/@jridgewell/remapping": {
563
+ "version": "2.3.5",
564
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
565
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
566
+ "dev": true,
567
+ "license": "MIT",
568
+ "dependencies": {
569
+ "@jridgewell/gen-mapping": "^0.3.5",
570
+ "@jridgewell/trace-mapping": "^0.3.24"
571
+ }
572
+ },
573
+ "node_modules/@jridgewell/resolve-uri": {
574
+ "version": "3.1.2",
575
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
576
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
577
+ "dev": true,
578
+ "license": "MIT",
579
+ "engines": {
580
+ "node": ">=6.0.0"
581
+ }
582
+ },
583
+ "node_modules/@jridgewell/sourcemap-codec": {
584
+ "version": "1.5.5",
585
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
586
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
587
+ "dev": true,
588
+ "license": "MIT"
589
+ },
590
+ "node_modules/@jridgewell/trace-mapping": {
591
+ "version": "0.3.31",
592
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
593
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
594
+ "dev": true,
595
+ "license": "MIT",
596
+ "dependencies": {
597
+ "@jridgewell/resolve-uri": "^3.1.0",
598
+ "@jridgewell/sourcemap-codec": "^1.4.14"
599
+ }
600
+ },
601
+ "node_modules/@napi-rs/wasm-runtime": {
602
+ "version": "1.0.7",
603
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz",
604
+ "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==",
605
+ "dev": true,
606
+ "license": "MIT",
607
+ "optional": true,
608
+ "dependencies": {
609
+ "@emnapi/core": "^1.5.0",
610
+ "@emnapi/runtime": "^1.5.0",
611
+ "@tybys/wasm-util": "^0.10.1"
612
+ }
613
+ },
614
+ "node_modules/@oxc-project/runtime": {
615
+ "version": "0.97.0",
616
+ "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.97.0.tgz",
617
+ "integrity": "sha512-yH0zw7z+jEws4dZ4IUKoix5Lh3yhqIJWF9Dc8PWvhpo7U7O+lJrv7ZZL4BeRO0la8LBQFwcCewtLBnVV7hPe/w==",
618
+ "dev": true,
619
+ "license": "MIT",
620
+ "engines": {
621
+ "node": "^20.19.0 || >=22.12.0"
622
+ }
623
+ },
624
+ "node_modules/@oxc-project/types": {
625
+ "version": "0.97.0",
626
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.97.0.tgz",
627
+ "integrity": "sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ==",
628
+ "dev": true,
629
+ "license": "MIT",
630
+ "funding": {
631
+ "url": "https://github.com/sponsors/Boshen"
632
+ }
633
+ },
634
+ "node_modules/@rolldown/binding-android-arm64": {
635
+ "version": "1.0.0-beta.50",
636
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.50.tgz",
637
+ "integrity": "sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag==",
638
+ "cpu": [
639
+ "arm64"
640
+ ],
641
+ "dev": true,
642
+ "license": "MIT",
643
+ "optional": true,
644
+ "os": [
645
+ "android"
646
+ ],
647
+ "engines": {
648
+ "node": "^20.19.0 || >=22.12.0"
649
+ }
650
+ },
651
+ "node_modules/@rolldown/binding-darwin-arm64": {
652
+ "version": "1.0.0-beta.50",
653
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.50.tgz",
654
+ "integrity": "sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w==",
655
+ "cpu": [
656
+ "arm64"
657
+ ],
658
+ "dev": true,
659
+ "license": "MIT",
660
+ "optional": true,
661
+ "os": [
662
+ "darwin"
663
+ ],
664
+ "engines": {
665
+ "node": "^20.19.0 || >=22.12.0"
666
+ }
667
+ },
668
+ "node_modules/@rolldown/binding-darwin-x64": {
669
+ "version": "1.0.0-beta.50",
670
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.50.tgz",
671
+ "integrity": "sha512-fFXDjXnuX7/gQZQm/1FoivVtRcyAzdjSik7Eo+9iwPQ9EgtA5/nB2+jmbzaKtMGG3q+BnZbdKHCtOacmNrkIDA==",
672
+ "cpu": [
673
+ "x64"
674
+ ],
675
+ "dev": true,
676
+ "license": "MIT",
677
+ "optional": true,
678
+ "os": [
679
+ "darwin"
680
+ ],
681
+ "engines": {
682
+ "node": "^20.19.0 || >=22.12.0"
683
+ }
684
+ },
685
+ "node_modules/@rolldown/binding-freebsd-x64": {
686
+ "version": "1.0.0-beta.50",
687
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.50.tgz",
688
+ "integrity": "sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q==",
689
+ "cpu": [
690
+ "x64"
691
+ ],
692
+ "dev": true,
693
+ "license": "MIT",
694
+ "optional": true,
695
+ "os": [
696
+ "freebsd"
697
+ ],
698
+ "engines": {
699
+ "node": "^20.19.0 || >=22.12.0"
700
+ }
701
+ },
702
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
703
+ "version": "1.0.0-beta.50",
704
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.50.tgz",
705
+ "integrity": "sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw==",
706
+ "cpu": [
707
+ "arm"
708
+ ],
709
+ "dev": true,
710
+ "license": "MIT",
711
+ "optional": true,
712
+ "os": [
713
+ "linux"
714
+ ],
715
+ "engines": {
716
+ "node": "^20.19.0 || >=22.12.0"
717
+ }
718
+ },
719
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
720
+ "version": "1.0.0-beta.50",
721
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.50.tgz",
722
+ "integrity": "sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw==",
723
+ "cpu": [
724
+ "arm64"
725
+ ],
726
+ "dev": true,
727
+ "license": "MIT",
728
+ "optional": true,
729
+ "os": [
730
+ "linux"
731
+ ],
732
+ "engines": {
733
+ "node": "^20.19.0 || >=22.12.0"
734
+ }
735
+ },
736
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
737
+ "version": "1.0.0-beta.50",
738
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.50.tgz",
739
+ "integrity": "sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw==",
740
+ "cpu": [
741
+ "arm64"
742
+ ],
743
+ "dev": true,
744
+ "license": "MIT",
745
+ "optional": true,
746
+ "os": [
747
+ "linux"
748
+ ],
749
+ "engines": {
750
+ "node": "^20.19.0 || >=22.12.0"
751
+ }
752
+ },
753
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
754
+ "version": "1.0.0-beta.50",
755
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.50.tgz",
756
+ "integrity": "sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg==",
757
+ "cpu": [
758
+ "x64"
759
+ ],
760
+ "dev": true,
761
+ "license": "MIT",
762
+ "optional": true,
763
+ "os": [
764
+ "linux"
765
+ ],
766
+ "engines": {
767
+ "node": "^20.19.0 || >=22.12.0"
768
+ }
769
+ },
770
+ "node_modules/@rolldown/binding-linux-x64-musl": {
771
+ "version": "1.0.0-beta.50",
772
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.50.tgz",
773
+ "integrity": "sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA==",
774
+ "cpu": [
775
+ "x64"
776
+ ],
777
+ "dev": true,
778
+ "license": "MIT",
779
+ "optional": true,
780
+ "os": [
781
+ "linux"
782
+ ],
783
+ "engines": {
784
+ "node": "^20.19.0 || >=22.12.0"
785
+ }
786
+ },
787
+ "node_modules/@rolldown/binding-openharmony-arm64": {
788
+ "version": "1.0.0-beta.50",
789
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.50.tgz",
790
+ "integrity": "sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA==",
791
+ "cpu": [
792
+ "arm64"
793
+ ],
794
+ "dev": true,
795
+ "license": "MIT",
796
+ "optional": true,
797
+ "os": [
798
+ "openharmony"
799
+ ],
800
+ "engines": {
801
+ "node": "^20.19.0 || >=22.12.0"
802
+ }
803
+ },
804
+ "node_modules/@rolldown/binding-wasm32-wasi": {
805
+ "version": "1.0.0-beta.50",
806
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.50.tgz",
807
+ "integrity": "sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg==",
808
+ "cpu": [
809
+ "wasm32"
810
+ ],
811
+ "dev": true,
812
+ "license": "MIT",
813
+ "optional": true,
814
+ "dependencies": {
815
+ "@napi-rs/wasm-runtime": "^1.0.7"
816
+ },
817
+ "engines": {
818
+ "node": ">=14.0.0"
819
+ }
820
+ },
821
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
822
+ "version": "1.0.0-beta.50",
823
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.50.tgz",
824
+ "integrity": "sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA==",
825
+ "cpu": [
826
+ "arm64"
827
+ ],
828
+ "dev": true,
829
+ "license": "MIT",
830
+ "optional": true,
831
+ "os": [
832
+ "win32"
833
+ ],
834
+ "engines": {
835
+ "node": "^20.19.0 || >=22.12.0"
836
+ }
837
+ },
838
+ "node_modules/@rolldown/binding-win32-ia32-msvc": {
839
+ "version": "1.0.0-beta.50",
840
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.50.tgz",
841
+ "integrity": "sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg==",
842
+ "cpu": [
843
+ "ia32"
844
+ ],
845
+ "dev": true,
846
+ "license": "MIT",
847
+ "optional": true,
848
+ "os": [
849
+ "win32"
850
+ ],
851
+ "engines": {
852
+ "node": "^20.19.0 || >=22.12.0"
853
+ }
854
+ },
855
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
856
+ "version": "1.0.0-beta.50",
857
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.50.tgz",
858
+ "integrity": "sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ==",
859
+ "cpu": [
860
+ "x64"
861
+ ],
862
+ "dev": true,
863
+ "license": "MIT",
864
+ "optional": true,
865
+ "os": [
866
+ "win32"
867
+ ],
868
+ "engines": {
869
+ "node": "^20.19.0 || >=22.12.0"
870
+ }
871
+ },
872
+ "node_modules/@rolldown/pluginutils": {
873
+ "version": "1.0.0-beta.47",
874
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz",
875
+ "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==",
876
+ "dev": true,
877
+ "license": "MIT"
878
+ },
879
+ "node_modules/@tybys/wasm-util": {
880
+ "version": "0.10.1",
881
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
882
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
883
+ "dev": true,
884
+ "license": "MIT",
885
+ "optional": true,
886
+ "dependencies": {
887
+ "tslib": "^2.4.0"
888
+ }
889
+ },
890
+ "node_modules/@types/babel__core": {
891
+ "version": "7.20.5",
892
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
893
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
894
+ "dev": true,
895
+ "license": "MIT",
896
+ "dependencies": {
897
+ "@babel/parser": "^7.20.7",
898
+ "@babel/types": "^7.20.7",
899
+ "@types/babel__generator": "*",
900
+ "@types/babel__template": "*",
901
+ "@types/babel__traverse": "*"
902
+ }
903
+ },
904
+ "node_modules/@types/babel__generator": {
905
+ "version": "7.27.0",
906
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
907
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
908
+ "dev": true,
909
+ "license": "MIT",
910
+ "dependencies": {
911
+ "@babel/types": "^7.0.0"
912
+ }
913
+ },
914
+ "node_modules/@types/babel__template": {
915
+ "version": "7.4.4",
916
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
917
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
918
+ "dev": true,
919
+ "license": "MIT",
920
+ "dependencies": {
921
+ "@babel/parser": "^7.1.0",
922
+ "@babel/types": "^7.0.0"
923
+ }
924
+ },
925
+ "node_modules/@types/babel__traverse": {
926
+ "version": "7.28.0",
927
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
928
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
929
+ "dev": true,
930
+ "license": "MIT",
931
+ "dependencies": {
932
+ "@babel/types": "^7.28.2"
933
+ }
934
+ },
935
+ "node_modules/@types/estree": {
936
+ "version": "1.0.8",
937
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
938
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
939
+ "dev": true,
940
+ "license": "MIT"
941
+ },
942
+ "node_modules/@types/json-schema": {
943
+ "version": "7.0.15",
944
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
945
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
946
+ "dev": true,
947
+ "license": "MIT"
948
+ },
949
+ "node_modules/@types/react": {
950
+ "version": "19.2.6",
951
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz",
952
+ "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==",
953
+ "dev": true,
954
+ "license": "MIT",
955
+ "dependencies": {
956
+ "csstype": "^3.2.2"
957
+ }
958
+ },
959
+ "node_modules/@types/react-dom": {
960
+ "version": "19.2.3",
961
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
962
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
963
+ "dev": true,
964
+ "license": "MIT",
965
+ "peerDependencies": {
966
+ "@types/react": "^19.2.0"
967
+ }
968
+ },
969
+ "node_modules/@vitejs/plugin-react": {
970
+ "version": "5.1.1",
971
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz",
972
+ "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==",
973
+ "dev": true,
974
+ "license": "MIT",
975
+ "dependencies": {
976
+ "@babel/core": "^7.28.5",
977
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
978
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
979
+ "@rolldown/pluginutils": "1.0.0-beta.47",
980
+ "@types/babel__core": "^7.20.5",
981
+ "react-refresh": "^0.18.0"
982
+ },
983
+ "engines": {
984
+ "node": "^20.19.0 || >=22.12.0"
985
+ },
986
+ "peerDependencies": {
987
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
988
+ }
989
+ },
990
+ "node_modules/acorn": {
991
+ "version": "8.15.0",
992
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
993
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
994
+ "dev": true,
995
+ "license": "MIT",
996
+ "bin": {
997
+ "acorn": "bin/acorn"
998
+ },
999
+ "engines": {
1000
+ "node": ">=0.4.0"
1001
+ }
1002
+ },
1003
+ "node_modules/acorn-jsx": {
1004
+ "version": "5.3.2",
1005
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
1006
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
1007
+ "dev": true,
1008
+ "license": "MIT",
1009
+ "peerDependencies": {
1010
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
1011
+ }
1012
+ },
1013
+ "node_modules/ajv": {
1014
+ "version": "6.12.6",
1015
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
1016
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
1017
+ "dev": true,
1018
+ "license": "MIT",
1019
+ "dependencies": {
1020
+ "fast-deep-equal": "^3.1.1",
1021
+ "fast-json-stable-stringify": "^2.0.0",
1022
+ "json-schema-traverse": "^0.4.1",
1023
+ "uri-js": "^4.2.2"
1024
+ },
1025
+ "funding": {
1026
+ "type": "github",
1027
+ "url": "https://github.com/sponsors/epoberezkin"
1028
+ }
1029
+ },
1030
+ "node_modules/ansi-styles": {
1031
+ "version": "4.3.0",
1032
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
1033
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
1034
+ "dev": true,
1035
+ "license": "MIT",
1036
+ "dependencies": {
1037
+ "color-convert": "^2.0.1"
1038
+ },
1039
+ "engines": {
1040
+ "node": ">=8"
1041
+ },
1042
+ "funding": {
1043
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
1044
+ }
1045
+ },
1046
+ "node_modules/argparse": {
1047
+ "version": "2.0.1",
1048
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
1049
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
1050
+ "dev": true,
1051
+ "license": "Python-2.0"
1052
+ },
1053
+ "node_modules/balanced-match": {
1054
+ "version": "1.0.2",
1055
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
1056
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
1057
+ "dev": true,
1058
+ "license": "MIT"
1059
+ },
1060
+ "node_modules/baseline-browser-mapping": {
1061
+ "version": "2.8.30",
1062
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz",
1063
+ "integrity": "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==",
1064
+ "dev": true,
1065
+ "license": "Apache-2.0",
1066
+ "bin": {
1067
+ "baseline-browser-mapping": "dist/cli.js"
1068
+ }
1069
+ },
1070
+ "node_modules/brace-expansion": {
1071
+ "version": "1.1.12",
1072
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
1073
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
1074
+ "dev": true,
1075
+ "license": "MIT",
1076
+ "dependencies": {
1077
+ "balanced-match": "^1.0.0",
1078
+ "concat-map": "0.0.1"
1079
+ }
1080
+ },
1081
+ "node_modules/browserslist": {
1082
+ "version": "4.28.0",
1083
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
1084
+ "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
1085
+ "dev": true,
1086
+ "funding": [
1087
+ {
1088
+ "type": "opencollective",
1089
+ "url": "https://opencollective.com/browserslist"
1090
+ },
1091
+ {
1092
+ "type": "tidelift",
1093
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1094
+ },
1095
+ {
1096
+ "type": "github",
1097
+ "url": "https://github.com/sponsors/ai"
1098
+ }
1099
+ ],
1100
+ "license": "MIT",
1101
+ "dependencies": {
1102
+ "baseline-browser-mapping": "^2.8.25",
1103
+ "caniuse-lite": "^1.0.30001754",
1104
+ "electron-to-chromium": "^1.5.249",
1105
+ "node-releases": "^2.0.27",
1106
+ "update-browserslist-db": "^1.1.4"
1107
+ },
1108
+ "bin": {
1109
+ "browserslist": "cli.js"
1110
+ },
1111
+ "engines": {
1112
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1113
+ }
1114
+ },
1115
+ "node_modules/callsites": {
1116
+ "version": "3.1.0",
1117
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
1118
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
1119
+ "dev": true,
1120
+ "license": "MIT",
1121
+ "engines": {
1122
+ "node": ">=6"
1123
+ }
1124
+ },
1125
+ "node_modules/caniuse-lite": {
1126
+ "version": "1.0.30001756",
1127
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz",
1128
+ "integrity": "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==",
1129
+ "dev": true,
1130
+ "funding": [
1131
+ {
1132
+ "type": "opencollective",
1133
+ "url": "https://opencollective.com/browserslist"
1134
+ },
1135
+ {
1136
+ "type": "tidelift",
1137
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1138
+ },
1139
+ {
1140
+ "type": "github",
1141
+ "url": "https://github.com/sponsors/ai"
1142
+ }
1143
+ ],
1144
+ "license": "CC-BY-4.0"
1145
+ },
1146
+ "node_modules/chalk": {
1147
+ "version": "4.1.2",
1148
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
1149
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
1150
+ "dev": true,
1151
+ "license": "MIT",
1152
+ "dependencies": {
1153
+ "ansi-styles": "^4.1.0",
1154
+ "supports-color": "^7.1.0"
1155
+ },
1156
+ "engines": {
1157
+ "node": ">=10"
1158
+ },
1159
+ "funding": {
1160
+ "url": "https://github.com/chalk/chalk?sponsor=1"
1161
+ }
1162
+ },
1163
+ "node_modules/color-convert": {
1164
+ "version": "2.0.1",
1165
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
1166
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
1167
+ "dev": true,
1168
+ "license": "MIT",
1169
+ "dependencies": {
1170
+ "color-name": "~1.1.4"
1171
+ },
1172
+ "engines": {
1173
+ "node": ">=7.0.0"
1174
+ }
1175
+ },
1176
+ "node_modules/color-name": {
1177
+ "version": "1.1.4",
1178
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
1179
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1180
+ "dev": true,
1181
+ "license": "MIT"
1182
+ },
1183
+ "node_modules/concat-map": {
1184
+ "version": "0.0.1",
1185
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
1186
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
1187
+ "dev": true,
1188
+ "license": "MIT"
1189
+ },
1190
+ "node_modules/convert-source-map": {
1191
+ "version": "2.0.0",
1192
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1193
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1194
+ "dev": true,
1195
+ "license": "MIT"
1196
+ },
1197
+ "node_modules/cross-spawn": {
1198
+ "version": "7.0.6",
1199
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
1200
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
1201
+ "dev": true,
1202
+ "license": "MIT",
1203
+ "dependencies": {
1204
+ "path-key": "^3.1.0",
1205
+ "shebang-command": "^2.0.0",
1206
+ "which": "^2.0.1"
1207
+ },
1208
+ "engines": {
1209
+ "node": ">= 8"
1210
+ }
1211
+ },
1212
+ "node_modules/csstype": {
1213
+ "version": "3.2.3",
1214
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1215
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1216
+ "dev": true,
1217
+ "license": "MIT"
1218
+ },
1219
+ "node_modules/debug": {
1220
+ "version": "4.4.3",
1221
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1222
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1223
+ "dev": true,
1224
+ "license": "MIT",
1225
+ "dependencies": {
1226
+ "ms": "^2.1.3"
1227
+ },
1228
+ "engines": {
1229
+ "node": ">=6.0"
1230
+ },
1231
+ "peerDependenciesMeta": {
1232
+ "supports-color": {
1233
+ "optional": true
1234
+ }
1235
+ }
1236
+ },
1237
+ "node_modules/deep-is": {
1238
+ "version": "0.1.4",
1239
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
1240
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
1241
+ "dev": true,
1242
+ "license": "MIT"
1243
+ },
1244
+ "node_modules/detect-libc": {
1245
+ "version": "2.1.2",
1246
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
1247
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
1248
+ "dev": true,
1249
+ "license": "Apache-2.0",
1250
+ "engines": {
1251
+ "node": ">=8"
1252
+ }
1253
+ },
1254
+ "node_modules/electron-to-chromium": {
1255
+ "version": "1.5.259",
1256
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz",
1257
+ "integrity": "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==",
1258
+ "dev": true,
1259
+ "license": "ISC"
1260
+ },
1261
+ "node_modules/escalade": {
1262
+ "version": "3.2.0",
1263
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1264
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1265
+ "dev": true,
1266
+ "license": "MIT",
1267
+ "engines": {
1268
+ "node": ">=6"
1269
+ }
1270
+ },
1271
+ "node_modules/escape-string-regexp": {
1272
+ "version": "4.0.0",
1273
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
1274
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
1275
+ "dev": true,
1276
+ "license": "MIT",
1277
+ "engines": {
1278
+ "node": ">=10"
1279
+ },
1280
+ "funding": {
1281
+ "url": "https://github.com/sponsors/sindresorhus"
1282
+ }
1283
+ },
1284
+ "node_modules/eslint": {
1285
+ "version": "9.39.1",
1286
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
1287
+ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
1288
+ "dev": true,
1289
+ "license": "MIT",
1290
+ "dependencies": {
1291
+ "@eslint-community/eslint-utils": "^4.8.0",
1292
+ "@eslint-community/regexpp": "^4.12.1",
1293
+ "@eslint/config-array": "^0.21.1",
1294
+ "@eslint/config-helpers": "^0.4.2",
1295
+ "@eslint/core": "^0.17.0",
1296
+ "@eslint/eslintrc": "^3.3.1",
1297
+ "@eslint/js": "9.39.1",
1298
+ "@eslint/plugin-kit": "^0.4.1",
1299
+ "@humanfs/node": "^0.16.6",
1300
+ "@humanwhocodes/module-importer": "^1.0.1",
1301
+ "@humanwhocodes/retry": "^0.4.2",
1302
+ "@types/estree": "^1.0.6",
1303
+ "ajv": "^6.12.4",
1304
+ "chalk": "^4.0.0",
1305
+ "cross-spawn": "^7.0.6",
1306
+ "debug": "^4.3.2",
1307
+ "escape-string-regexp": "^4.0.0",
1308
+ "eslint-scope": "^8.4.0",
1309
+ "eslint-visitor-keys": "^4.2.1",
1310
+ "espree": "^10.4.0",
1311
+ "esquery": "^1.5.0",
1312
+ "esutils": "^2.0.2",
1313
+ "fast-deep-equal": "^3.1.3",
1314
+ "file-entry-cache": "^8.0.0",
1315
+ "find-up": "^5.0.0",
1316
+ "glob-parent": "^6.0.2",
1317
+ "ignore": "^5.2.0",
1318
+ "imurmurhash": "^0.1.4",
1319
+ "is-glob": "^4.0.0",
1320
+ "json-stable-stringify-without-jsonify": "^1.0.1",
1321
+ "lodash.merge": "^4.6.2",
1322
+ "minimatch": "^3.1.2",
1323
+ "natural-compare": "^1.4.0",
1324
+ "optionator": "^0.9.3"
1325
+ },
1326
+ "bin": {
1327
+ "eslint": "bin/eslint.js"
1328
+ },
1329
+ "engines": {
1330
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1331
+ },
1332
+ "funding": {
1333
+ "url": "https://eslint.org/donate"
1334
+ },
1335
+ "peerDependencies": {
1336
+ "jiti": "*"
1337
+ },
1338
+ "peerDependenciesMeta": {
1339
+ "jiti": {
1340
+ "optional": true
1341
+ }
1342
+ }
1343
+ },
1344
+ "node_modules/eslint-plugin-react-hooks": {
1345
+ "version": "7.0.1",
1346
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
1347
+ "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
1348
+ "dev": true,
1349
+ "license": "MIT",
1350
+ "dependencies": {
1351
+ "@babel/core": "^7.24.4",
1352
+ "@babel/parser": "^7.24.4",
1353
+ "hermes-parser": "^0.25.1",
1354
+ "zod": "^3.25.0 || ^4.0.0",
1355
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
1356
+ },
1357
+ "engines": {
1358
+ "node": ">=18"
1359
+ },
1360
+ "peerDependencies": {
1361
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
1362
+ }
1363
+ },
1364
+ "node_modules/eslint-plugin-react-refresh": {
1365
+ "version": "0.4.24",
1366
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz",
1367
+ "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==",
1368
+ "dev": true,
1369
+ "license": "MIT",
1370
+ "peerDependencies": {
1371
+ "eslint": ">=8.40"
1372
+ }
1373
+ },
1374
+ "node_modules/eslint-scope": {
1375
+ "version": "8.4.0",
1376
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
1377
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
1378
+ "dev": true,
1379
+ "license": "BSD-2-Clause",
1380
+ "dependencies": {
1381
+ "esrecurse": "^4.3.0",
1382
+ "estraverse": "^5.2.0"
1383
+ },
1384
+ "engines": {
1385
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1386
+ },
1387
+ "funding": {
1388
+ "url": "https://opencollective.com/eslint"
1389
+ }
1390
+ },
1391
+ "node_modules/eslint-visitor-keys": {
1392
+ "version": "4.2.1",
1393
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
1394
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
1395
+ "dev": true,
1396
+ "license": "Apache-2.0",
1397
+ "engines": {
1398
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1399
+ },
1400
+ "funding": {
1401
+ "url": "https://opencollective.com/eslint"
1402
+ }
1403
+ },
1404
+ "node_modules/espree": {
1405
+ "version": "10.4.0",
1406
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
1407
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
1408
+ "dev": true,
1409
+ "license": "BSD-2-Clause",
1410
+ "dependencies": {
1411
+ "acorn": "^8.15.0",
1412
+ "acorn-jsx": "^5.3.2",
1413
+ "eslint-visitor-keys": "^4.2.1"
1414
+ },
1415
+ "engines": {
1416
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1417
+ },
1418
+ "funding": {
1419
+ "url": "https://opencollective.com/eslint"
1420
+ }
1421
+ },
1422
+ "node_modules/esquery": {
1423
+ "version": "1.6.0",
1424
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
1425
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
1426
+ "dev": true,
1427
+ "license": "BSD-3-Clause",
1428
+ "dependencies": {
1429
+ "estraverse": "^5.1.0"
1430
+ },
1431
+ "engines": {
1432
+ "node": ">=0.10"
1433
+ }
1434
+ },
1435
+ "node_modules/esrecurse": {
1436
+ "version": "4.3.0",
1437
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
1438
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
1439
+ "dev": true,
1440
+ "license": "BSD-2-Clause",
1441
+ "dependencies": {
1442
+ "estraverse": "^5.2.0"
1443
+ },
1444
+ "engines": {
1445
+ "node": ">=4.0"
1446
+ }
1447
+ },
1448
+ "node_modules/estraverse": {
1449
+ "version": "5.3.0",
1450
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
1451
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
1452
+ "dev": true,
1453
+ "license": "BSD-2-Clause",
1454
+ "engines": {
1455
+ "node": ">=4.0"
1456
+ }
1457
+ },
1458
+ "node_modules/esutils": {
1459
+ "version": "2.0.3",
1460
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
1461
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
1462
+ "dev": true,
1463
+ "license": "BSD-2-Clause",
1464
+ "engines": {
1465
+ "node": ">=0.10.0"
1466
+ }
1467
+ },
1468
+ "node_modules/fast-deep-equal": {
1469
+ "version": "3.1.3",
1470
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
1471
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
1472
+ "dev": true,
1473
+ "license": "MIT"
1474
+ },
1475
+ "node_modules/fast-json-stable-stringify": {
1476
+ "version": "2.1.0",
1477
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
1478
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
1479
+ "dev": true,
1480
+ "license": "MIT"
1481
+ },
1482
+ "node_modules/fast-levenshtein": {
1483
+ "version": "2.0.6",
1484
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
1485
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
1486
+ "dev": true,
1487
+ "license": "MIT"
1488
+ },
1489
+ "node_modules/fdir": {
1490
+ "version": "6.5.0",
1491
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1492
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1493
+ "dev": true,
1494
+ "license": "MIT",
1495
+ "engines": {
1496
+ "node": ">=12.0.0"
1497
+ },
1498
+ "peerDependencies": {
1499
+ "picomatch": "^3 || ^4"
1500
+ },
1501
+ "peerDependenciesMeta": {
1502
+ "picomatch": {
1503
+ "optional": true
1504
+ }
1505
+ }
1506
+ },
1507
+ "node_modules/file-entry-cache": {
1508
+ "version": "8.0.0",
1509
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
1510
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
1511
+ "dev": true,
1512
+ "license": "MIT",
1513
+ "dependencies": {
1514
+ "flat-cache": "^4.0.0"
1515
+ },
1516
+ "engines": {
1517
+ "node": ">=16.0.0"
1518
+ }
1519
+ },
1520
+ "node_modules/find-up": {
1521
+ "version": "5.0.0",
1522
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
1523
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
1524
+ "dev": true,
1525
+ "license": "MIT",
1526
+ "dependencies": {
1527
+ "locate-path": "^6.0.0",
1528
+ "path-exists": "^4.0.0"
1529
+ },
1530
+ "engines": {
1531
+ "node": ">=10"
1532
+ },
1533
+ "funding": {
1534
+ "url": "https://github.com/sponsors/sindresorhus"
1535
+ }
1536
+ },
1537
+ "node_modules/flat-cache": {
1538
+ "version": "4.0.1",
1539
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
1540
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
1541
+ "dev": true,
1542
+ "license": "MIT",
1543
+ "dependencies": {
1544
+ "flatted": "^3.2.9",
1545
+ "keyv": "^4.5.4"
1546
+ },
1547
+ "engines": {
1548
+ "node": ">=16"
1549
+ }
1550
+ },
1551
+ "node_modules/flatted": {
1552
+ "version": "3.3.3",
1553
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
1554
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
1555
+ "dev": true,
1556
+ "license": "ISC"
1557
+ },
1558
+ "node_modules/fsevents": {
1559
+ "version": "2.3.3",
1560
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1561
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1562
+ "dev": true,
1563
+ "hasInstallScript": true,
1564
+ "license": "MIT",
1565
+ "optional": true,
1566
+ "os": [
1567
+ "darwin"
1568
+ ],
1569
+ "engines": {
1570
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1571
+ }
1572
+ },
1573
+ "node_modules/gensync": {
1574
+ "version": "1.0.0-beta.2",
1575
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1576
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1577
+ "dev": true,
1578
+ "license": "MIT",
1579
+ "engines": {
1580
+ "node": ">=6.9.0"
1581
+ }
1582
+ },
1583
+ "node_modules/glob-parent": {
1584
+ "version": "6.0.2",
1585
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
1586
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
1587
+ "dev": true,
1588
+ "license": "ISC",
1589
+ "dependencies": {
1590
+ "is-glob": "^4.0.3"
1591
+ },
1592
+ "engines": {
1593
+ "node": ">=10.13.0"
1594
+ }
1595
+ },
1596
+ "node_modules/globals": {
1597
+ "version": "16.5.0",
1598
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
1599
+ "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
1600
+ "dev": true,
1601
+ "license": "MIT",
1602
+ "engines": {
1603
+ "node": ">=18"
1604
+ },
1605
+ "funding": {
1606
+ "url": "https://github.com/sponsors/sindresorhus"
1607
+ }
1608
+ },
1609
+ "node_modules/has-flag": {
1610
+ "version": "4.0.0",
1611
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
1612
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
1613
+ "dev": true,
1614
+ "license": "MIT",
1615
+ "engines": {
1616
+ "node": ">=8"
1617
+ }
1618
+ },
1619
+ "node_modules/hermes-estree": {
1620
+ "version": "0.25.1",
1621
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
1622
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
1623
+ "dev": true,
1624
+ "license": "MIT"
1625
+ },
1626
+ "node_modules/hermes-parser": {
1627
+ "version": "0.25.1",
1628
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
1629
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
1630
+ "dev": true,
1631
+ "license": "MIT",
1632
+ "dependencies": {
1633
+ "hermes-estree": "0.25.1"
1634
+ }
1635
+ },
1636
+ "node_modules/ignore": {
1637
+ "version": "5.3.2",
1638
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
1639
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
1640
+ "dev": true,
1641
+ "license": "MIT",
1642
+ "engines": {
1643
+ "node": ">= 4"
1644
+ }
1645
+ },
1646
+ "node_modules/import-fresh": {
1647
+ "version": "3.3.1",
1648
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
1649
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
1650
+ "dev": true,
1651
+ "license": "MIT",
1652
+ "dependencies": {
1653
+ "parent-module": "^1.0.0",
1654
+ "resolve-from": "^4.0.0"
1655
+ },
1656
+ "engines": {
1657
+ "node": ">=6"
1658
+ },
1659
+ "funding": {
1660
+ "url": "https://github.com/sponsors/sindresorhus"
1661
+ }
1662
+ },
1663
+ "node_modules/imurmurhash": {
1664
+ "version": "0.1.4",
1665
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
1666
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
1667
+ "dev": true,
1668
+ "license": "MIT",
1669
+ "engines": {
1670
+ "node": ">=0.8.19"
1671
+ }
1672
+ },
1673
+ "node_modules/is-extglob": {
1674
+ "version": "2.1.1",
1675
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1676
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1677
+ "dev": true,
1678
+ "license": "MIT",
1679
+ "engines": {
1680
+ "node": ">=0.10.0"
1681
+ }
1682
+ },
1683
+ "node_modules/is-glob": {
1684
+ "version": "4.0.3",
1685
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1686
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1687
+ "dev": true,
1688
+ "license": "MIT",
1689
+ "dependencies": {
1690
+ "is-extglob": "^2.1.1"
1691
+ },
1692
+ "engines": {
1693
+ "node": ">=0.10.0"
1694
+ }
1695
+ },
1696
+ "node_modules/isexe": {
1697
+ "version": "2.0.0",
1698
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
1699
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
1700
+ "dev": true,
1701
+ "license": "ISC"
1702
+ },
1703
+ "node_modules/js-tokens": {
1704
+ "version": "4.0.0",
1705
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1706
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1707
+ "dev": true,
1708
+ "license": "MIT"
1709
+ },
1710
+ "node_modules/js-yaml": {
1711
+ "version": "4.1.1",
1712
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
1713
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
1714
+ "dev": true,
1715
+ "license": "MIT",
1716
+ "dependencies": {
1717
+ "argparse": "^2.0.1"
1718
+ },
1719
+ "bin": {
1720
+ "js-yaml": "bin/js-yaml.js"
1721
+ }
1722
+ },
1723
+ "node_modules/jsesc": {
1724
+ "version": "3.1.0",
1725
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1726
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1727
+ "dev": true,
1728
+ "license": "MIT",
1729
+ "bin": {
1730
+ "jsesc": "bin/jsesc"
1731
+ },
1732
+ "engines": {
1733
+ "node": ">=6"
1734
+ }
1735
+ },
1736
+ "node_modules/json-buffer": {
1737
+ "version": "3.0.1",
1738
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
1739
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
1740
+ "dev": true,
1741
+ "license": "MIT"
1742
+ },
1743
+ "node_modules/json-schema-traverse": {
1744
+ "version": "0.4.1",
1745
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
1746
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
1747
+ "dev": true,
1748
+ "license": "MIT"
1749
+ },
1750
+ "node_modules/json-stable-stringify-without-jsonify": {
1751
+ "version": "1.0.1",
1752
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
1753
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
1754
+ "dev": true,
1755
+ "license": "MIT"
1756
+ },
1757
+ "node_modules/json5": {
1758
+ "version": "2.2.3",
1759
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1760
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1761
+ "dev": true,
1762
+ "license": "MIT",
1763
+ "bin": {
1764
+ "json5": "lib/cli.js"
1765
+ },
1766
+ "engines": {
1767
+ "node": ">=6"
1768
+ }
1769
+ },
1770
+ "node_modules/keyv": {
1771
+ "version": "4.5.4",
1772
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
1773
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
1774
+ "dev": true,
1775
+ "license": "MIT",
1776
+ "dependencies": {
1777
+ "json-buffer": "3.0.1"
1778
+ }
1779
+ },
1780
+ "node_modules/levn": {
1781
+ "version": "0.4.1",
1782
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
1783
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
1784
+ "dev": true,
1785
+ "license": "MIT",
1786
+ "dependencies": {
1787
+ "prelude-ls": "^1.2.1",
1788
+ "type-check": "~0.4.0"
1789
+ },
1790
+ "engines": {
1791
+ "node": ">= 0.8.0"
1792
+ }
1793
+ },
1794
+ "node_modules/lightningcss": {
1795
+ "version": "1.30.2",
1796
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
1797
+ "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
1798
+ "dev": true,
1799
+ "license": "MPL-2.0",
1800
+ "dependencies": {
1801
+ "detect-libc": "^2.0.3"
1802
+ },
1803
+ "engines": {
1804
+ "node": ">= 12.0.0"
1805
+ },
1806
+ "funding": {
1807
+ "type": "opencollective",
1808
+ "url": "https://opencollective.com/parcel"
1809
+ },
1810
+ "optionalDependencies": {
1811
+ "lightningcss-android-arm64": "1.30.2",
1812
+ "lightningcss-darwin-arm64": "1.30.2",
1813
+ "lightningcss-darwin-x64": "1.30.2",
1814
+ "lightningcss-freebsd-x64": "1.30.2",
1815
+ "lightningcss-linux-arm-gnueabihf": "1.30.2",
1816
+ "lightningcss-linux-arm64-gnu": "1.30.2",
1817
+ "lightningcss-linux-arm64-musl": "1.30.2",
1818
+ "lightningcss-linux-x64-gnu": "1.30.2",
1819
+ "lightningcss-linux-x64-musl": "1.30.2",
1820
+ "lightningcss-win32-arm64-msvc": "1.30.2",
1821
+ "lightningcss-win32-x64-msvc": "1.30.2"
1822
+ }
1823
+ },
1824
+ "node_modules/lightningcss-android-arm64": {
1825
+ "version": "1.30.2",
1826
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
1827
+ "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
1828
+ "cpu": [
1829
+ "arm64"
1830
+ ],
1831
+ "dev": true,
1832
+ "license": "MPL-2.0",
1833
+ "optional": true,
1834
+ "os": [
1835
+ "android"
1836
+ ],
1837
+ "engines": {
1838
+ "node": ">= 12.0.0"
1839
+ },
1840
+ "funding": {
1841
+ "type": "opencollective",
1842
+ "url": "https://opencollective.com/parcel"
1843
+ }
1844
+ },
1845
+ "node_modules/lightningcss-darwin-arm64": {
1846
+ "version": "1.30.2",
1847
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
1848
+ "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
1849
+ "cpu": [
1850
+ "arm64"
1851
+ ],
1852
+ "dev": true,
1853
+ "license": "MPL-2.0",
1854
+ "optional": true,
1855
+ "os": [
1856
+ "darwin"
1857
+ ],
1858
+ "engines": {
1859
+ "node": ">= 12.0.0"
1860
+ },
1861
+ "funding": {
1862
+ "type": "opencollective",
1863
+ "url": "https://opencollective.com/parcel"
1864
+ }
1865
+ },
1866
+ "node_modules/lightningcss-darwin-x64": {
1867
+ "version": "1.30.2",
1868
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
1869
+ "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
1870
+ "cpu": [
1871
+ "x64"
1872
+ ],
1873
+ "dev": true,
1874
+ "license": "MPL-2.0",
1875
+ "optional": true,
1876
+ "os": [
1877
+ "darwin"
1878
+ ],
1879
+ "engines": {
1880
+ "node": ">= 12.0.0"
1881
+ },
1882
+ "funding": {
1883
+ "type": "opencollective",
1884
+ "url": "https://opencollective.com/parcel"
1885
+ }
1886
+ },
1887
+ "node_modules/lightningcss-freebsd-x64": {
1888
+ "version": "1.30.2",
1889
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
1890
+ "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
1891
+ "cpu": [
1892
+ "x64"
1893
+ ],
1894
+ "dev": true,
1895
+ "license": "MPL-2.0",
1896
+ "optional": true,
1897
+ "os": [
1898
+ "freebsd"
1899
+ ],
1900
+ "engines": {
1901
+ "node": ">= 12.0.0"
1902
+ },
1903
+ "funding": {
1904
+ "type": "opencollective",
1905
+ "url": "https://opencollective.com/parcel"
1906
+ }
1907
+ },
1908
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
1909
+ "version": "1.30.2",
1910
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
1911
+ "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
1912
+ "cpu": [
1913
+ "arm"
1914
+ ],
1915
+ "dev": true,
1916
+ "license": "MPL-2.0",
1917
+ "optional": true,
1918
+ "os": [
1919
+ "linux"
1920
+ ],
1921
+ "engines": {
1922
+ "node": ">= 12.0.0"
1923
+ },
1924
+ "funding": {
1925
+ "type": "opencollective",
1926
+ "url": "https://opencollective.com/parcel"
1927
+ }
1928
+ },
1929
+ "node_modules/lightningcss-linux-arm64-gnu": {
1930
+ "version": "1.30.2",
1931
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
1932
+ "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
1933
+ "cpu": [
1934
+ "arm64"
1935
+ ],
1936
+ "dev": true,
1937
+ "license": "MPL-2.0",
1938
+ "optional": true,
1939
+ "os": [
1940
+ "linux"
1941
+ ],
1942
+ "engines": {
1943
+ "node": ">= 12.0.0"
1944
+ },
1945
+ "funding": {
1946
+ "type": "opencollective",
1947
+ "url": "https://opencollective.com/parcel"
1948
+ }
1949
+ },
1950
+ "node_modules/lightningcss-linux-arm64-musl": {
1951
+ "version": "1.30.2",
1952
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
1953
+ "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
1954
+ "cpu": [
1955
+ "arm64"
1956
+ ],
1957
+ "dev": true,
1958
+ "license": "MPL-2.0",
1959
+ "optional": true,
1960
+ "os": [
1961
+ "linux"
1962
+ ],
1963
+ "engines": {
1964
+ "node": ">= 12.0.0"
1965
+ },
1966
+ "funding": {
1967
+ "type": "opencollective",
1968
+ "url": "https://opencollective.com/parcel"
1969
+ }
1970
+ },
1971
+ "node_modules/lightningcss-linux-x64-gnu": {
1972
+ "version": "1.30.2",
1973
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
1974
+ "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
1975
+ "cpu": [
1976
+ "x64"
1977
+ ],
1978
+ "dev": true,
1979
+ "license": "MPL-2.0",
1980
+ "optional": true,
1981
+ "os": [
1982
+ "linux"
1983
+ ],
1984
+ "engines": {
1985
+ "node": ">= 12.0.0"
1986
+ },
1987
+ "funding": {
1988
+ "type": "opencollective",
1989
+ "url": "https://opencollective.com/parcel"
1990
+ }
1991
+ },
1992
+ "node_modules/lightningcss-linux-x64-musl": {
1993
+ "version": "1.30.2",
1994
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
1995
+ "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
1996
+ "cpu": [
1997
+ "x64"
1998
+ ],
1999
+ "dev": true,
2000
+ "license": "MPL-2.0",
2001
+ "optional": true,
2002
+ "os": [
2003
+ "linux"
2004
+ ],
2005
+ "engines": {
2006
+ "node": ">= 12.0.0"
2007
+ },
2008
+ "funding": {
2009
+ "type": "opencollective",
2010
+ "url": "https://opencollective.com/parcel"
2011
+ }
2012
+ },
2013
+ "node_modules/lightningcss-win32-arm64-msvc": {
2014
+ "version": "1.30.2",
2015
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
2016
+ "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
2017
+ "cpu": [
2018
+ "arm64"
2019
+ ],
2020
+ "dev": true,
2021
+ "license": "MPL-2.0",
2022
+ "optional": true,
2023
+ "os": [
2024
+ "win32"
2025
+ ],
2026
+ "engines": {
2027
+ "node": ">= 12.0.0"
2028
+ },
2029
+ "funding": {
2030
+ "type": "opencollective",
2031
+ "url": "https://opencollective.com/parcel"
2032
+ }
2033
+ },
2034
+ "node_modules/lightningcss-win32-x64-msvc": {
2035
+ "version": "1.30.2",
2036
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
2037
+ "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
2038
+ "cpu": [
2039
+ "x64"
2040
+ ],
2041
+ "dev": true,
2042
+ "license": "MPL-2.0",
2043
+ "optional": true,
2044
+ "os": [
2045
+ "win32"
2046
+ ],
2047
+ "engines": {
2048
+ "node": ">= 12.0.0"
2049
+ },
2050
+ "funding": {
2051
+ "type": "opencollective",
2052
+ "url": "https://opencollective.com/parcel"
2053
+ }
2054
+ },
2055
+ "node_modules/locate-path": {
2056
+ "version": "6.0.0",
2057
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
2058
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
2059
+ "dev": true,
2060
+ "license": "MIT",
2061
+ "dependencies": {
2062
+ "p-locate": "^5.0.0"
2063
+ },
2064
+ "engines": {
2065
+ "node": ">=10"
2066
+ },
2067
+ "funding": {
2068
+ "url": "https://github.com/sponsors/sindresorhus"
2069
+ }
2070
+ },
2071
+ "node_modules/lodash.merge": {
2072
+ "version": "4.6.2",
2073
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
2074
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
2075
+ "dev": true,
2076
+ "license": "MIT"
2077
+ },
2078
+ "node_modules/lru-cache": {
2079
+ "version": "5.1.1",
2080
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
2081
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
2082
+ "dev": true,
2083
+ "license": "ISC",
2084
+ "dependencies": {
2085
+ "yallist": "^3.0.2"
2086
+ }
2087
+ },
2088
+ "node_modules/minimatch": {
2089
+ "version": "3.1.2",
2090
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
2091
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
2092
+ "dev": true,
2093
+ "license": "ISC",
2094
+ "dependencies": {
2095
+ "brace-expansion": "^1.1.7"
2096
+ },
2097
+ "engines": {
2098
+ "node": "*"
2099
+ }
2100
+ },
2101
+ "node_modules/ms": {
2102
+ "version": "2.1.3",
2103
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
2104
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
2105
+ "dev": true,
2106
+ "license": "MIT"
2107
+ },
2108
+ "node_modules/nanoid": {
2109
+ "version": "3.3.11",
2110
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
2111
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
2112
+ "dev": true,
2113
+ "funding": [
2114
+ {
2115
+ "type": "github",
2116
+ "url": "https://github.com/sponsors/ai"
2117
+ }
2118
+ ],
2119
+ "license": "MIT",
2120
+ "bin": {
2121
+ "nanoid": "bin/nanoid.cjs"
2122
+ },
2123
+ "engines": {
2124
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
2125
+ }
2126
+ },
2127
+ "node_modules/natural-compare": {
2128
+ "version": "1.4.0",
2129
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
2130
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
2131
+ "dev": true,
2132
+ "license": "MIT"
2133
+ },
2134
+ "node_modules/node-releases": {
2135
+ "version": "2.0.27",
2136
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
2137
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
2138
+ "dev": true,
2139
+ "license": "MIT"
2140
+ },
2141
+ "node_modules/optionator": {
2142
+ "version": "0.9.4",
2143
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
2144
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
2145
+ "dev": true,
2146
+ "license": "MIT",
2147
+ "dependencies": {
2148
+ "deep-is": "^0.1.3",
2149
+ "fast-levenshtein": "^2.0.6",
2150
+ "levn": "^0.4.1",
2151
+ "prelude-ls": "^1.2.1",
2152
+ "type-check": "^0.4.0",
2153
+ "word-wrap": "^1.2.5"
2154
+ },
2155
+ "engines": {
2156
+ "node": ">= 0.8.0"
2157
+ }
2158
+ },
2159
+ "node_modules/p-limit": {
2160
+ "version": "3.1.0",
2161
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
2162
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
2163
+ "dev": true,
2164
+ "license": "MIT",
2165
+ "dependencies": {
2166
+ "yocto-queue": "^0.1.0"
2167
+ },
2168
+ "engines": {
2169
+ "node": ">=10"
2170
+ },
2171
+ "funding": {
2172
+ "url": "https://github.com/sponsors/sindresorhus"
2173
+ }
2174
+ },
2175
+ "node_modules/p-locate": {
2176
+ "version": "5.0.0",
2177
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
2178
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
2179
+ "dev": true,
2180
+ "license": "MIT",
2181
+ "dependencies": {
2182
+ "p-limit": "^3.0.2"
2183
+ },
2184
+ "engines": {
2185
+ "node": ">=10"
2186
+ },
2187
+ "funding": {
2188
+ "url": "https://github.com/sponsors/sindresorhus"
2189
+ }
2190
+ },
2191
+ "node_modules/parent-module": {
2192
+ "version": "1.0.1",
2193
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
2194
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
2195
+ "dev": true,
2196
+ "license": "MIT",
2197
+ "dependencies": {
2198
+ "callsites": "^3.0.0"
2199
+ },
2200
+ "engines": {
2201
+ "node": ">=6"
2202
+ }
2203
+ },
2204
+ "node_modules/path-exists": {
2205
+ "version": "4.0.0",
2206
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
2207
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
2208
+ "dev": true,
2209
+ "license": "MIT",
2210
+ "engines": {
2211
+ "node": ">=8"
2212
+ }
2213
+ },
2214
+ "node_modules/path-key": {
2215
+ "version": "3.1.1",
2216
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
2217
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
2218
+ "dev": true,
2219
+ "license": "MIT",
2220
+ "engines": {
2221
+ "node": ">=8"
2222
+ }
2223
+ },
2224
+ "node_modules/picocolors": {
2225
+ "version": "1.1.1",
2226
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
2227
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
2228
+ "dev": true,
2229
+ "license": "ISC"
2230
+ },
2231
+ "node_modules/picomatch": {
2232
+ "version": "4.0.3",
2233
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
2234
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
2235
+ "dev": true,
2236
+ "license": "MIT",
2237
+ "engines": {
2238
+ "node": ">=12"
2239
+ },
2240
+ "funding": {
2241
+ "url": "https://github.com/sponsors/jonschlinkert"
2242
+ }
2243
+ },
2244
+ "node_modules/postcss": {
2245
+ "version": "8.5.6",
2246
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
2247
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
2248
+ "dev": true,
2249
+ "funding": [
2250
+ {
2251
+ "type": "opencollective",
2252
+ "url": "https://opencollective.com/postcss/"
2253
+ },
2254
+ {
2255
+ "type": "tidelift",
2256
+ "url": "https://tidelift.com/funding/github/npm/postcss"
2257
+ },
2258
+ {
2259
+ "type": "github",
2260
+ "url": "https://github.com/sponsors/ai"
2261
+ }
2262
+ ],
2263
+ "license": "MIT",
2264
+ "dependencies": {
2265
+ "nanoid": "^3.3.11",
2266
+ "picocolors": "^1.1.1",
2267
+ "source-map-js": "^1.2.1"
2268
+ },
2269
+ "engines": {
2270
+ "node": "^10 || ^12 || >=14"
2271
+ }
2272
+ },
2273
+ "node_modules/prelude-ls": {
2274
+ "version": "1.2.1",
2275
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
2276
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
2277
+ "dev": true,
2278
+ "license": "MIT",
2279
+ "engines": {
2280
+ "node": ">= 0.8.0"
2281
+ }
2282
+ },
2283
+ "node_modules/punycode": {
2284
+ "version": "2.3.1",
2285
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
2286
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
2287
+ "dev": true,
2288
+ "license": "MIT",
2289
+ "engines": {
2290
+ "node": ">=6"
2291
+ }
2292
+ },
2293
+ "node_modules/react": {
2294
+ "version": "19.2.0",
2295
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
2296
+ "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
2297
+ "license": "MIT",
2298
+ "engines": {
2299
+ "node": ">=0.10.0"
2300
+ }
2301
+ },
2302
+ "node_modules/react-dom": {
2303
+ "version": "19.2.0",
2304
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
2305
+ "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
2306
+ "license": "MIT",
2307
+ "dependencies": {
2308
+ "scheduler": "^0.27.0"
2309
+ },
2310
+ "peerDependencies": {
2311
+ "react": "^19.2.0"
2312
+ }
2313
+ },
2314
+ "node_modules/react-refresh": {
2315
+ "version": "0.18.0",
2316
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
2317
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
2318
+ "dev": true,
2319
+ "license": "MIT",
2320
+ "engines": {
2321
+ "node": ">=0.10.0"
2322
+ }
2323
+ },
2324
+ "node_modules/resolve-from": {
2325
+ "version": "4.0.0",
2326
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
2327
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
2328
+ "dev": true,
2329
+ "license": "MIT",
2330
+ "engines": {
2331
+ "node": ">=4"
2332
+ }
2333
+ },
2334
+ "node_modules/rolldown": {
2335
+ "version": "1.0.0-beta.50",
2336
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.50.tgz",
2337
+ "integrity": "sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A==",
2338
+ "dev": true,
2339
+ "license": "MIT",
2340
+ "dependencies": {
2341
+ "@oxc-project/types": "=0.97.0",
2342
+ "@rolldown/pluginutils": "1.0.0-beta.50"
2343
+ },
2344
+ "bin": {
2345
+ "rolldown": "bin/cli.mjs"
2346
+ },
2347
+ "engines": {
2348
+ "node": "^20.19.0 || >=22.12.0"
2349
+ },
2350
+ "optionalDependencies": {
2351
+ "@rolldown/binding-android-arm64": "1.0.0-beta.50",
2352
+ "@rolldown/binding-darwin-arm64": "1.0.0-beta.50",
2353
+ "@rolldown/binding-darwin-x64": "1.0.0-beta.50",
2354
+ "@rolldown/binding-freebsd-x64": "1.0.0-beta.50",
2355
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.50",
2356
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.50",
2357
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.50",
2358
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.50",
2359
+ "@rolldown/binding-linux-x64-musl": "1.0.0-beta.50",
2360
+ "@rolldown/binding-openharmony-arm64": "1.0.0-beta.50",
2361
+ "@rolldown/binding-wasm32-wasi": "1.0.0-beta.50",
2362
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.50",
2363
+ "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.50",
2364
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.50"
2365
+ }
2366
+ },
2367
+ "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
2368
+ "version": "1.0.0-beta.50",
2369
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.50.tgz",
2370
+ "integrity": "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==",
2371
+ "dev": true,
2372
+ "license": "MIT"
2373
+ },
2374
+ "node_modules/scheduler": {
2375
+ "version": "0.27.0",
2376
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
2377
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
2378
+ "license": "MIT"
2379
+ },
2380
+ "node_modules/semver": {
2381
+ "version": "6.3.1",
2382
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
2383
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
2384
+ "dev": true,
2385
+ "license": "ISC",
2386
+ "bin": {
2387
+ "semver": "bin/semver.js"
2388
+ }
2389
+ },
2390
+ "node_modules/shebang-command": {
2391
+ "version": "2.0.0",
2392
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
2393
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
2394
+ "dev": true,
2395
+ "license": "MIT",
2396
+ "dependencies": {
2397
+ "shebang-regex": "^3.0.0"
2398
+ },
2399
+ "engines": {
2400
+ "node": ">=8"
2401
+ }
2402
+ },
2403
+ "node_modules/shebang-regex": {
2404
+ "version": "3.0.0",
2405
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
2406
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
2407
+ "dev": true,
2408
+ "license": "MIT",
2409
+ "engines": {
2410
+ "node": ">=8"
2411
+ }
2412
+ },
2413
+ "node_modules/source-map-js": {
2414
+ "version": "1.2.1",
2415
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
2416
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
2417
+ "dev": true,
2418
+ "license": "BSD-3-Clause",
2419
+ "engines": {
2420
+ "node": ">=0.10.0"
2421
+ }
2422
+ },
2423
+ "node_modules/strip-json-comments": {
2424
+ "version": "3.1.1",
2425
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
2426
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
2427
+ "dev": true,
2428
+ "license": "MIT",
2429
+ "engines": {
2430
+ "node": ">=8"
2431
+ },
2432
+ "funding": {
2433
+ "url": "https://github.com/sponsors/sindresorhus"
2434
+ }
2435
+ },
2436
+ "node_modules/supports-color": {
2437
+ "version": "7.2.0",
2438
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
2439
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
2440
+ "dev": true,
2441
+ "license": "MIT",
2442
+ "dependencies": {
2443
+ "has-flag": "^4.0.0"
2444
+ },
2445
+ "engines": {
2446
+ "node": ">=8"
2447
+ }
2448
+ },
2449
+ "node_modules/tinyglobby": {
2450
+ "version": "0.2.15",
2451
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
2452
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
2453
+ "dev": true,
2454
+ "license": "MIT",
2455
+ "dependencies": {
2456
+ "fdir": "^6.5.0",
2457
+ "picomatch": "^4.0.3"
2458
+ },
2459
+ "engines": {
2460
+ "node": ">=12.0.0"
2461
+ },
2462
+ "funding": {
2463
+ "url": "https://github.com/sponsors/SuperchupuDev"
2464
+ }
2465
+ },
2466
+ "node_modules/tslib": {
2467
+ "version": "2.8.1",
2468
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
2469
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
2470
+ "dev": true,
2471
+ "license": "0BSD",
2472
+ "optional": true
2473
+ },
2474
+ "node_modules/type-check": {
2475
+ "version": "0.4.0",
2476
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
2477
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
2478
+ "dev": true,
2479
+ "license": "MIT",
2480
+ "dependencies": {
2481
+ "prelude-ls": "^1.2.1"
2482
+ },
2483
+ "engines": {
2484
+ "node": ">= 0.8.0"
2485
+ }
2486
+ },
2487
+ "node_modules/update-browserslist-db": {
2488
+ "version": "1.1.4",
2489
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
2490
+ "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
2491
+ "dev": true,
2492
+ "funding": [
2493
+ {
2494
+ "type": "opencollective",
2495
+ "url": "https://opencollective.com/browserslist"
2496
+ },
2497
+ {
2498
+ "type": "tidelift",
2499
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
2500
+ },
2501
+ {
2502
+ "type": "github",
2503
+ "url": "https://github.com/sponsors/ai"
2504
+ }
2505
+ ],
2506
+ "license": "MIT",
2507
+ "dependencies": {
2508
+ "escalade": "^3.2.0",
2509
+ "picocolors": "^1.1.1"
2510
+ },
2511
+ "bin": {
2512
+ "update-browserslist-db": "cli.js"
2513
+ },
2514
+ "peerDependencies": {
2515
+ "browserslist": ">= 4.21.0"
2516
+ }
2517
+ },
2518
+ "node_modules/uri-js": {
2519
+ "version": "4.4.1",
2520
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
2521
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
2522
+ "dev": true,
2523
+ "license": "BSD-2-Clause",
2524
+ "dependencies": {
2525
+ "punycode": "^2.1.0"
2526
+ }
2527
+ },
2528
+ "node_modules/vite": {
2529
+ "name": "rolldown-vite",
2530
+ "version": "7.2.5",
2531
+ "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.2.5.tgz",
2532
+ "integrity": "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==",
2533
+ "dev": true,
2534
+ "license": "MIT",
2535
+ "dependencies": {
2536
+ "@oxc-project/runtime": "0.97.0",
2537
+ "fdir": "^6.5.0",
2538
+ "lightningcss": "^1.30.2",
2539
+ "picomatch": "^4.0.3",
2540
+ "postcss": "^8.5.6",
2541
+ "rolldown": "1.0.0-beta.50",
2542
+ "tinyglobby": "^0.2.15"
2543
+ },
2544
+ "bin": {
2545
+ "vite": "bin/vite.js"
2546
+ },
2547
+ "engines": {
2548
+ "node": "^20.19.0 || >=22.12.0"
2549
+ },
2550
+ "funding": {
2551
+ "url": "https://github.com/vitejs/vite?sponsor=1"
2552
+ },
2553
+ "optionalDependencies": {
2554
+ "fsevents": "~2.3.3"
2555
+ },
2556
+ "peerDependencies": {
2557
+ "@types/node": "^20.19.0 || >=22.12.0",
2558
+ "esbuild": "^0.25.0",
2559
+ "jiti": ">=1.21.0",
2560
+ "less": "^4.0.0",
2561
+ "sass": "^1.70.0",
2562
+ "sass-embedded": "^1.70.0",
2563
+ "stylus": ">=0.54.8",
2564
+ "sugarss": "^5.0.0",
2565
+ "terser": "^5.16.0",
2566
+ "tsx": "^4.8.1",
2567
+ "yaml": "^2.4.2"
2568
+ },
2569
+ "peerDependenciesMeta": {
2570
+ "@types/node": {
2571
+ "optional": true
2572
+ },
2573
+ "esbuild": {
2574
+ "optional": true
2575
+ },
2576
+ "jiti": {
2577
+ "optional": true
2578
+ },
2579
+ "less": {
2580
+ "optional": true
2581
+ },
2582
+ "sass": {
2583
+ "optional": true
2584
+ },
2585
+ "sass-embedded": {
2586
+ "optional": true
2587
+ },
2588
+ "stylus": {
2589
+ "optional": true
2590
+ },
2591
+ "sugarss": {
2592
+ "optional": true
2593
+ },
2594
+ "terser": {
2595
+ "optional": true
2596
+ },
2597
+ "tsx": {
2598
+ "optional": true
2599
+ },
2600
+ "yaml": {
2601
+ "optional": true
2602
+ }
2603
+ }
2604
+ },
2605
+ "node_modules/which": {
2606
+ "version": "2.0.2",
2607
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
2608
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
2609
+ "dev": true,
2610
+ "license": "ISC",
2611
+ "dependencies": {
2612
+ "isexe": "^2.0.0"
2613
+ },
2614
+ "bin": {
2615
+ "node-which": "bin/node-which"
2616
+ },
2617
+ "engines": {
2618
+ "node": ">= 8"
2619
+ }
2620
+ },
2621
+ "node_modules/word-wrap": {
2622
+ "version": "1.2.5",
2623
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
2624
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
2625
+ "dev": true,
2626
+ "license": "MIT",
2627
+ "engines": {
2628
+ "node": ">=0.10.0"
2629
+ }
2630
+ },
2631
+ "node_modules/yallist": {
2632
+ "version": "3.1.1",
2633
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
2634
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
2635
+ "dev": true,
2636
+ "license": "ISC"
2637
+ },
2638
+ "node_modules/yocto-queue": {
2639
+ "version": "0.1.0",
2640
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
2641
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
2642
+ "dev": true,
2643
+ "license": "MIT",
2644
+ "engines": {
2645
+ "node": ">=10"
2646
+ },
2647
+ "funding": {
2648
+ "url": "https://github.com/sponsors/sindresorhus"
2649
+ }
2650
+ },
2651
+ "node_modules/zod": {
2652
+ "version": "4.1.12",
2653
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
2654
+ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
2655
+ "dev": true,
2656
+ "license": "MIT",
2657
+ "funding": {
2658
+ "url": "https://github.com/sponsors/colinhacks"
2659
+ }
2660
+ },
2661
+ "node_modules/zod-validation-error": {
2662
+ "version": "4.0.2",
2663
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
2664
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
2665
+ "dev": true,
2666
+ "license": "MIT",
2667
+ "engines": {
2668
+ "node": ">=18.0.0"
2669
+ },
2670
+ "peerDependencies": {
2671
+ "zod": "^3.25.0 || ^4.0.0"
2672
+ }
2673
+ }
2674
+ }
2675
+ }
frontend/package.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "react": "^19.2.0",
14
+ "react-dom": "^19.2.0"
15
+ },
16
+ "devDependencies": {
17
+ "@eslint/js": "^9.39.1",
18
+ "@types/react": "^19.2.5",
19
+ "@types/react-dom": "^19.2.3",
20
+ "@vitejs/plugin-react": "^5.1.1",
21
+ "eslint": "^9.39.1",
22
+ "eslint-plugin-react-hooks": "^7.0.1",
23
+ "eslint-plugin-react-refresh": "^0.4.24",
24
+ "globals": "^16.5.0",
25
+ "vite": "npm:rolldown-vite@7.2.5"
26
+ },
27
+ "overrides": {
28
+ "vite": "npm:rolldown-vite@7.2.5"
29
+ }
30
+ }
frontend/public/vite.svg ADDED
frontend/src/App.css ADDED
@@ -0,0 +1,885 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg-app: #0f172a;
3
+ --bg-sidebar: #1e293b;
4
+ --bg-panel: #1e293b;
5
+ --bg-input: #334155;
6
+ --border-color: #334155;
7
+ --primary: #3b82f6;
8
+ --primary-hover: #2563eb;
9
+ --text-main: #f8fafc;
10
+ --text-muted: #94a3b8;
11
+ --accent-gradient: linear-gradient(135deg, #3b82f6, #8b5cf6);
12
+
13
+ --radius-lg: 12px;
14
+ --radius-md: 8px;
15
+ --radius-sm: 4px;
16
+
17
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
18
+ }
19
+
20
+ @keyframes glow-rotate {
21
+ 0% {
22
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.4),
23
+ 0 0 25px rgba(59, 130, 246, 0.6),
24
+ 0 0 50px rgba(59, 130, 246, 0.3),
25
+ -10px 0 30px rgba(139, 92, 246, 0.4),
26
+ var(--shadow-lg);
27
+ }
28
+ 25% {
29
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.4),
30
+ 0 0 25px rgba(59, 130, 246, 0.6),
31
+ 0 0 50px rgba(59, 130, 246, 0.3),
32
+ 0 -10px 30px rgba(139, 92, 246, 0.4),
33
+ var(--shadow-lg);
34
+ }
35
+ 50% {
36
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.4),
37
+ 0 0 25px rgba(59, 130, 246, 0.6),
38
+ 0 0 50px rgba(59, 130, 246, 0.3),
39
+ 10px 0 30px rgba(139, 92, 246, 0.4),
40
+ var(--shadow-lg);
41
+ }
42
+ 75% {
43
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.4),
44
+ 0 0 25px rgba(59, 130, 246, 0.6),
45
+ 0 0 50px rgba(59, 130, 246, 0.3),
46
+ 0 10px 30px rgba(139, 92, 246, 0.4),
47
+ var(--shadow-lg);
48
+ }
49
+ 100% {
50
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.4),
51
+ 0 0 25px rgba(59, 130, 246, 0.6),
52
+ 0 0 50px rgba(59, 130, 246, 0.3),
53
+ -10px 0 30px rgba(139, 92, 246, 0.4),
54
+ var(--shadow-lg);
55
+ }
56
+ }
57
+
58
+ * {
59
+ box-sizing: border-box;
60
+ }
61
+
62
+ body {
63
+ margin: 0;
64
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
65
+ background-color: var(--bg-app);
66
+ color: var(--text-main);
67
+ -webkit-font-smoothing: antialiased;
68
+ }
69
+
70
+ #root {
71
+ height: 100vh;
72
+ display: flex;
73
+ overflow: hidden;
74
+ }
75
+
76
+ /* Layout */
77
+ .app-shell {
78
+ display: grid;
79
+ grid-template-columns: 280px 1fr;
80
+ width: 100%;
81
+ height: 100vh;
82
+ }
83
+
84
+ /* Sidebar */
85
+ .sidebar {
86
+ background-color: var(--bg-sidebar);
87
+ border-right: 1px solid var(--border-color);
88
+ padding: 1.5rem;
89
+ display: flex;
90
+ flex-direction: column;
91
+ gap: 2rem;
92
+ overflow-y: auto;
93
+ height: 100%;
94
+ min-height: 0;
95
+ }
96
+
97
+ .brand {
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 0.75rem;
101
+ }
102
+
103
+ .brand h1 {
104
+ font-size: 1.25rem;
105
+ font-weight: 700;
106
+ margin: 0;
107
+ background: var(--accent-gradient);
108
+ -webkit-background-clip: text;
109
+ -webkit-text-fill-color: transparent;
110
+ }
111
+
112
+ .sidebar-section h3 {
113
+ font-size: 0.75rem;
114
+ text-transform: uppercase;
115
+ letter-spacing: 0.05em;
116
+ color: var(--text-muted);
117
+ margin-bottom: 1rem;
118
+ }
119
+
120
+ /* Dataset Controls */
121
+ .dataset-control {
122
+ display: flex;
123
+ flex-direction: column;
124
+ gap: 1rem;
125
+ }
126
+
127
+ .input-group {
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 0.5rem;
131
+ }
132
+
133
+ .input-group label {
134
+ font-size: 0.875rem;
135
+ color: var(--text-muted);
136
+ }
137
+
138
+ .input-group input {
139
+ background: var(--bg-app);
140
+ border: 1px solid var(--border-color);
141
+ color: var(--text-main);
142
+ padding: 0.5rem;
143
+ border-radius: var(--radius-md);
144
+ font-size: 0.875rem;
145
+ }
146
+
147
+ .btn-secondary {
148
+ background: transparent;
149
+ border: 1px solid var(--border-color);
150
+ color: var(--text-main);
151
+ padding: 0.5rem;
152
+ border-radius: var(--radius-md);
153
+ cursor: pointer;
154
+ font-size: 0.875rem;
155
+ transition: all 0.2s;
156
+ }
157
+
158
+ .btn-secondary:hover:not(:disabled) {
159
+ background: var(--bg-input);
160
+ }
161
+
162
+ .btn-primary {
163
+ background: var(--primary);
164
+ border: none;
165
+ color: white;
166
+ padding: 0.5rem 1rem;
167
+ border-radius: var(--radius-md);
168
+ cursor: pointer;
169
+ font-weight: 500;
170
+ transition: background 0.2s;
171
+ }
172
+
173
+ .btn-primary:hover:not(:disabled) {
174
+ background: var(--primary-hover);
175
+ }
176
+
177
+ .upload-label {
178
+ display: block;
179
+ text-align: center;
180
+ background: var(--bg-input);
181
+ border: 1px dashed var(--text-muted);
182
+ padding: 1rem;
183
+ border-radius: var(--radius-md);
184
+ cursor: pointer;
185
+ font-size: 0.875rem;
186
+ transition: all 0.2s;
187
+ }
188
+
189
+ .upload-label:hover {
190
+ border-color: var(--primary);
191
+ color: var(--primary);
192
+ }
193
+
194
+ .upload-label input {
195
+ display: none;
196
+ }
197
+
198
+ .dataset-list {
199
+ list-style: none;
200
+ padding: 0;
201
+ margin: 0;
202
+ display: flex;
203
+ flex-direction: column;
204
+ gap: 0.5rem;
205
+ }
206
+
207
+ .dataset-item {
208
+ display: flex;
209
+ justify-content: space-between;
210
+ align-items: center;
211
+ padding: 0.5rem;
212
+ background: var(--bg-app);
213
+ border-radius: var(--radius-md);
214
+ font-size: 0.8rem;
215
+ }
216
+
217
+ .dataset-item strong {
218
+ color: var(--primary);
219
+ }
220
+
221
+ /* Main Content */
222
+ .main-content {
223
+ display: grid;
224
+ grid-template-columns: 1fr 400px;
225
+ overflow: hidden;
226
+ height: 100%;
227
+ min-height: 0;
228
+ }
229
+
230
+ /* Chat Area */
231
+ .chat-section {
232
+ display: flex;
233
+ flex-direction: column;
234
+ height: 100%;
235
+ min-height: 0;
236
+ position: relative;
237
+ overflow: hidden;
238
+ }
239
+
240
+ .chat-header {
241
+ padding: 1rem 2rem;
242
+ border-bottom: 1px solid var(--border-color);
243
+ display: flex;
244
+ justify-content: space-between;
245
+ align-items: center;
246
+ background: rgba(15, 23, 42, 0.8);
247
+ backdrop-filter: blur(8px);
248
+ }
249
+
250
+ .status-indicator {
251
+ display: flex;
252
+ align-items: center;
253
+ gap: 0.5rem;
254
+ font-size: 0.875rem;
255
+ color: var(--text-muted);
256
+ }
257
+
258
+ .dot {
259
+ width: 8px;
260
+ height: 8px;
261
+ border-radius: 50%;
262
+ background-color: #22c55e;
263
+ }
264
+
265
+ .dot.busy {
266
+ background-color: #eab308;
267
+ animation: pulse 1.5s infinite;
268
+ }
269
+
270
+ @keyframes pulse {
271
+ 0% { opacity: 1; }
272
+ 50% { opacity: 0.5; }
273
+ 100% { opacity: 1; }
274
+ }
275
+
276
+ .messages-container {
277
+ flex: 1;
278
+ overflow-y: auto;
279
+ padding: 2rem;
280
+ display: flex;
281
+ flex-direction: column;
282
+ gap: 1.5rem;
283
+ min-height: 0;
284
+ scroll-behavior: smooth;
285
+ }
286
+
287
+ .welcome-screen {
288
+ display: flex;
289
+ flex-direction: column;
290
+ align-items: center;
291
+ justify-content: center;
292
+ height: 100%;
293
+ text-align: center;
294
+ color: var(--text-muted);
295
+ max-width: 600px;
296
+ margin: 0 auto;
297
+ }
298
+
299
+ .welcome-screen h2 {
300
+ color: var(--text-main);
301
+ font-size: 2rem;
302
+ margin-bottom: 1rem;
303
+ }
304
+
305
+ .quick-prompts {
306
+ display: grid;
307
+ grid-template-columns: repeat(2, 1fr);
308
+ gap: 1rem;
309
+ margin-top: 2rem;
310
+ width: 100%;
311
+ }
312
+
313
+ .prompt-card {
314
+ background: var(--bg-panel);
315
+ border: 1px solid var(--border-color);
316
+ padding: 1rem;
317
+ border-radius: var(--radius-lg);
318
+ text-align: left;
319
+ cursor: pointer;
320
+ transition: all 0.2s;
321
+ }
322
+
323
+ .prompt-card:hover {
324
+ border-color: var(--primary);
325
+ transform: translateY(-2px);
326
+ }
327
+
328
+ /* Messages */
329
+ .message {
330
+ display: flex;
331
+ gap: 1rem;
332
+ max-width: 800px;
333
+ }
334
+
335
+ .message.user {
336
+ flex-direction: row-reverse;
337
+ align-self: flex-end;
338
+ }
339
+
340
+ .avatar {
341
+ width: 32px;
342
+ height: 32px;
343
+ border-radius: 50%;
344
+ display: flex;
345
+ align-items: center;
346
+ justify-content: center;
347
+ font-size: 1.2rem;
348
+ flex-shrink: 0;
349
+ }
350
+
351
+ .message.agent .avatar {
352
+ background: var(--primary);
353
+ color: white;
354
+ }
355
+
356
+ .message.user .avatar {
357
+ background: var(--text-muted);
358
+ color: var(--bg-app);
359
+ }
360
+
361
+ .message-content {
362
+ background: var(--bg-panel);
363
+ padding: 1rem 1.5rem;
364
+ border-radius: var(--radius-lg);
365
+ border-top-left-radius: 2px;
366
+ line-height: 1.6;
367
+ font-size: 0.95rem;
368
+ }
369
+
370
+ .message.user .message-content {
371
+ background: var(--primary);
372
+ color: white;
373
+ border-top-left-radius: var(--radius-lg);
374
+ border-top-right-radius: 2px;
375
+ }
376
+
377
+ .input-area {
378
+ padding: 2rem;
379
+ background: linear-gradient(to top, var(--bg-app) 80%, transparent);
380
+ }
381
+
382
+ .input-wrapper {
383
+ max-width: 1500px;
384
+ margin: 0 auto;
385
+ position: relative;
386
+ }
387
+ .input-shell {
388
+ display: flex;
389
+ align-items: flex-end;
390
+ gap: 1rem;
391
+ background: var(--bg-panel);
392
+ border: 2px solid var(--border-color);
393
+ padding: 1.5rem 1.5rem 1.5rem 2rem;
394
+ border-radius: 999px;
395
+ box-shadow: var(--shadow-lg);
396
+ transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
397
+ }
398
+
399
+ .input-shell:focus-within {
400
+ border-color: var(--primary);
401
+ animation: glow-rotate 3s ease-in-out infinite;
402
+ }
403
+
404
+ .input-wrapper textarea {
405
+ flex: 1;
406
+ width: 100%;
407
+ background: transparent;
408
+ border: none;
409
+ padding: 0;
410
+ border-radius: 0;
411
+ color: var(--text-main);
412
+ font-size: 1.05rem;
413
+ resize: none;
414
+ min-height: 50px;
415
+ line-height: 1.6;
416
+ overflow: hidden;
417
+ transition: border-color 0.2s, box-shadow 0.2s;
418
+ }
419
+
420
+ .input-wrapper textarea:focus {
421
+ outline: none;
422
+ }
423
+
424
+ .send-btn {
425
+ flex-shrink: 0;
426
+ border: none;
427
+ background: linear-gradient(135deg, #60a5fa, #8b5cf6);
428
+ width: 80px;
429
+ height: 56px;
430
+ border-radius: 999px;
431
+ cursor: pointer;
432
+ display: flex;
433
+ align-items: center;
434
+ justify-content: center;
435
+ color: #fff;
436
+ box-shadow: 0 10px 25px rgba(99, 102, 241, 0.35);
437
+ transition: transform 0.2s, box-shadow 0.2s, opacity 0.2s;
438
+ }
439
+
440
+ .send-btn svg {
441
+ width: 26px;
442
+ height: 26px;
443
+ stroke: currentColor;
444
+ fill: none;
445
+ }
446
+
447
+ .send-btn:hover:not(:disabled) {
448
+ transform: translateX(2px) scale(1.02);
449
+ box-shadow: 0 12px 28px rgba(99, 102, 241, 0.5);
450
+ }
451
+
452
+ .send-btn:disabled {
453
+ opacity: 0.6;
454
+ cursor: not-allowed;
455
+ }
456
+
457
+ /* Results Panel */
458
+ .results-section {
459
+ background: var(--bg-sidebar);
460
+ border-left: 1px solid var(--border-color);
461
+ display: flex;
462
+ flex-direction: column;
463
+ height: 100%;
464
+ min-height: 0;
465
+ overflow: hidden;
466
+ }
467
+
468
+ .results-header {
469
+ padding: 1rem;
470
+ border-bottom: 1px solid var(--border-color);
471
+ }
472
+
473
+ .results-header h2 {
474
+ margin: 0;
475
+ font-size: 1rem;
476
+ font-weight: 600;
477
+ }
478
+
479
+ .results-content {
480
+ flex: 1;
481
+ overflow-y: auto;
482
+ padding: 1rem;
483
+ display: flex;
484
+ flex-direction: column;
485
+ gap: 1rem;
486
+ min-height: 0;
487
+ }
488
+
489
+ .result-card {
490
+ background: var(--bg-app);
491
+ border: 1px solid var(--border-color);
492
+ border-radius: var(--radius-md);
493
+ padding: 1rem;
494
+ }
495
+
496
+ .card-header {
497
+ display: flex;
498
+ justify-content: space-between;
499
+ align-items: center;
500
+ margin-bottom: 0.75rem;
501
+ font-size: 0.8rem;
502
+ text-transform: uppercase;
503
+ letter-spacing: 0.05em;
504
+ color: var(--text-muted);
505
+ }
506
+
507
+ .badge {
508
+ background: rgba(59, 130, 246, 0.1);
509
+ color: var(--primary);
510
+ padding: 0.2rem 0.5rem;
511
+ border-radius: 4px;
512
+ font-size: 0.7rem;
513
+ }
514
+
515
+ .trend-grid {
516
+ display: grid;
517
+ grid-template-columns: repeat(3, 1fr);
518
+ gap: 0.5rem;
519
+ text-align: center;
520
+ }
521
+
522
+ .trend-grid div {
523
+ display: flex;
524
+ flex-direction: column;
525
+ gap: 0.25rem;
526
+ }
527
+
528
+ .trend-grid span {
529
+ font-size: 0.75rem;
530
+ color: var(--text-muted);
531
+ }
532
+
533
+ .trend-grid strong {
534
+ font-size: 1rem;
535
+ }
536
+
537
+ .anomaly-list {
538
+ list-style: none;
539
+ padding: 0;
540
+ margin: 0;
541
+ }
542
+
543
+ .anomaly-list li {
544
+ display: flex;
545
+ justify-content: space-between;
546
+ padding: 0.5rem 0;
547
+ border-bottom: 1px solid var(--border-color);
548
+ font-size: 0.875rem;
549
+ }
550
+
551
+ .anomaly-list li:last-child {
552
+ border-bottom: none;
553
+ }
554
+
555
+ code {
556
+ display: block;
557
+ background: #000;
558
+ padding: 0.75rem;
559
+ border-radius: var(--radius-sm);
560
+ font-family: 'Fira Code', monospace;
561
+ font-size: 0.8rem;
562
+ overflow-x: auto;
563
+ color: #a5b4fc;
564
+ }
565
+
566
+ .viz-container img {
567
+ width: 100%;
568
+ border-radius: var(--radius-sm);
569
+ border: 1px solid var(--border-color);
570
+ }
571
+
572
+ .table-container {
573
+ overflow-x: auto;
574
+ }
575
+
576
+ table {
577
+ width: 100%;
578
+ border-collapse: collapse;
579
+ font-size: 0.8rem;
580
+ }
581
+
582
+ th, td {
583
+ padding: 0.5rem;
584
+ text-align: left;
585
+ border-bottom: 1px solid var(--border-color);
586
+ }
587
+
588
+ th {
589
+ color: var(--text-muted);
590
+ font-weight: 500;
591
+ }
592
+
593
+ .download-btn {
594
+ display: block;
595
+ width: 100%;
596
+ text-align: center;
597
+ background: var(--bg-input);
598
+ color: var(--text-main);
599
+ text-decoration: none;
600
+ padding: 0.75rem;
601
+ border-radius: var(--radius-md);
602
+ font-size: 0.875rem;
603
+ transition: background 0.2s;
604
+ }
605
+
606
+ .download-btn:hover {
607
+ background: var(--border-color);
608
+ }
609
+
610
+ .empty-state-results {
611
+ text-align: center;
612
+ color: var(--text-muted);
613
+ padding: 2rem;
614
+ font-size: 0.9rem;
615
+ }
616
+
617
+ /* Responsive */
618
+ @media (max-width: 1024px) {
619
+ .main-content {
620
+ grid-template-columns: 1fr;
621
+ }
622
+ .results-section {
623
+ display: none; /* Or make it a modal/drawer */
624
+ }
625
+ .results-section.active {
626
+ display: flex;
627
+ position: fixed;
628
+ inset: 0;
629
+ z-index: 50;
630
+ }
631
+ }
632
+
633
+ .viz-preview img {
634
+ width: 100%;
635
+ display: block;
636
+ }
637
+
638
+ .chart-summary {
639
+ margin-top: 0.75rem;
640
+ font-size: 0.9rem;
641
+ color: rgba(255, 255, 255, 0.78);
642
+ }
643
+
644
+ .report-chip {
645
+ display: inline-flex;
646
+ align-items: center;
647
+ gap: 0.4rem;
648
+ margin-top: 0.75rem;
649
+ padding: 0.4rem 0.9rem;
650
+ border-radius: 999px;
651
+ text-decoration: none;
652
+ color: #1b1d34;
653
+ background: #9ef4c5;
654
+ font-weight: 600;
655
+ }
656
+
657
+ .diagnostic-chips {
658
+ display: flex;
659
+ flex-wrap: wrap;
660
+ gap: 0.4rem;
661
+ margin-top: 0.6rem;
662
+ }
663
+
664
+ .diagnostic-chip {
665
+ font-size: 0.75rem;
666
+ border-radius: 999px;
667
+ padding: 0.25rem 0.7rem;
668
+ border: 1px solid rgba(255, 255, 255, 0.3);
669
+ background: rgba(255, 255, 255, 0.08);
670
+ }
671
+
672
+ .input-area {
673
+ margin-top: 1.5rem;
674
+ display: flex;
675
+ gap: 0.75rem;
676
+ }
677
+
678
+ .input-area input {
679
+ flex: 1;
680
+ border-radius: 999px;
681
+ border: 1px solid rgba(255, 255, 255, 0.15);
682
+ background: rgba(255, 255, 255, 0.04);
683
+ color: inherit;
684
+ padding: 0.95rem 1.4rem;
685
+ font-size: 1.05rem;
686
+ }
687
+
688
+ .input-area input:focus {
689
+ outline: none;
690
+ border-color: rgba(122, 131, 255, 0.6);
691
+ }
692
+
693
+ .input-area button {
694
+ border: none;
695
+ border-radius: 999px;
696
+ padding: 0.95rem 1.8rem;
697
+ background: linear-gradient(135deg, #7a83ff, #5cd4f4);
698
+ color: #05070f;
699
+ font-weight: 600;
700
+ cursor: pointer;
701
+ transition: transform 0.2s ease;
702
+ }
703
+
704
+ .input-area button:disabled {
705
+ opacity: 0.5;
706
+ cursor: not-allowed;
707
+ }
708
+
709
+ .input-area button:not(:disabled):hover {
710
+ transform: translateY(-1px);
711
+ }
712
+
713
+ .error-text {
714
+ margin-top: 0.75rem;
715
+ color: #ffb4b4;
716
+ font-size: 0.9rem;
717
+ }
718
+
719
+ .result-stack {
720
+ display: flex;
721
+ flex-direction: column;
722
+ gap: 1.25rem;
723
+ }
724
+
725
+ .insight-card {
726
+ background: rgba(158, 244, 197, 0.08);
727
+ border: 1px solid rgba(158, 244, 197, 0.4);
728
+ border-radius: 16px;
729
+ padding: 1rem 1.2rem;
730
+ color: #d4ffe7;
731
+ }
732
+
733
+ .meta-card {
734
+ border-radius: 16px;
735
+ padding: 1rem 1.2rem;
736
+ border: 1px solid rgba(255, 255, 255, 0.08);
737
+ background: rgba(255, 255, 255, 0.03);
738
+ }
739
+
740
+ .trend-grid {
741
+ display: grid;
742
+ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
743
+ gap: 0.75rem;
744
+ margin-top: 0.75rem;
745
+ }
746
+
747
+ .trend-grid span {
748
+ display: block;
749
+ font-size: 0.75rem;
750
+ text-transform: uppercase;
751
+ letter-spacing: 0.08em;
752
+ color: rgba(255, 255, 255, 0.5);
753
+ }
754
+
755
+ .trend-grid strong {
756
+ display: block;
757
+ font-size: 1.1rem;
758
+ }
759
+
760
+ .anomaly-list {
761
+ list-style: none;
762
+ margin: 0.75rem 0 0;
763
+ padding: 0;
764
+ display: flex;
765
+ flex-direction: column;
766
+ gap: 0.35rem;
767
+ }
768
+
769
+ .anomaly-list li {
770
+ display: flex;
771
+ gap: 0.5rem;
772
+ flex-wrap: wrap;
773
+ font-size: 0.85rem;
774
+ color: rgba(255, 255, 255, 0.85);
775
+ }
776
+
777
+ .anomaly-list li strong {
778
+ color: #ffffff;
779
+ }
780
+
781
+ .meta-head {
782
+ display: flex;
783
+ justify-content: space-between;
784
+ font-size: 0.85rem;
785
+ color: rgba(255, 255, 255, 0.65);
786
+ text-transform: uppercase;
787
+ letter-spacing: 0.08em;
788
+ }
789
+
790
+ .badge {
791
+ padding: 0.1rem 0.6rem;
792
+ border-radius: 999px;
793
+ border: 1px solid rgba(255, 255, 255, 0.2);
794
+ }
795
+
796
+ .meta-card code {
797
+ display: block;
798
+ margin-top: 0.5rem;
799
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
800
+ color: #9bd0ff;
801
+ white-space: pre-wrap;
802
+ }
803
+
804
+ .data-preview {
805
+ border-radius: 18px;
806
+ border: 1px solid rgba(255, 255, 255, 0.09);
807
+ background: rgba(1, 2, 15, 0.4);
808
+ padding: 1rem;
809
+ }
810
+
811
+ .table-wrapper {
812
+ overflow-x: auto;
813
+ margin-top: 0.75rem;
814
+ }
815
+
816
+ table {
817
+ width: 100%;
818
+ border-collapse: collapse;
819
+ font-size: 0.9rem;
820
+ }
821
+
822
+ th,
823
+ td {
824
+ padding: 0.55rem 0.75rem;
825
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
826
+ text-align: left;
827
+ }
828
+
829
+ th {
830
+ font-weight: 600;
831
+ color: rgba(255, 255, 255, 0.7);
832
+ }
833
+
834
+ .viz-card {
835
+ border-radius: 18px;
836
+ border: 1px solid rgba(255, 255, 255, 0.09);
837
+ background: rgba(255, 255, 255, 0.03);
838
+ padding: 1rem;
839
+ }
840
+
841
+ .viz-card img {
842
+ width: 100%;
843
+ border-radius: 12px;
844
+ }
845
+
846
+ .report-button {
847
+ display: inline-flex;
848
+ justify-content: center;
849
+ padding: 0.9rem 1.5rem;
850
+ border-radius: 12px;
851
+ text-decoration: none;
852
+ font-weight: 600;
853
+ background: linear-gradient(135deg, #4ade80, #22d3ee);
854
+ color: #041019;
855
+ }
856
+
857
+ .empty-state {
858
+ padding: 2rem;
859
+ text-align: center;
860
+ color: rgba(255, 255, 255, 0.6);
861
+ }
862
+
863
+ @media (max-width: 1100px) {
864
+ .dashboard {
865
+ grid-template-columns: 1fr;
866
+ }
867
+
868
+ .messages-area {
869
+ max-height: 45vh;
870
+ }
871
+ }
872
+
873
+ @media (max-width: 600px) {
874
+ .app-shell {
875
+ padding: 2rem 1rem 3rem;
876
+ }
877
+
878
+ .panel {
879
+ padding: 1.25rem;
880
+ }
881
+
882
+ .bubble {
883
+ max-width: 100%;
884
+ }
885
+ }
frontend/src/App.jsx ADDED
@@ -0,0 +1,546 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef, useEffect } from 'react'
2
+ import './App.css'
3
+
4
+ const QUICK_PROMPTS = [
5
+ 'What were total sales by category?',
6
+ 'Compare revenue by region last quarter',
7
+ 'Show monthly sales trend for 2023',
8
+ 'Top products by revenue'
9
+ ]
10
+
11
+ const DEFAULT_AGENT_MESSAGE = {
12
+ type: 'agent',
13
+ content: 'Hi! Ask me anything about your business data and I will generate SQL, visuals, and insights for you.'
14
+ }
15
+
16
+ function App() {
17
+ const sessionIdRef = useRef(null)
18
+ if (!sessionIdRef.current) {
19
+ sessionIdRef.current = crypto?.randomUUID ? crypto.randomUUID() : `session-${Date.now()}`
20
+ }
21
+ const sessionId = sessionIdRef.current
22
+
23
+ const [messages, setMessages] = useState([DEFAULT_AGENT_MESSAGE])
24
+ const [input, setInput] = useState('')
25
+ const [loading, setLoading] = useState(false)
26
+ const [activeResult, setActiveResult] = useState(null)
27
+ const [error, setError] = useState(null)
28
+ const [datasetCatalog, setDatasetCatalog] = useState([])
29
+ const [catalogLoading, setCatalogLoading] = useState(false)
30
+ const [tableName, setTableName] = useState('sales')
31
+ const [uploading, setUploading] = useState(false)
32
+ const [uploadError, setUploadError] = useState(null)
33
+ const [clearing, setClearing] = useState(false)
34
+ const messagesEndRef = useRef(null)
35
+ const inputRef = useRef(null)
36
+
37
+ const [datasetPreview, setDatasetPreview] = useState(null)
38
+
39
+ const dataPreview = activeResult?.data ?? []
40
+ const previewColumns = dataPreview.length ? Object.keys(dataPreview[0]) : []
41
+ const previewRows = dataPreview.slice(0, 5)
42
+
43
+ const scrollToBottom = () => {
44
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
45
+ }
46
+
47
+ useEffect(() => {
48
+ scrollToBottom()
49
+ }, [messages])
50
+
51
+ useEffect(() => {
52
+ if (!inputRef.current) return
53
+ const el = inputRef.current
54
+ el.style.height = 'auto'
55
+ const nextHeight = Math.min(el.scrollHeight, 160)
56
+ el.style.height = `${nextHeight}px`
57
+ }, [input])
58
+
59
+ const fetchDatasetCatalog = async () => {
60
+ setCatalogLoading(true)
61
+ try {
62
+ const res = await fetch('http://localhost:8000/api/datasets')
63
+ const payload = await res.json()
64
+ setDatasetCatalog(payload.tables || [])
65
+ } catch (err) {
66
+ console.error('Failed to fetch dataset catalog', err)
67
+ } finally {
68
+ setCatalogLoading(false)
69
+ }
70
+ }
71
+
72
+ useEffect(() => {
73
+ fetchDatasetCatalog()
74
+ }, [])
75
+
76
+ const sendQuery = async (question) => {
77
+ const trimmed = question.trim()
78
+ if (!trimmed) return
79
+
80
+ const userMessage = { type: 'user', content: trimmed }
81
+ setMessages((prev) => [...prev, userMessage])
82
+ setInput('')
83
+ setLoading(true)
84
+ setError(null)
85
+
86
+ try {
87
+ const response = await fetch('http://localhost:8000/api/analyze', {
88
+ method: 'POST',
89
+ headers: {
90
+ 'Content-Type': 'application/json'
91
+ },
92
+ body: JSON.stringify({ query: trimmed, session_id: sessionId })
93
+ })
94
+
95
+ const data = await response.json()
96
+
97
+ if (!response.ok || data.error) {
98
+ const message = data.error || 'Something went wrong.'
99
+ setError(message)
100
+ setMessages((prev) => [...prev, { type: 'agent', content: `⚠️ ${message}` }])
101
+ return
102
+ }
103
+
104
+ const agentPayload = {
105
+ type: 'agent',
106
+ content: data.insights || 'Analysis complete.',
107
+ sql: data.sql_query,
108
+ visualization: data.visualization_url,
109
+ chartSummary: data.visualization_summary,
110
+ trendSummary: data.trend_analysis?.summary,
111
+ anomalySummary: data.anomaly_analysis?.summary,
112
+ report: data.report_url
113
+ }
114
+
115
+ setMessages((prev) => [...prev, agentPayload])
116
+ setActiveResult(data)
117
+ } catch (err) {
118
+ const fallback = `Network error: ${err.message}`
119
+ setError(fallback)
120
+ setMessages((prev) => [...prev, { type: 'agent', content: fallback }])
121
+ } finally {
122
+ setLoading(false)
123
+ }
124
+ }
125
+
126
+ const handleSubmit = (e) => {
127
+ e.preventDefault()
128
+ if (loading) return
129
+ sendQuery(input)
130
+ }
131
+
132
+ const handlePromptClick = (prompt) => {
133
+ if (loading) return
134
+ setInput(prompt)
135
+ sendQuery(prompt)
136
+ }
137
+
138
+ const handleDatasetUpload = async (event) => {
139
+ const file = event.target.files?.[0]
140
+ if (!file || uploading) return
141
+
142
+ const formData = new FormData()
143
+ formData.append('file', file)
144
+ formData.append('table_name', tableName || 'sales')
145
+
146
+ setUploading(true)
147
+ setUploadError(null)
148
+
149
+ try {
150
+ const response = await fetch('http://localhost:8000/api/upload-csv', {
151
+ method: 'POST',
152
+ body: formData
153
+ })
154
+
155
+ const data = await response.json()
156
+
157
+ if (!response.ok || data.status !== 'success') {
158
+ throw new Error(data.detail || 'Upload failed')
159
+ }
160
+
161
+ if (data.preview) {
162
+ setDatasetPreview({
163
+ table: data.table,
164
+ columns: data.columns,
165
+ rows: data.preview
166
+ })
167
+ setActiveResult(null)
168
+ }
169
+
170
+ fetchDatasetCatalog()
171
+ } catch (err) {
172
+ setUploadError(err.message)
173
+ } finally {
174
+ setUploading(false)
175
+ event.target.value = ''
176
+ }
177
+ }
178
+
179
+ const handleClearChat = async () => {
180
+ if (loading || clearing) return
181
+
182
+ setClearing(true)
183
+ setMessages([DEFAULT_AGENT_MESSAGE])
184
+ setActiveResult(null)
185
+ setError(null)
186
+
187
+ try {
188
+ await fetch('http://localhost:8000/api/session/reset', {
189
+ method: 'POST',
190
+ headers: { 'Content-Type': 'application/json' },
191
+ body: JSON.stringify({ session_id: sessionId })
192
+ })
193
+ } catch (err) {
194
+ console.error('Failed to reset session', err)
195
+ } finally {
196
+ setClearing(false)
197
+ }
198
+ }
199
+
200
+ return (
201
+ <div className="app-shell">
202
+ <aside className="sidebar">
203
+ <div className="brand">
204
+ <h1>InsightPilot</h1>
205
+ </div>
206
+
207
+ <div className="sidebar-section">
208
+ <h3>Dataset Control</h3>
209
+ <div className="dataset-control">
210
+ <div className="input-group">
211
+ <label>Target Table</label>
212
+ <input
213
+ type="text"
214
+ value={tableName}
215
+ onChange={(e) => setTableName(e.target.value)}
216
+ placeholder="sales"
217
+ />
218
+ </div>
219
+
220
+ <label className="upload-label">
221
+ {uploading ? 'Uploading...' : 'Click to Upload CSV'}
222
+ <input type="file" accept=".csv" onChange={handleDatasetUpload} disabled={uploading} />
223
+ </label>
224
+ {uploadError && <p className="error-text">{uploadError}</p>}
225
+
226
+ <button className="btn-secondary" onClick={fetchDatasetCatalog} disabled={catalogLoading}>
227
+ {catalogLoading ? 'Refreshing...' : 'Refresh Catalog'}
228
+ </button>
229
+ </div>
230
+ </div>
231
+
232
+ <div className="sidebar-section">
233
+ <h3>Available Tables</h3>
234
+ <ul className="dataset-list">
235
+ {datasetCatalog.length === 0 ? (
236
+ <li className="dataset-item">No tables found</li>
237
+ ) : (
238
+ datasetCatalog.map((table) => (
239
+ <li key={table.table} className="dataset-item">
240
+ <strong>{table.table}</strong>
241
+ <span>{table.rows?.toLocaleString()} rows</span>
242
+ </li>
243
+ ))
244
+ )}
245
+ </ul>
246
+ </div>
247
+ </aside>
248
+
249
+ <main className="main-content">
250
+ <section className="chat-section">
251
+ <header className="chat-header">
252
+ <div className="status-indicator">
253
+ <span className={`dot ${loading ? 'busy' : ''}`}></span>
254
+ {loading ? 'Agent is thinking...' : 'Agent is ready'}
255
+ </div>
256
+ <button
257
+ className="btn-secondary"
258
+ onClick={handleClearChat}
259
+ disabled={loading || clearing || messages.length <= 1}
260
+ >
261
+ {clearing ? 'Clearing...' : 'Clear Chat'}
262
+ </button>
263
+ </header>
264
+
265
+ <div className="messages-container">
266
+ {messages.length === 1 ? (
267
+ <div className="welcome-screen">
268
+ <h2>Autonomous Data Analyst</h2>
269
+ <p>Ask natural-language questions about your warehouse. The agent will plan the query, validate results, draw charts, and package everything in a PDF.</p>
270
+ <div className="quick-prompts">
271
+ {QUICK_PROMPTS.map((prompt) => (
272
+ <button key={prompt} className="prompt-card" onClick={() => handlePromptClick(prompt)}>
273
+ {prompt}
274
+ </button>
275
+ ))}
276
+ </div>
277
+ </div>
278
+ ) : (
279
+ messages.map((msg, index) => (
280
+ <div key={index} className={`message ${msg.type}`}>
281
+ <div className="avatar">
282
+ {msg.type === 'agent' ? '🤖' : '👤'}
283
+ </div>
284
+ <div className="message-content">
285
+ <p>{msg.content}</p>
286
+ {msg.sql && (
287
+ <div className="sql-snippet">
288
+ <code>{msg.sql}</code>
289
+ </div>
290
+ )}
291
+ {(msg.trendSummary || msg.anomalySummary) && (
292
+ <div className="diagnostic-chips">
293
+ {msg.trendSummary && <span className="badge">Trend: {msg.trendSummary}</span>}
294
+ {msg.anomalySummary && <span className="badge">Anomaly: {msg.anomalySummary}</span>}
295
+ </div>
296
+ )}
297
+ </div>
298
+ </div>
299
+ ))
300
+ )}
301
+ <div ref={messagesEndRef} />
302
+ </div>
303
+
304
+ <div className="input-area">
305
+ <form className="input-wrapper" onSubmit={handleSubmit}>
306
+ <div className="input-shell">
307
+ <textarea
308
+ ref={inputRef}
309
+ rows={1}
310
+ value={input}
311
+ onChange={(e) => setInput(e.target.value)}
312
+ onKeyDown={(e) => {
313
+ if (e.key === 'Enter' && !e.shiftKey) {
314
+ e.preventDefault()
315
+ if (!loading) {
316
+ sendQuery(input)
317
+ }
318
+ }
319
+ }}
320
+ placeholder="Ask anything about your data..."
321
+ disabled={loading}
322
+ />
323
+ <button type="submit" className="send-btn" disabled={loading}>
324
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
325
+ <path d="M22 2L11 13"></path>
326
+ <path d="M22 2L15 22L11 13L2 9L22 2Z"></path>
327
+ </svg>
328
+ </button>
329
+ </div>
330
+ </form>
331
+ </div>
332
+ </section>
333
+
334
+ <section className="results-section">
335
+ <div className="results-header">
336
+ <h2>Analysis Results</h2>
337
+ </div>
338
+ <div className="results-content">
339
+ {datasetPreview && !activeResult && (
340
+ <div className="result-card">
341
+ <div className="card-header">
342
+ <span>Uploaded Data Preview: {datasetPreview.table}</span>
343
+ </div>
344
+ <div className="table-container">
345
+ <table>
346
+ <thead>
347
+ <tr>
348
+ {datasetPreview.columns.map(col => <th key={col}>{col}</th>)}
349
+ </tr>
350
+ </thead>
351
+ <tbody>
352
+ {datasetPreview.rows.map((row, i) => (
353
+ <tr key={i}>
354
+ {datasetPreview.columns.map(col => <td key={col}>{row[col]}</td>)}
355
+ </tr>
356
+ ))}
357
+ </tbody>
358
+ </table>
359
+ </div>
360
+ </div>
361
+ )}
362
+
363
+ {activeResult ? (
364
+ <>
365
+ <div className="result-card">
366
+ <div className="card-header">
367
+ <span>Insight</span>
368
+ </div>
369
+ <p>{activeResult.insights}</p>
370
+ </div>
371
+
372
+ {activeResult.visualization_url && (
373
+ <div className="result-card">
374
+ <div className="card-header">
375
+ <span>Visualization</span>
376
+ </div>
377
+ <div className="viz-container">
378
+ <img src={`http://localhost:8000${activeResult.visualization_url}`} alt="Visualization" />
379
+ </div>
380
+ </div>
381
+ )}
382
+
383
+ {activeResult.trend_analysis && (
384
+ <div className="result-card">
385
+ <div className="card-header">
386
+ <span>Trend Analysis</span>
387
+ <span className="badge">Diagnostic</span>
388
+ </div>
389
+ <p>{activeResult.trend_analysis.summary}</p>
390
+ <div className="trend-grid">
391
+ <div>
392
+ <span>Start</span>
393
+ <strong>{Number.isFinite(activeResult.trend_analysis.start) ? activeResult.trend_analysis.start.toFixed(2) : '-'}</strong>
394
+ </div>
395
+ <div>
396
+ <span>End</span>
397
+ <strong>{Number.isFinite(activeResult.trend_analysis.end) ? activeResult.trend_analysis.end.toFixed(2) : '-'}</strong>
398
+ </div>
399
+ <div>
400
+ <span>Change</span>
401
+ <strong>{activeResult.trend_analysis.change_pct?.toFixed(1)}%</strong>
402
+ </div>
403
+ </div>
404
+ </div>
405
+ )}
406
+
407
+ {activeResult.anomaly_analysis && (
408
+ <div className="result-card">
409
+ <div className="card-header">
410
+ <span>Anomalies</span>
411
+ <span className="badge">Diagnostic</span>
412
+ </div>
413
+ <p>{activeResult.anomaly_analysis.summary}</p>
414
+ <ul className="anomaly-list">
415
+ {activeResult.anomaly_analysis.anomalies?.slice(0, 3).map((a, i) => (
416
+ <li key={i}>
417
+ <span>{a.period}</span>
418
+ <strong>z={a.z_score?.toFixed(2)}</strong>
419
+ </li>
420
+ ))}
421
+ </ul>
422
+ </div>
423
+ )}
424
+
425
+ {activeResult.forecast_analysis && (
426
+ <div className="result-card">
427
+ <div className="card-header">
428
+ <span>Forecast</span>
429
+ <span className="badge">Predictive</span>
430
+ </div>
431
+ <p>{activeResult.forecast_analysis.summary}</p>
432
+ {activeResult.forecast_analysis.method && (
433
+ <div style={{marginTop: '0.5rem', fontSize: '0.85rem', color: '#8899aa'}}>
434
+ Method: {activeResult.forecast_analysis.method}
435
+ </div>
436
+ )}
437
+ {activeResult.forecast_analysis.forecasts && (
438
+ <div className="table-container" style={{marginTop: '1rem'}}>
439
+ <table>
440
+ <thead>
441
+ <tr>
442
+ <th>Period</th>
443
+ <th>Forecast</th>
444
+ <th>Lower Bound</th>
445
+ <th>Upper Bound</th>
446
+ </tr>
447
+ </thead>
448
+ <tbody>
449
+ {activeResult.forecast_analysis.forecasts.map((f, i) => (
450
+ <tr key={i}>
451
+ <td>{f.period}</td>
452
+ <td>{Number.isFinite(f.value) ? f.value.toFixed(2) : '-'}</td>
453
+ <td>{Number.isFinite(f.lower_bound) ? f.lower_bound.toFixed(2) : '-'}</td>
454
+ <td>{Number.isFinite(f.upper_bound) ? f.upper_bound.toFixed(2) : '-'}</td>
455
+ </tr>
456
+ ))}
457
+ </tbody>
458
+ </table>
459
+ </div>
460
+ )}
461
+ </div>
462
+ )}
463
+
464
+ {activeResult.statistical_tests && (
465
+ <div className="result-card">
466
+ <div className="card-header">
467
+ <span>Statistical Tests</span>
468
+ <span className="badge">Diagnostic</span>
469
+ </div>
470
+ <p>{activeResult.statistical_tests.summary}</p>
471
+ <div style={{marginTop: '1rem'}}>
472
+ {activeResult.statistical_tests.tests && Object.entries(activeResult.statistical_tests.tests).map(([testName, testData]) => (
473
+ <div key={testName} style={{marginBottom: '1rem', padding: '0.75rem', background: '#1e2430', borderRadius: '6px'}}>
474
+ <div style={{fontWeight: '600', marginBottom: '0.5rem', textTransform: 'capitalize'}}>
475
+ {testName.replace(/_/g, ' ')}
476
+ </div>
477
+ <div style={{fontSize: '0.9rem', color: '#8899aa'}}>
478
+ {testData.summary || testData.test || '-'}
479
+ </div>
480
+ {testData.p_value !== undefined && (
481
+ <div style={{marginTop: '0.5rem', fontSize: '0.85rem'}}>
482
+ <span style={{color: '#8899aa'}}>p-value: </span>
483
+ <span style={{fontFamily: 'monospace', color: testData.significant || testData.is_stationary === false ? '#ff6b6b' : '#51cf66'}}>
484
+ {Number.isFinite(testData.p_value) ? testData.p_value.toFixed(4) : '-'}
485
+ </span>
486
+ {testData.significant !== undefined && (
487
+ <span style={{marginLeft: '0.75rem', color: testData.significant ? '#ff6b6b' : '#51cf66'}}>
488
+ {testData.significant ? '✓ Significant' : '○ Not Significant'}
489
+ </span>
490
+ )}
491
+ {testData.is_stationary !== undefined && (
492
+ <span style={{marginLeft: '0.75rem', color: testData.is_stationary ? '#51cf66' : '#ff6b6b'}}>
493
+ {testData.is_stationary ? '✓ Stationary' : '○ Non-Stationary'}
494
+ </span>
495
+ )}
496
+ </div>
497
+ )}
498
+ </div>
499
+ ))}
500
+ </div>
501
+ </div>
502
+ )}
503
+
504
+ {previewColumns.length > 0 && (
505
+ <div className="result-card">
506
+ <div className="card-header">
507
+ <span>Data Preview</span>
508
+ </div>
509
+ <div className="table-container">
510
+ <table>
511
+ <thead>
512
+ <tr>
513
+ {previewColumns.map(col => <th key={col}>{col}</th>)}
514
+ </tr>
515
+ </thead>
516
+ <tbody>
517
+ {previewRows.map((row, i) => (
518
+ <tr key={i}>
519
+ {previewColumns.map(col => <td key={col}>{row[col]}</td>)}
520
+ </tr>
521
+ ))}
522
+ </tbody>
523
+ </table>
524
+ </div>
525
+ </div>
526
+ )}
527
+
528
+ {activeResult.report_url && (
529
+ <a href={`http://localhost:8000${activeResult.report_url}`} target="_blank" rel="noreferrer" className="download-btn">
530
+ Download PDF Report
531
+ </a>
532
+ )}
533
+ </>
534
+ ) : (
535
+ <div className="empty-state-results">
536
+ <p>Run a query to see detailed analysis, charts, and data previews here.</p>
537
+ </div>
538
+ )}
539
+ </div>
540
+ </section>
541
+ </main>
542
+ </div>
543
+ )
544
+ }
545
+
546
+ export default App
frontend/src/assets/react.svg ADDED
frontend/src/index.css ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+ color-scheme: dark;
6
+ }
7
+
8
+ body {
9
+ margin: 0;
10
+ padding: 0;
11
+ min-height: 100vh;
12
+ width: 100%;
13
+ }
frontend/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
frontend/vite.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })
requirements.txt ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python dependencies from backend
2
+ fastapi==0.109.0
3
+ uvicorn[standard]==0.27.0
4
+ langgraph==0.0.20
5
+ langchain==0.1.0
6
+ langchain-groq==0.0.1
7
+ langchain-community==0.0.13
8
+ sqlalchemy==2.0.25
9
+ pandas==2.1.4
10
+ numpy==1.26.3
11
+ scipy==1.11.4
12
+ statsmodels==0.14.1
13
+ matplotlib==3.8.2
14
+ reportlab==4.0.8
15
+ python-dotenv==1.0.0
16
+ pydantic==2.5.3
17
+ requests==2.31.0
18
+ python-multipart==0.0.6
19
+
20
+ # Gradio for HF Spaces UI
21
+ gradio==4.16.0
22
+
23
+ # Additional helpful packages
24
+ aiofiles==23.2.1
setup_hf_deployment.sh ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Quick setup script for HF Spaces deployment
3
+
4
+ echo "🚀 Setting up InsightPilot for Hugging Face Spaces deployment"
5
+ echo ""
6
+
7
+ # Check if we should build frontend
8
+ read -p "Build React frontend? (y/n - takes a few minutes): " build_frontend
9
+
10
+ if [ "$build_frontend" = "y" ] || [ "$build_frontend" = "Y" ]; then
11
+ echo "📦 Building frontend..."
12
+ cd frontend
13
+ npm install
14
+ npm run build
15
+ cd ..
16
+ echo "✅ Frontend built successfully!"
17
+ else
18
+ echo "⏭️ Skipping frontend build (app will work without it)"
19
+ fi
20
+
21
+ echo ""
22
+ echo "📝 Next steps:"
23
+ echo ""
24
+ echo "1. Create a new Space on Hugging Face:"
25
+ echo " → https://huggingface.co/new-space"
26
+ echo " → Choose SDK: Gradio"
27
+ echo " → Choose Hardware: CPU basic (free)"
28
+ echo ""
29
+ echo "2. Clone your new Space:"
30
+ echo " git clone https://huggingface.co/spaces/YOUR_USERNAME/SPACE_NAME"
31
+ echo ""
32
+ echo "3. Copy files to your Space:"
33
+ echo " cp app.py YOUR_SPACE/"
34
+ echo " cp requirements.txt YOUR_SPACE/"
35
+ echo " cp README_HF.md YOUR_SPACE/README.md"
36
+ echo " cp -r backend YOUR_SPACE/"
37
+ echo " cp -r data YOUR_SPACE/"
38
+ if [ "$build_frontend" = "y" ] || [ "$build_frontend" = "Y" ]; then
39
+ echo " cp -r frontend/dist YOUR_SPACE/frontend/dist"
40
+ fi
41
+ echo ""
42
+ echo "4. Push to Hugging Face:"
43
+ echo " cd YOUR_SPACE"
44
+ echo " git add ."
45
+ echo " git commit -m 'Initial deployment'"
46
+ echo " git push"
47
+ echo ""
48
+ echo "5. Add your GROQ_API_KEY in Space Settings → Repository secrets"
49
+ echo ""
50
+ echo "📖 Full deployment guide: See DEPLOYMENT.md"
51
+ echo ""
52
+ echo "✨ Done! Ready to deploy to Hugging Face Spaces!"