borndeveloper commited on
Commit
ecd72cb
·
0 Parent(s):

Initial commit: PDC Intelligence application

Browse files

- FastAPI backend with construction prediction
- Next.js frontend with industrial UI
- Excel data files for textile manufacturing
- Dockerfile for HuggingFace Spaces deployment

Files changed (48) hide show
  1. .gitattributes +2 -0
  2. .gitignore +30 -0
  3. Dockerfile +50 -0
  4. PDC data/Feasibility Process (1).docx +0 -0
  5. PDC data/Feasibility Process.docx +0 -0
  6. PDC data/Fiber Code in Blend Ratio (1).docx +0 -0
  7. PDC data/Input & Out Put.xlsx +3 -0
  8. PDC data/Input Parameter (1).docx +0 -0
  9. PDC data/Piece Dyed Artcle details.xlsx +3 -0
  10. PDC data/Weave Reference.docx +0 -0
  11. PDC data/Working 24 Jan.xlsx +3 -0
  12. README.md +98 -0
  13. app.py +703 -0
  14. backend/.gitignore +3 -0
  15. backend/app/__init__.py +0 -0
  16. backend/app/data/Input & Out Put.xlsx +3 -0
  17. backend/app/data/Piece Dyed Artcle details.xlsx +3 -0
  18. backend/app/data/Working 24 Jan.xlsx +3 -0
  19. backend/app/main.py +93 -0
  20. backend/app/services/__init__.py +0 -0
  21. backend/app/services/data_service.py +876 -0
  22. backend/requirements.txt +7 -0
  23. backend/test_perf.py +10 -0
  24. backend/tests/__init__.py +0 -0
  25. backend/tests/test_calculations.py +36 -0
  26. backend/tests/test_data_source_audit.py +19 -0
  27. backend/tests/test_formula_integrity.py +70 -0
  28. backend/tests/test_prediction_validation.py +64 -0
  29. data/Input & Out Put.xlsx +3 -0
  30. data/Piece Dyed Artcle details.xlsx +3 -0
  31. data/Working 24 Jan.xlsx +3 -0
  32. frontend/.gitignore +7 -0
  33. frontend/app/globals.css +76 -0
  34. frontend/app/layout.tsx +19 -0
  35. frontend/app/page.tsx +991 -0
  36. frontend/components/ui/button.tsx +34 -0
  37. frontend/components/ui/card.tsx +34 -0
  38. frontend/lib/utils.ts +6 -0
  39. frontend/next-env.d.ts +6 -0
  40. frontend/next.config.mjs +13 -0
  41. frontend/package-lock.json +2754 -0
  42. frontend/package.json +29 -0
  43. frontend/postcss.config.js +6 -0
  44. frontend/tailwind.config.js +53 -0
  45. frontend/tsconfig.json +41 -0
  46. frontend/tsconfig.tsbuildinfo +0 -0
  47. requirements.txt +6 -0
  48. run_app.sh +56 -0
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.xlsx filter=lfs diff=lfs merge=lfs -text
2
+ *.xls filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ venv/
8
+ .venv/
9
+ *.egg-info/
10
+ dist/
11
+ build/
12
+ *.egg
13
+ .pytest_cache/
14
+
15
+ # Node
16
+ node_modules/
17
+ .next/
18
+ out/
19
+ .env
20
+ .env.local
21
+
22
+ # IDE
23
+ .vscode/
24
+ .idea/
25
+ *.swp
26
+ *.swo
27
+
28
+ # OS
29
+ .DS_Store
30
+ Thumbs.db
Dockerfile ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Install Node.js 20.x for building Next.js frontend
4
+ RUN apt-get update && apt-get install -y curl && \
5
+ curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
6
+ apt-get install -y nodejs && \
7
+ rm -rf /var/lib/apt/lists/*
8
+
9
+ # Create user for security
10
+ RUN useradd -m -u 1000 user
11
+ USER user
12
+
13
+ # Set PATH
14
+ ENV PATH="/home/user/.local/bin:$PATH"
15
+
16
+ WORKDIR /app
17
+
18
+ # Copy and install Python dependencies first (better caching)
19
+ COPY --chown=user requirements.txt .
20
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
21
+
22
+ # Copy frontend package files
23
+ COPY --chown=user frontend/package.json frontend/package-lock.json ./frontend/
24
+
25
+ # Install Node dependencies and build Next.js
26
+ WORKDIR /app/frontend
27
+ RUN npm install
28
+
29
+ COPY --chown=user ../frontend/app ./app
30
+ COPY --chown=user ../frontend/components ./components
31
+ COPY --chown=user ../frontend/lib ./lib
32
+ COPY --chown=user ../frontend/tailwind.config.js ../frontend/postcss.config.js ../frontend/tsconfig.json ../frontend/next.config.mjs ./
33
+
34
+ # Build Next.js to static output
35
+ RUN npm run build
36
+
37
+ # Set workdir back to app root
38
+ WORKDIR /app
39
+
40
+ # Copy backend source
41
+ COPY --chown=user app.py .
42
+ COPY --chown=user backend/app ./backend/app
43
+
44
+ # Copy data files
45
+ COPY --chown=user data ./data
46
+
47
+ # Expose Hugging Face required port
48
+ EXPOSE 7860
49
+
50
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
PDC data/Feasibility Process (1).docx ADDED
Binary file (11.8 kB). View file
 
PDC data/Feasibility Process.docx ADDED
Binary file (9.59 kB). View file
 
PDC data/Fiber Code in Blend Ratio (1).docx ADDED
Binary file (24.7 kB). View file
 
PDC data/Input & Out Put.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8b71a764d4c14209ba3c8eff8011580736a048b176bb465284a593ccfdd7611a
3
+ size 9417
PDC data/Input Parameter (1).docx ADDED
Binary file (12.1 kB). View file
 
PDC data/Piece Dyed Artcle details.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d95d9af054823e797a618008199dfc2ebb184118cb95bdab29913e779ac792cd
3
+ size 300302
PDC data/Weave Reference.docx ADDED
Binary file (14 kB). View file
 
PDC data/Working 24 Jan.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:98c52c8631ef9a73f9d3487e7650ce702ee21d2da9223bc5d1312bc4cf64e2a6
3
+ size 1746811
README.md ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PDC Intelligence Prototype (MVP)
2
+
3
+ This prototype is built under `PDC/` and aligned to the PDC meeting notes and process documents.
4
+
5
+ ## Scope Covered
6
+
7
+ - Phase-1 focus: greige construction recommendation from finish-side requirements.
8
+ - Dashboard for dataset health, coverage, and core relativity metrics.
9
+ - Article search with filters and article-level detail.
10
+ - Construction calculator with optimistic/expected/pessimistic scenarios.
11
+ - Relativity analytics by weave/blend and numeric correlation matrix.
12
+ - Feasibility process flow mapping from meeting/process docs.
13
+ - Built-in validation report endpoint for ongoing prediction quality checks.
14
+
15
+ ## Folder Layout
16
+
17
+ - `backend/`: FastAPI APIs and statistical engine.
18
+ - `frontend/`: Next.js dashboard and calculator UI.
19
+ - `backend/app/data/`: copied data files for runtime.
20
+
21
+ ## Backend Run
22
+
23
+ ```bash
24
+ cd backend
25
+ python3 -m venv .venv
26
+ .venv/bin/pip install -r requirements.txt
27
+ .venv/bin/uvicorn app.main:app --reload --port 8000
28
+ ```
29
+
30
+ ## Frontend Run
31
+
32
+ ```bash
33
+ cd frontend
34
+ npm install
35
+ npm run dev
36
+ ```
37
+
38
+ Frontend rewrites `/api/*` to backend `http://localhost:8000/api/*`.
39
+
40
+ ## One-command Run
41
+
42
+ From `PDC/`:
43
+
44
+ ```bash
45
+ ./run_app.sh
46
+ ```
47
+
48
+ This script will:
49
+
50
+ - create backend venv if missing
51
+ - install backend dependencies
52
+ - run backend tests before startup
53
+ - install frontend dependencies
54
+ - start backend and frontend together
55
+
56
+ ## Verification Performed
57
+
58
+ - Unit tests:
59
+ - formula correctness checks
60
+ - prediction response contract checks
61
+ - holdout MAE threshold checks
62
+ - validation report generation checks
63
+
64
+ Run tests:
65
+
66
+ ```bash
67
+ cd backend
68
+ .venv/bin/python -m pytest -q
69
+ ```
70
+
71
+ Current run: `3 passed`
72
+
73
+ Validation API:
74
+
75
+ ```bash
76
+ GET /api/validation/report?sample_size=250&seed=42
77
+ ```
78
+
79
+ Returns MAE, error percentiles, and confidence distribution.
80
+
81
+ Data source audit API:
82
+
83
+ ```bash
84
+ GET /api/data-source/audit
85
+ ```
86
+
87
+ Returns source file usage, raw vs used row counts, dataset distribution, and column-level null coverage.
88
+
89
+ ## Predictive Validation Snapshot
90
+
91
+ On a 300-row holdout sample:
92
+
93
+ - MAE (Greige EPI): `13.92`
94
+ - MAE (Greige PPI): `8.132`
95
+ - P90 absolute error (Greige EPI): `33.0`
96
+ - P90 absolute error (Greige PPI): `20.0`
97
+
98
+ These are baseline MVP metrics and can be improved by adding tighter cluster filters (count band, loom, weave aliases) and weighted neighbors.
app.py ADDED
@@ -0,0 +1,703 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from functools import lru_cache
3
+ from typing import Any
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from fastapi import FastAPI, Query
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from fastapi.responses import FileResponse, HTMLResponse
10
+ from fastapi.staticfiles import StaticFiles
11
+
12
+ # Data directory
13
+ DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
14
+ WORKING_PATH = os.path.join(DATA_DIR, "Working 24 Jan.xlsx")
15
+ PIECE_PATH = os.path.join(DATA_DIR, "Piece Dyed Artcle details.xlsx")
16
+
17
+ app = FastAPI(title="PDC Intelligence API", version="1.0.0")
18
+
19
+ app.add_middleware(
20
+ CORSMiddleware,
21
+ allow_origins=["*"],
22
+ allow_credentials=True,
23
+ allow_methods=["*"],
24
+ allow_headers=["*"],
25
+ )
26
+
27
+
28
+ # ============== Data Service ==============
29
+
30
+
31
+ def _clean_text(value: Any) -> str:
32
+ if value is None:
33
+ return ""
34
+ return str(value).strip()
35
+
36
+
37
+ def _safe_float(value: Any) -> float | None:
38
+ if value is None:
39
+ return None
40
+ text = str(value).strip().replace(",", "")
41
+ if text == "" or text.lower() in {"nan", "none"}:
42
+ return None
43
+ try:
44
+ return float(text)
45
+ except Exception:
46
+ return None
47
+
48
+
49
+ def _extract_count(value: Any) -> float | None:
50
+ import re
51
+
52
+ text = _clean_text(value).upper()
53
+ nums = re.findall(r"\d+(?:\.\d+)?", text)
54
+ if not nums:
55
+ return None
56
+ vals = [float(n) for n in nums]
57
+ if not vals:
58
+ return None
59
+ return float(max(vals))
60
+
61
+
62
+ def _normalize_weave(value: Any) -> str:
63
+ import re
64
+
65
+ text = _clean_text(value).upper()
66
+ text = re.sub(r"\s+", " ", text)
67
+ text = text.replace("TWL", "TWILL")
68
+ return text
69
+
70
+
71
+ def _normalize_blend(value: Any) -> str:
72
+ import re
73
+
74
+ text = _clean_text(value).upper().replace("PER", "%")
75
+ text = re.sub(r"\s+", " ", text)
76
+ text = text.replace("COTTON", "CO")
77
+ text = text.replace("POLYESTER", "PES")
78
+ return text
79
+
80
+
81
+ def _count_band(value: float | None) -> str:
82
+ if value is None:
83
+ return "unknown"
84
+ return "below_40" if value < 40 else "40_and_above"
85
+
86
+
87
+ class DataService:
88
+ def __init__(self) -> None:
89
+ self.df: pd.DataFrame | None = None
90
+ self.is_loaded = False
91
+ self.last_error = ""
92
+
93
+ def load_data(self) -> None:
94
+ try:
95
+ working = pd.read_excel(WORKING_PATH)
96
+ piece = pd.read_excel(PIECE_PATH)
97
+
98
+ working["dataset"] = "working"
99
+ piece["dataset"] = "piece_dyed"
100
+
101
+ if "Loom Type" not in working.columns:
102
+ working["Loom Type"] = ""
103
+
104
+ cols = sorted(set(working.columns).union(set(piece.columns)))
105
+ working = working.reindex(columns=cols)
106
+ piece = piece.reindex(columns=cols)
107
+
108
+ df = pd.concat([working, piece], ignore_index=True)
109
+
110
+ df["article"] = df["Article"].astype(str)
111
+ df["master_article"] = df["Master Article"].astype(str)
112
+ df["variant"] = df["Options/Variants"].astype(str)
113
+ df["loom_type"] = df["Loom Type"].fillna("").astype(str)
114
+
115
+ df["weave_raw"] = df["WEAVE"].fillna("").astype(str)
116
+ df["weave"] = df["weave_raw"].map(_normalize_weave)
117
+ df["blend_raw"] = df["Blend Ratio"].fillna("").astype(str)
118
+ df["blend"] = df["blend_raw"].map(_normalize_blend)
119
+
120
+ for col in [
121
+ "Reed Count",
122
+ "Ends per dent",
123
+ "Reed space",
124
+ "Greige EPI",
125
+ "Greige PPI",
126
+ "Greige Width in INCH",
127
+ "FINISH EPI",
128
+ "FINISH PPI",
129
+ "FINISH GSM",
130
+ "FINISH WIDTH",
131
+ "ON LOOM EPI",
132
+ "ON LOOM PPI",
133
+ ]:
134
+ df[col] = pd.to_numeric(df[col], errors="coerce")
135
+
136
+ df["warp_count"] = df[" Warp Code 1"].map(_extract_count)
137
+ df["weft_count"] = df[" Weft Code 1"].map(_extract_count)
138
+ df["count_ref"] = df[["warp_count", "weft_count"]].max(axis=1)
139
+ df["count_band"] = df["count_ref"].map(_count_band)
140
+
141
+ df["epi_change_pct"] = np.where(
142
+ df["Greige EPI"] > 0,
143
+ ((df["FINISH EPI"] - df["Greige EPI"]) / df["Greige EPI"]) * 100,
144
+ np.nan,
145
+ )
146
+ df["ppi_change_pct"] = np.where(
147
+ df["Greige PPI"] > 0,
148
+ ((df["FINISH PPI"] - df["Greige PPI"]) / df["Greige PPI"]) * 100,
149
+ np.nan,
150
+ )
151
+ df["width_change_pct"] = np.where(
152
+ df["Greige Width in INCH"] > 0,
153
+ (
154
+ (df["FINISH WIDTH"] - df["Greige Width in INCH"])
155
+ / df["Greige Width in INCH"]
156
+ )
157
+ * 100,
158
+ np.nan,
159
+ )
160
+
161
+ df = df[(df["weave"].str.len() > 0) & (df["blend"].str.len() > 0)]
162
+
163
+ self.df = df
164
+ self.is_loaded = True
165
+ self.last_error = ""
166
+ except Exception as exc:
167
+ self.last_error = str(exc)
168
+ self.is_loaded = False
169
+
170
+ def _ensure(self) -> pd.DataFrame:
171
+ if not self.is_loaded or self.df is None:
172
+ self.load_data()
173
+ return self.df
174
+
175
+ def get_health(self) -> dict:
176
+ df = self._ensure()
177
+ return {
178
+ "status": "healthy" if self.is_loaded else "degraded",
179
+ "rows": int(len(df)),
180
+ "error": self.last_error,
181
+ }
182
+
183
+ @lru_cache(maxsize=1)
184
+ def get_dashboard_summary(self) -> dict:
185
+ df = self._ensure()
186
+ top_weaves = (
187
+ df["weave"]
188
+ .value_counts()
189
+ .head(10)
190
+ .rename_axis("weave")
191
+ .reset_index(name="count")
192
+ )
193
+ top_blends = (
194
+ df["blend"]
195
+ .value_counts()
196
+ .head(10)
197
+ .rename_axis("blend")
198
+ .reset_index(name="count")
199
+ )
200
+ return {
201
+ "totals": {
202
+ "rows": int(len(df)),
203
+ "unique_articles": int(df["article"].nunique()),
204
+ "unique_master_articles": int(df["master_article"].nunique()),
205
+ "unique_weaves": int(df["weave"].nunique()),
206
+ "unique_blends": int(df["blend"].nunique()),
207
+ },
208
+ "top_weaves": top_weaves.to_dict(orient="records"),
209
+ "top_blends": top_blends.to_dict(orient="records"),
210
+ }
211
+
212
+ @lru_cache(maxsize=1)
213
+ def get_filters(self) -> dict:
214
+ df = self._ensure()
215
+ return {
216
+ "weaves": sorted(df["weave"].dropna().unique().tolist())[:500],
217
+ "blends": sorted(df["blend"].dropna().unique().tolist())[:500],
218
+ "loom_types": sorted(
219
+ [
220
+ x
221
+ for x in df["loom_type"].dropna().unique().tolist()
222
+ if str(x).strip()
223
+ ]
224
+ ),
225
+ "datasets": ["all", "working", "piece_dyed"],
226
+ }
227
+
228
+ def get_articles(
229
+ self,
230
+ page: int,
231
+ limit: int,
232
+ search: str,
233
+ weave: str,
234
+ blend: str,
235
+ loom_type: str,
236
+ dataset: str,
237
+ count_band: str,
238
+ ) -> dict:
239
+ df = self._ensure()
240
+ if search:
241
+ s = search.lower().strip()
242
+ df = df[
243
+ df["master_article"].str.lower().str.contains(s, na=False)
244
+ | df["article"].str.lower().str.contains(s, na=False)
245
+ | df["weave"].str.lower().str.contains(s, na=False)
246
+ | df["blend"].str.lower().str.contains(s, na=False)
247
+ ]
248
+ if weave:
249
+ df = df[df["weave"] == weave]
250
+ if blend:
251
+ df = df[df["blend"] == blend]
252
+ if loom_type:
253
+ df = df[df["loom_type"] == loom_type]
254
+ if dataset in {"working", "piece_dyed"}:
255
+ df = df[df["dataset"] == dataset]
256
+ if count_band in {"below_40", "40_and_above"}:
257
+ df = df[df["count_band"] == count_band]
258
+
259
+ grouped = (
260
+ df.groupby("master_article", dropna=False)
261
+ .agg(
262
+ article=("article", "first"),
263
+ weave=("weave", "first"),
264
+ blend=("blend", "first"),
265
+ warp_count=("warp_count", "max"),
266
+ weft_count=("weft_count", "max"),
267
+ loom_type=("loom_type", "first"),
268
+ rows=("master_article", "count"),
269
+ reed_count=("Reed Count", "median"),
270
+ finish_epi_median=("FINISH EPI", "median"),
271
+ finish_ppi_median=("FINISH PPI", "median"),
272
+ greige_epi_median=("Greige EPI", "median"),
273
+ greige_ppi_median=("Greige PPI", "median"),
274
+ )
275
+ .reset_index()
276
+ .sort_values("rows", ascending=False)
277
+ )
278
+
279
+ total = int(len(grouped))
280
+ start = (page - 1) * limit
281
+ chunk = grouped.iloc[start : start + limit].copy()
282
+ for col in [
283
+ "finish_epi_median",
284
+ "finish_ppi_median",
285
+ "greige_epi_median",
286
+ "greige_ppi_median",
287
+ ]:
288
+ chunk[col] = chunk[col].round(3)
289
+
290
+ return {
291
+ "data": chunk.to_dict(orient="records"),
292
+ "page": page,
293
+ "limit": limit,
294
+ "total": total,
295
+ "total_pages": int(np.ceil(total / limit)) if total else 0,
296
+ }
297
+
298
+ def _field_stats(self, series: pd.Series) -> dict:
299
+ s = pd.to_numeric(series, errors="coerce").dropna()
300
+ if s.empty:
301
+ return {
302
+ "p25": None,
303
+ "median": None,
304
+ "p75": None,
305
+ "mean": None,
306
+ "std": None,
307
+ "cv": None,
308
+ "used_count": 0,
309
+ "raw_count": 0,
310
+ }
311
+
312
+ raw_count = len(s)
313
+ if len(s) >= 8:
314
+ q1 = s.quantile(0.25).item()
315
+ q3 = s.quantile(0.75).item()
316
+ iqr = q3 - q1
317
+ lower = q1 - (1.5 * iqr)
318
+ upper = q3 + (1.5 * iqr)
319
+ s = s[(s >= lower) & (s <= upper)]
320
+ if s.empty:
321
+ s = pd.to_numeric(series, errors="coerce").dropna()
322
+
323
+ return {
324
+ "p25": round(float(s.quantile(0.25).item()), 3),
325
+ "median": round(float(s.median().item()), 3),
326
+ "p75": round(float(s.quantile(0.75).item()), 3),
327
+ "mean": round(float(s.mean().item()), 3),
328
+ "std": round(float(s.std(ddof=1).item())) if len(s) > 1 else 0.0,
329
+ "cv": round(float(abs(s.std(ddof=1) / s.mean()) * 100), 3)
330
+ if len(s) > 1 and s.mean() != 0
331
+ else 0.0,
332
+ "used_count": int(len(s)),
333
+ "raw_count": int(raw_count),
334
+ }
335
+
336
+ def _distance_score(self, row: pd.Series, payload: dict) -> float:
337
+ score = 0.0
338
+ if (
339
+ payload.get("weave")
340
+ and _normalize_weave(payload.get("weave")) != row["weave"]
341
+ ):
342
+ score += 4.0
343
+ if (
344
+ payload.get("blend")
345
+ and _normalize_blend(payload.get("blend")) != row["blend"]
346
+ ):
347
+ score += 3.0
348
+
349
+ for inp, col, w in [
350
+ ("warp_count", "warp_count", 2.0),
351
+ ("weft_count", "weft_count", 2.0),
352
+ ("finish_epi", "FINISH EPI", 1.5),
353
+ ("finish_ppi", "FINISH PPI", 1.5),
354
+ ("target_gsm", "FINISH GSM", 1.0),
355
+ ]:
356
+ v = _safe_float(payload.get(inp))
357
+ if v is None:
358
+ continue
359
+ rv = _safe_float(row.get(col))
360
+ if rv is None:
361
+ score += 1.0
362
+ else:
363
+ score += w * abs(v - rv) / max(abs(v), 1.0)
364
+ return score
365
+
366
+ def _confidence(self, match_count: int, cv: float) -> str:
367
+ if match_count >= 60 and cv <= 20:
368
+ return "high"
369
+ if match_count >= 20 and cv <= 35:
370
+ return "medium"
371
+ if match_count >= 8:
372
+ return "low"
373
+ return "very_low"
374
+
375
+ def predict_construction(self, payload: dict) -> dict:
376
+ df = self._ensure()
377
+
378
+ weave_in = _normalize_weave(payload.get("weave", ""))
379
+ blend_in = _normalize_blend(payload.get("blend", ""))
380
+ dataset = _clean_text(payload.get("dataset", "all")).lower()
381
+ loom_type_in = _clean_text(payload.get("loom_type", ""))
382
+
383
+ warp_in = _safe_float(payload.get("warp_count"))
384
+ weft_in = _safe_float(payload.get("weft_count"))
385
+ finish_epi_in = _safe_float(payload.get("finish_epi"))
386
+ finish_ppi_in = _safe_float(payload.get("finish_ppi"))
387
+ target_gsm_in = _safe_float(payload.get("target_gsm"))
388
+
389
+ input_count_ref = (
390
+ max(v for v in [warp_in, weft_in] if v is not None)
391
+ if warp_in or weft_in
392
+ else None
393
+ )
394
+ input_count_band = _count_band(input_count_ref)
395
+
396
+ if not weave_in or not blend_in:
397
+ return {
398
+ "error": "weave and blend are required",
399
+ "required_fields": ["weave", "blend"],
400
+ }
401
+
402
+ if dataset in {"working", "piece_dyed"}:
403
+ df = df[df["dataset"] == dataset]
404
+
405
+ if loom_type_in:
406
+ loom_filtered = df[df["loom_type"].str.lower() == loom_type_in.lower()]
407
+ if len(loom_filtered) >= 30:
408
+ df = loom_filtered
409
+
410
+ # Hierarchical filtering
411
+ lvl1 = df[(df["weave"] == weave_in) & (df["blend"] == blend_in)]
412
+ if input_count_band != "unknown":
413
+ lvl1_band = lvl1[lvl1["count_band"] == input_count_band]
414
+ if len(lvl1_band) >= 20:
415
+ lvl1 = lvl1_band
416
+
417
+ lvl2 = df[df["weave"] == weave_in]
418
+ if input_count_band != "unknown":
419
+ lvl2_band = lvl2[lvl2["count_band"] == input_count_band]
420
+ if len(lvl2_band) >= 25:
421
+ lvl2 = lvl2_band
422
+
423
+ lvl3 = df
424
+
425
+ chosen = lvl1
426
+ source = "exact weave + blend"
427
+ if len(chosen) < 20:
428
+ chosen = lvl2
429
+ source = "weave fallback"
430
+ if len(chosen) < 20:
431
+ chosen = lvl3
432
+ source = "global fallback"
433
+
434
+ # Pre-filter
435
+ pre = chosen.copy()
436
+ if finish_epi_in is not None:
437
+ pre_epi = pre[
438
+ pre["FINISH EPI"].between(finish_epi_in * 0.7, finish_epi_in * 1.3)
439
+ ]
440
+ if len(pre_epi) >= 20:
441
+ pre = pre_epi
442
+ if finish_ppi_in is not None:
443
+ pre_ppi = pre[
444
+ pre["FINISH PPI"].between(finish_ppi_in * 0.7, finish_ppi_in * 1.3)
445
+ ]
446
+ if len(pre_ppi) >= 20:
447
+ pre = pre_ppi
448
+ if target_gsm_in is not None:
449
+ pre_gsm = pre[
450
+ pre["FINISH GSM"].between(target_gsm_in * 0.75, target_gsm_in * 1.25)
451
+ ]
452
+ if len(pre_gsm) >= 20:
453
+ pre = pre_gsm
454
+ chosen = pre
455
+
456
+ work = chosen.copy()
457
+ work["score"] = work.apply(lambda r: self._distance_score(r, payload), axis=1)
458
+ work = work.sort_values("score", ascending=True)
459
+ top = work.head(min(250, len(work)))
460
+
461
+ outputs = {
462
+ "reed_count": "Reed Count",
463
+ "ends_per_dent": "Ends per dent",
464
+ "reed_space": "Reed space",
465
+ "greige_epi": "Greige EPI",
466
+ "greige_ppi": "Greige PPI",
467
+ "finish_epi": "FINISH EPI",
468
+ "finish_ppi": "FINISH PPI",
469
+ "target_gsm": "FINISH GSM",
470
+ "finish_width": "FINISH WIDTH",
471
+ "loom_type": "loom_type",
472
+ }
473
+
474
+ field_stats = {
475
+ k: self._field_stats(top[v]) for k, v in outputs.items() if v in top.columns
476
+ }
477
+
478
+ recommendation = {k: st["median"] for k, st in field_stats.items()}
479
+
480
+ epi_cv = field_stats.get("greige_epi", {}).get("cv") or 0
481
+ ppi_cv = field_stats.get("greige_ppi", {}).get("cv") or 0
482
+ cv = round(float(np.mean([epi_cv, ppi_cv])), 3)
483
+
484
+ confidence = self._confidence(len(top), cv)
485
+
486
+ references = top[
487
+ [
488
+ "master_article",
489
+ "article",
490
+ "weave",
491
+ "blend",
492
+ "Greige EPI",
493
+ "Greige PPI",
494
+ "FINISH EPI",
495
+ "FINISH PPI",
496
+ "FINISH GSM",
497
+ "Reed Count",
498
+ "Reed space",
499
+ "score",
500
+ ]
501
+ ].head(8)
502
+ references["score"] = references["score"].round(3)
503
+
504
+ return {
505
+ "input": {
506
+ "warp_count": warp_in,
507
+ "weft_count": weft_in,
508
+ "finish_epi": finish_epi_in,
509
+ "finish_ppi": finish_ppi_in,
510
+ "weave": weave_in,
511
+ "blend": blend_in,
512
+ "target_gsm": target_gsm_in,
513
+ "count_band": input_count_band,
514
+ "dataset": dataset,
515
+ },
516
+ "recommendation": recommendation,
517
+ "statistics": field_stats,
518
+ "data_quality": {
519
+ "matching_source": source,
520
+ "matches_analyzed": int(len(top)),
521
+ "coefficient_of_variation": cv,
522
+ "confidence": confidence,
523
+ },
524
+ "reference_articles": references.to_dict(orient="records"),
525
+ }
526
+
527
+ @lru_cache(maxsize=4)
528
+ def get_validation_report(self, sample_size: int = 250, seed: int = 42) -> dict:
529
+ df = self._ensure().dropna(
530
+ subset=[
531
+ "weave",
532
+ "blend",
533
+ "Greige EPI",
534
+ "Greige PPI",
535
+ "FINISH EPI",
536
+ "FINISH PPI",
537
+ ]
538
+ )
539
+ if df.empty:
540
+ return {"error": "No valid rows for validation"}
541
+
542
+ n = int(min(max(30, sample_size), len(df)))
543
+ sample = df.sample(n=n, random_state=seed)
544
+
545
+ abs_epi, abs_ppi, conf = (
546
+ [],
547
+ [],
548
+ {"high": 0, "medium": 0, "low": 0, "very_low": 0},
549
+ )
550
+
551
+ for _, row in sample.iterrows():
552
+ pred = self.predict_construction(
553
+ {
554
+ "weave": row["weave"],
555
+ "blend": row["blend"],
556
+ "finish_epi": float(row["FINISH EPI"]),
557
+ "finish_ppi": float(row["FINISH PPI"]),
558
+ "target_gsm": float(row["FINISH GSM"])
559
+ if pd.notna(row["FINISH GSM"])
560
+ else None,
561
+ }
562
+ )
563
+ rec = pred.get("recommendation", {})
564
+ if rec.get("greige_epi") and rec.get("greige_ppi"):
565
+ abs_epi.append(abs(float(rec["greige_epi"]) - float(row["Greige EPI"])))
566
+ abs_ppi.append(abs(float(rec["greige_ppi"]) - float(row["Greige PPI"])))
567
+ c = pred.get("data_quality", {}).get("confidence", "very_low")
568
+ conf[c] = conf.get(c, 0) + 1
569
+
570
+ if not abs_epi or not abs_ppi:
571
+ return {"error": "Validation failed to produce comparable predictions"}
572
+
573
+ return {
574
+ "sample_size": n,
575
+ "scored_rows": len(abs_epi),
576
+ "mae": {
577
+ "greige_epi": round(float(np.mean(abs_epi)), 3),
578
+ "greige_ppi": round(float(np.mean(abs_ppi)), 3),
579
+ },
580
+ "confidence_distribution": conf,
581
+ }
582
+
583
+
584
+ data_service = DataService()
585
+
586
+
587
+ # ============== API Routes ==============
588
+
589
+
590
+ @app.on_event("startup")
591
+ async def startup_event():
592
+ print("Loading data...")
593
+ data_service.load_data()
594
+ print("Warming up caches...")
595
+ data_service.get_dashboard_summary()
596
+ data_service.get_filters()
597
+ print("Ready.")
598
+
599
+
600
+ @app.get("/")
601
+ def root():
602
+ index_path = os.path.join(
603
+ os.path.dirname(os.path.abspath(__file__)), "frontend", "out", "index.html"
604
+ )
605
+ if os.path.exists(index_path):
606
+ return FileResponse(index_path)
607
+ return {"message": "PDC Intelligence API. Build frontend to serve UI."}
608
+
609
+
610
+ @app.get("/api/health")
611
+ def health():
612
+ return data_service.get_health()
613
+
614
+
615
+ @app.get("/api/dashboard")
616
+ def dashboard():
617
+ return data_service.get_dashboard_summary()
618
+
619
+
620
+ @app.get("/api/filters")
621
+ def filters():
622
+ return data_service.get_filters()
623
+
624
+
625
+ @app.get("/api/articles")
626
+ def articles(
627
+ page: int = Query(1, ge=1),
628
+ limit: int = Query(25, ge=1, le=200),
629
+ search: str = "",
630
+ weave: str = "",
631
+ blend: str = "",
632
+ loom_type: str = "",
633
+ dataset: str = "",
634
+ count_band: str = "",
635
+ ):
636
+ return data_service.get_articles(
637
+ page, limit, search, weave, blend, loom_type, dataset, count_band
638
+ )
639
+
640
+
641
+ @app.get("/api/analytics/relativity")
642
+ def relativity():
643
+ return data_service.get_dashboard_summary()
644
+
645
+
646
+ @app.post("/api/predictions/construction")
647
+ def predict(payload: dict) -> dict:
648
+ return data_service.predict_construction(payload)
649
+
650
+
651
+ @app.get("/api/validation/report")
652
+ def validation_report(sample_size: int = Query(250, ge=30, le=2000), seed: int = 42):
653
+ return data_service.get_validation_report(sample_size=sample_size, seed=seed)
654
+
655
+
656
+ # Serve Next.js static frontend
657
+ FRONTEND_BUILD_DIR = os.path.join(
658
+ os.path.dirname(os.path.abspath(__file__)), "frontend", "out"
659
+ )
660
+ if os.path.isdir(FRONTEND_BUILD_DIR):
661
+ app.mount(
662
+ "/", StaticFiles(directory=FRONTEND_BUILD_DIR, html=True), name="frontend"
663
+ )
664
+ else:
665
+ # Fallback: serve basic HTML if frontend not built
666
+ @app.get("/")
667
+ def serve_frontend():
668
+ return HTMLResponse(
669
+ content="""
670
+ <!DOCTYPE html>
671
+ <html>
672
+ <head>
673
+ <title>PDC Intelligence</title>
674
+ <meta charset="utf-8">
675
+ <meta name="viewport" content="width=device-width, initial-scale=1">
676
+ <script src="https://cdn.tailwindcss.com"></script>
677
+ </head>
678
+ <body class="bg-slate-50">
679
+ <div class="p-8">
680
+ <h1 class="text-2xl font-bold text-slate-800">PDC Intelligence</h1>
681
+ <p class="text-slate-600 mt-2">Backend is running. API available at /api/*</p>
682
+ <div class="mt-4 p-4 bg-white rounded-lg border border-slate-200">
683
+ <h2 class="font-semibold">Available Endpoints:</h2>
684
+ <ul class="mt-2 space-y-1 text-sm">
685
+ <li><code class="bg-slate-100 px-2 py-1">GET /api/health</code> - Health check</li>
686
+ <li><code class="bg-slate-100 px-2 py-1">GET /api/dashboard</code> - Dashboard summary</li>
687
+ <li><code class="bg-slate-100 px-2 py-1">GET /api/filters</code> - Available filters</li>
688
+ <li><code class="bg-slate-100 px-2 py-1">GET /api/articles</code> - Article search</li>
689
+ <li><code class="bg-slate-100 px-2 py-1">POST /api/predictions/construction</code> - Construction prediction</li>
690
+ </ul>
691
+ </div>
692
+ </div>
693
+ </body>
694
+ </html>
695
+ """,
696
+ status_code=200,
697
+ )
698
+
699
+
700
+ if __name__ == "__main__":
701
+ import uvicorn
702
+
703
+ uvicorn.run(app, host="0.0.0.0", port=7860)
backend/.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ .venv
2
+ __pycache__
3
+ .pytest_cache
backend/app/__init__.py ADDED
File without changes
backend/app/data/Input & Out Put.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8b71a764d4c14209ba3c8eff8011580736a048b176bb465284a593ccfdd7611a
3
+ size 9417
backend/app/data/Piece Dyed Artcle details.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d95d9af054823e797a618008199dfc2ebb184118cb95bdab29913e779ac792cd
3
+ size 300302
backend/app/data/Working 24 Jan.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:98c52c8631ef9a73f9d3487e7650ce702ee21d2da9223bc5d1312bc4cf64e2a6
3
+ size 1746811
backend/app/main.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Query
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+
4
+ from app.services.data_service import data_service
5
+
6
+ app = FastAPI(title="PDC Intelligence API", version="1.0.0")
7
+
8
+ app.add_middleware(
9
+ CORSMiddleware,
10
+ allow_origins=["http://localhost:3000", "http://localhost:8000"],
11
+ allow_credentials=True,
12
+ allow_methods=["*"],
13
+ allow_headers=["*"],
14
+ )
15
+
16
+ @app.on_event("startup")
17
+ async def startup_event() -> None:
18
+ print("Loading data...")
19
+ data_service.load_data()
20
+ print("Warming up API caches...")
21
+ data_service.get_dashboard_summary()
22
+ data_service.get_filters()
23
+ data_service.get_relativity_analytics()
24
+ data_service.get_validation_report(sample_size=100, seed=42)
25
+ print("Cache warming complete.")
26
+
27
+ @app.get("/")
28
+ def root() -> dict:
29
+ return {"message": "PDC Intelligence API is running"}
30
+
31
+ @app.get("/api/health")
32
+ def health() -> dict:
33
+ return data_service.get_health()
34
+
35
+ @app.get("/api/dashboard")
36
+ def dashboard() -> dict:
37
+ return data_service.get_dashboard_summary()
38
+
39
+ @app.get("/api/filters")
40
+ def filters() -> dict:
41
+ return data_service.get_filters()
42
+
43
+ @app.get("/api/articles")
44
+ def articles(
45
+ page: int = Query(1, ge=1),
46
+ limit: int = Query(25, ge=1, le=200),
47
+ search: str = "",
48
+ weave: str = "",
49
+ blend: str = "",
50
+ loom_type: str = "",
51
+ dataset: str = "",
52
+ count_band: str = "",
53
+ ) -> dict:
54
+ return data_service.get_articles(
55
+ page,
56
+ limit,
57
+ search,
58
+ weave,
59
+ blend,
60
+ loom_type,
61
+ dataset,
62
+ count_band,
63
+ )
64
+
65
+ @app.get("/api/article/{master_article}")
66
+ def article(master_article: str) -> dict:
67
+ return data_service.get_article_detail(master_article)
68
+
69
+ @app.get("/api/analytics/relativity")
70
+ def relativity() -> dict:
71
+ return data_service.get_relativity_analytics()
72
+
73
+ @app.get("/api/process-flow")
74
+ def process_flow() -> dict:
75
+ return data_service.get_process_flow()
76
+
77
+ @app.get("/api/documentation")
78
+ def documentation() -> dict:
79
+ return data_service.get_documentation()
80
+
81
+ @app.get("/api/data-source/audit")
82
+ def data_source_audit() -> dict:
83
+ return data_service.get_data_source_audit()
84
+
85
+ @app.get("/api/validation/report")
86
+ def validation_report(
87
+ sample_size: int = Query(250, ge=30, le=2000), seed: int = 42
88
+ ) -> dict:
89
+ return data_service.get_validation_report(sample_size=sample_size, seed=seed)
90
+
91
+ @app.post("/api/predictions/construction")
92
+ def predict(payload: dict) -> dict:
93
+ return data_service.predict_construction(payload)
backend/app/services/__init__.py ADDED
File without changes
backend/app/services/data_service.py ADDED
@@ -0,0 +1,876 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import lru_cache
2
+ import os
3
+ import re
4
+ from typing import Any
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+
9
+
10
+ _DATA_DIR = os.path.join(
11
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data"
12
+ )
13
+ WORKING_PATH = os.path.join(_DATA_DIR, "Working 24 Jan.xlsx")
14
+ PIECE_PATH = os.path.join(_DATA_DIR, "Piece Dyed Artcle details.xlsx")
15
+
16
+
17
+ def _clean_text(value: Any) -> str:
18
+ if value is None:
19
+ return ""
20
+ return str(value).strip()
21
+
22
+
23
+ def _safe_float(value: Any) -> float | None:
24
+ if value is None:
25
+ return None
26
+ text = str(value).strip().replace(",", "")
27
+ if text == "" or text.lower() in {"nan", "none"}:
28
+ return None
29
+ try:
30
+ return float(text)
31
+ except Exception:
32
+ return None
33
+
34
+
35
+ def _extract_count(value: Any) -> float | None:
36
+ text = _clean_text(value).upper()
37
+ nums = re.findall(r"\d+(?:\.\d+)?", text)
38
+ if not nums:
39
+ return None
40
+ vals = [float(n) for n in nums]
41
+ if not vals:
42
+ return None
43
+ return float(max(vals))
44
+
45
+
46
+ def _normalize_weave(value: Any) -> str:
47
+ text = _clean_text(value).upper()
48
+ text = re.sub(r"\s+", " ", text)
49
+ text = text.replace("TWL", "TWILL")
50
+ return text
51
+
52
+
53
+ def _normalize_blend(value: Any) -> str:
54
+ text = _clean_text(value).upper().replace("PER", "%")
55
+ text = re.sub(r"\s+", " ", text)
56
+ text = text.replace("COTTON", "CO")
57
+ text = text.replace("POLYESTER", "PES")
58
+ return text
59
+
60
+
61
+ def _count_band(value: float | None) -> str:
62
+ if value is None:
63
+ return "unknown"
64
+ return "below_40" if value < 40 else "40_and_above"
65
+
66
+
67
+ class DataService:
68
+ def __init__(self) -> None:
69
+ self.df: pd.DataFrame | None = None
70
+ self.is_loaded = False
71
+ self.last_error = ""
72
+ self.raw_working_rows = 0
73
+ self.raw_piece_rows = 0
74
+ self.used_working_rows = 0
75
+ self.used_piece_rows = 0
76
+
77
+ def load_data(self) -> None:
78
+ try:
79
+ working = pd.read_excel(WORKING_PATH)
80
+ piece = pd.read_excel(PIECE_PATH)
81
+ self.raw_working_rows = int(len(working))
82
+ self.raw_piece_rows = int(len(piece))
83
+
84
+ working["dataset"] = "working"
85
+ piece["dataset"] = "piece_dyed"
86
+
87
+ if "Loom Type" not in working.columns:
88
+ working["Loom Type"] = ""
89
+
90
+ cols = sorted(set(working.columns).union(set(piece.columns)))
91
+ working = working.reindex(columns=cols)
92
+ piece = piece.reindex(columns=cols)
93
+
94
+ df = pd.concat([working, piece], ignore_index=True)
95
+
96
+ # Canonical fields
97
+ df["article"] = df["Article"].astype(str)
98
+ df["master_article"] = df["Master Article"].astype(str)
99
+ df["variant"] = df["Options/Variants"].astype(str)
100
+ df["loom_type"] = df["Loom Type"].fillna("").astype(str)
101
+
102
+ df["weave_raw"] = df["WEAVE"].fillna("").astype(str)
103
+ df["weave"] = df["weave_raw"].map(_normalize_weave)
104
+ df["blend_raw"] = df["Blend Ratio"].fillna("").astype(str)
105
+ df["blend"] = df["blend_raw"].map(_normalize_blend)
106
+
107
+ for col in [
108
+ "Reed Count",
109
+ "Ends per dent",
110
+ "Reed space",
111
+ "Greige EPI",
112
+ "Greige PPI",
113
+ "Greige Width in INCH",
114
+ "FINISH EPI",
115
+ "FINISH PPI",
116
+ "FINISH GSM",
117
+ "FINISH WIDTH",
118
+ "ON LOOM EPI",
119
+ "ON LOOM PPI",
120
+ ]:
121
+ df[col] = pd.to_numeric(df[col], errors="coerce")
122
+
123
+ df["warp_count"] = df[" Warp Code 1"].map(_extract_count)
124
+ df["weft_count"] = df[" Weft Code 1"].map(_extract_count)
125
+ df["count_ref"] = df[["warp_count", "weft_count"]].max(axis=1)
126
+ df["count_band"] = df["count_ref"].map(_count_band)
127
+
128
+ df["epi_change_pct"] = np.where(
129
+ df["Greige EPI"] > 0,
130
+ ((df["FINISH EPI"] - df["Greige EPI"]) / df["Greige EPI"]) * 100,
131
+ np.nan,
132
+ )
133
+ df["ppi_change_pct"] = np.where(
134
+ df["Greige PPI"] > 0,
135
+ ((df["FINISH PPI"] - df["Greige PPI"]) / df["Greige PPI"]) * 100,
136
+ np.nan,
137
+ )
138
+ df["width_change_pct"] = np.where(
139
+ df["Greige Width in INCH"] > 0,
140
+ (
141
+ (df["Greige Width in INCH"] - df["FINISH WIDTH"])
142
+ / df["Greige Width in INCH"]
143
+ )
144
+ * 100,
145
+ np.nan,
146
+ )
147
+
148
+ # Keep valid rows with essential fields
149
+ df = df[(df["weave"].str.len() > 0) & (df["blend"].str.len() > 0)]
150
+ self.used_working_rows = int((df["dataset"] == "working").sum())
151
+ self.used_piece_rows = int((df["dataset"] == "piece_dyed").sum())
152
+
153
+ self.df = df
154
+ self.is_loaded = True
155
+ self.last_error = ""
156
+ except Exception as exc:
157
+ self.last_error = str(exc)
158
+ self.is_loaded = False
159
+ raise
160
+
161
+ def _ensure(self) -> pd.DataFrame:
162
+ if not self.is_loaded or self.df is None:
163
+ self.load_data()
164
+ return self.df
165
+
166
+ def get_health(self) -> dict:
167
+ df = self._ensure()
168
+ return {
169
+ "status": "healthy" if self.is_loaded else "degraded",
170
+ "rows": int(len(df)),
171
+ "error": self.last_error,
172
+ }
173
+
174
+ @lru_cache(maxsize=1)
175
+ def get_dashboard_summary(self) -> dict:
176
+ df = self._ensure()
177
+ top_weaves = (
178
+ df["weave"]
179
+ .value_counts()
180
+ .head(10)
181
+ .rename_axis("weave")
182
+ .reset_index(name="count")
183
+ )
184
+ top_blends = (
185
+ df["blend"]
186
+ .value_counts()
187
+ .head(10)
188
+ .rename_axis("blend")
189
+ .reset_index(name="count")
190
+ )
191
+
192
+ return {
193
+ "totals": {
194
+ "rows": int(len(df)),
195
+ "unique_articles": int(df["article"].nunique()),
196
+ "unique_master_articles": int(df["master_article"].nunique()),
197
+ "unique_weaves": int(df["weave"].nunique()),
198
+ "unique_blends": int(df["blend"].nunique()),
199
+ },
200
+ "quality": {
201
+ "finish_ppi_missing_pct": round(
202
+ float(df["FINISH PPI"].isna().mean() * 100), 2
203
+ ),
204
+ "loom_type_missing_pct": round(
205
+ float(df["loom_type"].eq("").mean() * 100), 2
206
+ ),
207
+ },
208
+ "relativity": {
209
+ "median_epi_change_pct": round(
210
+ float(df["epi_change_pct"].median(skipna=True)), 3
211
+ ),
212
+ "median_ppi_change_pct": round(
213
+ float(df["ppi_change_pct"].median(skipna=True)), 3
214
+ ),
215
+ "median_width_change_pct": round(
216
+ float(df["width_change_pct"].median(skipna=True)), 3
217
+ ),
218
+ },
219
+ "top_weaves": top_weaves.to_dict(orient="records"),
220
+ "top_blends": top_blends.to_dict(orient="records"),
221
+ }
222
+
223
+ @lru_cache(maxsize=1)
224
+ def get_filters(self) -> dict:
225
+ df = self._ensure()
226
+ return {
227
+ "weaves": sorted(df["weave"].dropna().unique().tolist())[:500],
228
+ "blends": sorted(df["blend"].dropna().unique().tolist())[:500],
229
+ "loom_types": sorted(
230
+ [
231
+ x
232
+ for x in df["loom_type"].dropna().unique().tolist()
233
+ if str(x).strip()
234
+ ]
235
+ ),
236
+ "count_bands": ["unknown", "below_40", "40_and_above"],
237
+ "datasets": ["all", "working", "piece_dyed"],
238
+ }
239
+
240
+ def get_articles(
241
+ self,
242
+ page: int,
243
+ limit: int,
244
+ search: str,
245
+ weave: str,
246
+ blend: str,
247
+ loom_type: str,
248
+ dataset: str,
249
+ count_band: str,
250
+ ) -> dict:
251
+ df = self._ensure()
252
+
253
+ if search:
254
+ s = search.lower().strip()
255
+ df = df[
256
+ df["master_article"].str.lower().str.contains(s, na=False)
257
+ | df["article"].str.lower().str.contains(s, na=False)
258
+ | df["variant"].str.lower().str.contains(s, na=False)
259
+ | df["weave"].str.lower().str.contains(s, na=False)
260
+ | df["blend"].str.lower().str.contains(s, na=False)
261
+ ]
262
+ if weave:
263
+ df = df[df["weave"] == weave]
264
+ if blend:
265
+ df = df[df["blend"] == blend]
266
+ if loom_type:
267
+ df = df[df["loom_type"] == loom_type]
268
+ if dataset in {"working", "piece_dyed"}:
269
+ df = df[df["dataset"] == dataset]
270
+ if count_band in {"unknown", "below_40", "40_and_above"}:
271
+ df = df[df["count_band"] == count_band]
272
+
273
+ grouped = (
274
+ df.groupby("master_article", dropna=False)
275
+ .agg(
276
+ article=("article", "first"),
277
+ variant=("variant", "first"),
278
+ weave=("weave", "first"),
279
+ blend=("blend", "first"),
280
+ warp_count=("warp_count", "max"),
281
+ weft_count=("weft_count", "max"),
282
+ loom_type=("loom_type", "first"),
283
+ rows=("master_article", "count"),
284
+ reed_count=("Reed Count", "median"),
285
+ ends_per_dent=("Ends per dent", "median"),
286
+ reed_space=("Reed space", "median"),
287
+ finish_gsm_median=("FINISH GSM", "median"),
288
+ finish_epi_median=("FINISH EPI", "median"),
289
+ finish_ppi_median=("FINISH PPI", "median"),
290
+ greige_epi_median=("Greige EPI", "median"),
291
+ greige_ppi_median=("Greige PPI", "median"),
292
+ )
293
+ .reset_index()
294
+ .sort_values("rows", ascending=False)
295
+ )
296
+
297
+ total = int(len(grouped))
298
+ start = (page - 1) * limit
299
+ end = start + limit
300
+
301
+ chunk = grouped.iloc[start:end].copy()
302
+ for col in [
303
+ "finish_gsm_median",
304
+ "finish_epi_median",
305
+ "finish_ppi_median",
306
+ "greige_epi_median",
307
+ "greige_ppi_median",
308
+ ]:
309
+ chunk[col] = chunk[col].round(3)
310
+
311
+ return {
312
+ "data": chunk.to_dict(orient="records"),
313
+ "page": page,
314
+ "limit": limit,
315
+ "total": total,
316
+ "total_pages": int(np.ceil(total / limit)) if total else 0,
317
+ }
318
+
319
+ def get_article_detail(self, master_article: str) -> dict:
320
+ df = self._ensure()
321
+ view = df[df["master_article"].astype(str) == str(master_article)].copy()
322
+ if view.empty:
323
+ return {"error": "Article not found"}
324
+
325
+ head = view.iloc[0]
326
+ stats = {
327
+ "rows": int(len(view)),
328
+ "finish_epi": round(float(view["FINISH EPI"].median(skipna=True)), 3),
329
+ "finish_ppi": round(float(view["FINISH PPI"].median(skipna=True)), 3),
330
+ "finish_gsm": round(float(view["FINISH GSM"].median(skipna=True)), 3),
331
+ "greige_epi": round(float(view["Greige EPI"].median(skipna=True)), 3),
332
+ "greige_ppi": round(float(view["Greige PPI"].median(skipna=True)), 3),
333
+ "epi_change_pct": round(
334
+ float(view["epi_change_pct"].median(skipna=True)), 3
335
+ ),
336
+ "ppi_change_pct": round(
337
+ float(view["ppi_change_pct"].median(skipna=True)), 3
338
+ ),
339
+ "width_change_pct": round(
340
+ float(view["width_change_pct"].median(skipna=True)), 3
341
+ ),
342
+ }
343
+
344
+ samples = view[
345
+ [
346
+ "dataset",
347
+ "article",
348
+ "variant",
349
+ "weave",
350
+ "blend",
351
+ "Greige EPI",
352
+ "Greige PPI",
353
+ "FINISH EPI",
354
+ "FINISH PPI",
355
+ "FINISH GSM",
356
+ "FINISH WIDTH",
357
+ ]
358
+ ].head(20)
359
+
360
+ return {
361
+ "master_article": str(master_article),
362
+ "profile": {
363
+ "article": str(head.get("article", "")),
364
+ "variant": str(head.get("variant", "")),
365
+ "weave": str(head.get("weave", "")),
366
+ "blend": str(head.get("blend", "")),
367
+ "loom_type": str(head.get("loom_type", "")),
368
+ },
369
+ "stats": stats,
370
+ "samples": samples.to_dict(orient="records"),
371
+ }
372
+
373
+ def _safe_cv(self, series: pd.Series) -> float:
374
+ series = pd.to_numeric(series, errors="coerce").dropna()
375
+ if len(series) < 2:
376
+ return 0.0
377
+ mean = float(series.mean().item())
378
+ if abs(mean) < 1e-9:
379
+ return 0.0
380
+ std = float(series.std(ddof=1).item())
381
+ return float(abs(std / mean) * 100)
382
+
383
+ def _field_stats(self, series: pd.Series) -> dict:
384
+ s = pd.to_numeric(series, errors="coerce").dropna()
385
+ if s.empty:
386
+ return {
387
+ "p25": None,
388
+ "median": None,
389
+ "p75": None,
390
+ "mean": None,
391
+ "std": None,
392
+ "cv": None,
393
+ "used_count": 0,
394
+ "raw_count": 0,
395
+ }
396
+
397
+ raw_count = len(s)
398
+ if len(s) >= 8:
399
+ q1 = s.quantile(0.25).item()
400
+ q3 = s.quantile(0.75).item()
401
+ iqr = q3 - q1
402
+ lower = q1 - (1.5 * iqr)
403
+ upper = q3 + (1.5 * iqr)
404
+ s = s[(s >= lower) & (s <= upper)]
405
+ if s.empty:
406
+ s = pd.to_numeric(series, errors="coerce").dropna()
407
+
408
+ return {
409
+ "p25": round(float(s.quantile(0.25).item()), 3),
410
+ "median": round(float(s.median().item()), 3),
411
+ "p75": round(float(s.quantile(0.75).item()), 3),
412
+ "mean": round(float(s.mean().item()), 3),
413
+ "std": round(float(s.std(ddof=1).item()) if len(s) > 1 else 0.0, 3),
414
+ "cv": round(float(self._safe_cv(s)), 3),
415
+ "used_count": int(len(s)),
416
+ "raw_count": int(raw_count),
417
+ }
418
+
419
+ def _distance_score(self, row: pd.Series, payload: dict) -> float:
420
+ score = 0.0
421
+ if (
422
+ payload.get("weave")
423
+ and _normalize_weave(payload.get("weave")) != row["weave"]
424
+ ):
425
+ score += 4.0
426
+ if (
427
+ payload.get("blend")
428
+ and _normalize_blend(payload.get("blend")) != row["blend"]
429
+ ):
430
+ score += 3.0
431
+
432
+ for inp, col, w in [
433
+ ("warp_count", "warp_count", 2.0),
434
+ ("weft_count", "weft_count", 2.0),
435
+ ("finish_epi", "FINISH EPI", 1.5),
436
+ ("finish_ppi", "FINISH PPI", 1.5),
437
+ ("target_gsm", "FINISH GSM", 1.0),
438
+ ]:
439
+ v = _safe_float(payload.get(inp))
440
+ if v is None:
441
+ continue
442
+ rv = _safe_float(row.get(col))
443
+ if rv is None:
444
+ score += 1.0
445
+ else:
446
+ score += w * abs(v - rv) / max(abs(v), 1.0)
447
+ return score
448
+
449
+ def _confidence(self, match_count: int, cv: float) -> str:
450
+ if match_count >= 60 and cv <= 20:
451
+ return "high"
452
+ if match_count >= 20 and cv <= 35:
453
+ return "medium"
454
+ if match_count >= 8:
455
+ return "low"
456
+ return "very_low"
457
+
458
+ def predict_construction(self, payload: dict) -> dict:
459
+ df = self._ensure()
460
+
461
+ weave_in = _normalize_weave(payload.get("weave", ""))
462
+ blend_in = _normalize_blend(payload.get("blend", ""))
463
+ dataset = _clean_text(payload.get("dataset", "all")).lower()
464
+ loom_type_in = _clean_text(payload.get("loom_type", ""))
465
+
466
+ warp_in = _safe_float(payload.get("warp_count"))
467
+ weft_in = _safe_float(payload.get("weft_count"))
468
+ finish_epi_in = _safe_float(payload.get("finish_epi"))
469
+ finish_ppi_in = _safe_float(payload.get("finish_ppi"))
470
+ target_gsm_in = _safe_float(payload.get("target_gsm"))
471
+
472
+ input_count_ref = None
473
+ if warp_in is not None or weft_in is not None:
474
+ input_count_ref = max(v for v in [warp_in, weft_in] if v is not None)
475
+ input_count_band = _count_band(input_count_ref)
476
+
477
+ if not weave_in or not blend_in:
478
+ return {
479
+ "error": "weave and blend are required",
480
+ "required_fields": ["weave", "blend"],
481
+ }
482
+
483
+ if dataset in {"working", "piece_dyed"}:
484
+ df = df[df["dataset"] == dataset]
485
+
486
+ if loom_type_in:
487
+ loom_filtered = df[df["loom_type"].str.lower() == loom_type_in.lower()]
488
+ if len(loom_filtered) >= 30:
489
+ df = loom_filtered
490
+
491
+ # Hierarchical filtering
492
+ lvl1 = df[(df["weave"] == weave_in) & (df["blend"] == blend_in)]
493
+ if input_count_band != "unknown":
494
+ lvl1_band = lvl1[lvl1["count_band"] == input_count_band]
495
+ if len(lvl1_band) >= 20:
496
+ lvl1 = lvl1_band
497
+
498
+ lvl2 = df[df["weave"] == weave_in]
499
+ if input_count_band != "unknown":
500
+ lvl2_band = lvl2[lvl2["count_band"] == input_count_band]
501
+ if len(lvl2_band) >= 25:
502
+ lvl2 = lvl2_band
503
+
504
+ lvl3 = df
505
+
506
+ chosen = lvl1
507
+ source = "exact weave + blend"
508
+ if len(chosen) < 20:
509
+ chosen = lvl2
510
+ source = "weave fallback"
511
+ if len(chosen) < 20:
512
+ chosen = lvl3
513
+ source = "global fallback"
514
+
515
+ # Optional numeric pre-filter to improve neighborhood quality
516
+ pre = chosen.copy()
517
+ if finish_epi_in is not None and "FINISH EPI" in pre.columns:
518
+ pre_epi = pre[
519
+ pre["FINISH EPI"].between(finish_epi_in * 0.7, finish_epi_in * 1.3)
520
+ ]
521
+ if len(pre_epi) >= 20:
522
+ pre = pre_epi
523
+ if finish_ppi_in is not None and "FINISH PPI" in pre.columns:
524
+ pre_ppi = pre[
525
+ pre["FINISH PPI"].between(finish_ppi_in * 0.7, finish_ppi_in * 1.3)
526
+ ]
527
+ if len(pre_ppi) >= 20:
528
+ pre = pre_ppi
529
+ if target_gsm_in is not None and "FINISH GSM" in pre.columns:
530
+ pre_gsm = pre[
531
+ pre["FINISH GSM"].between(target_gsm_in * 0.75, target_gsm_in * 1.25)
532
+ ]
533
+ if len(pre_gsm) >= 20:
534
+ pre = pre_gsm
535
+ chosen = pre
536
+
537
+ work = chosen.copy()
538
+ work["score"] = work.apply(lambda r: self._distance_score(r, payload), axis=1)
539
+ work = work.sort_values("score", ascending=True)
540
+ top = work.head(min(250, len(work)))
541
+
542
+ outputs = {
543
+ "reed_count": "Reed Count",
544
+ "ends_per_dent": "Ends per dent",
545
+ "reed_space": "Reed space",
546
+ "greige_epi": "Greige EPI",
547
+ "greige_ppi": "Greige PPI",
548
+ "finish_epi": "FINISH EPI",
549
+ "finish_ppi": "FINISH PPI",
550
+ "target_gsm": "FINISH GSM",
551
+ "finish_width": "FINISH WIDTH",
552
+ "loom_type": "loom_type",
553
+ "cover_factor": "ON LOOM EPI",
554
+ }
555
+
556
+ field_stats = {
557
+ k: self._field_stats(top[v]) for k, v in outputs.items() if v in top.columns
558
+ }
559
+
560
+ recommendation = {}
561
+ optimistic = {}
562
+ pessimistic = {}
563
+ for k, st in field_stats.items():
564
+ recommendation[k] = st["median"]
565
+ optimistic[k] = st["p25"]
566
+ pessimistic[k] = st["p75"]
567
+
568
+ # Loom type is categorical
569
+ if "loom_type" in top.columns:
570
+ mode_series = top["loom_type"].mask(top["loom_type"] == "").dropna()
571
+ recommendation["loom_type"] = (
572
+ mode_series.mode().iloc[0] if not mode_series.empty else ""
573
+ )
574
+ optimistic["loom_type"] = recommendation["loom_type"]
575
+ pessimistic["loom_type"] = recommendation["loom_type"]
576
+
577
+ epi_cv = field_stats.get("greige_epi", {}).get("cv") or 0
578
+ ppi_cv = field_stats.get("greige_ppi", {}).get("cv") or 0
579
+ cv = round(float(np.mean([epi_cv, ppi_cv])), 3)
580
+
581
+ confidence = self._confidence(len(top), cv)
582
+
583
+ references = top[
584
+ [
585
+ "master_article",
586
+ "article",
587
+ "weave",
588
+ "blend",
589
+ "Greige EPI",
590
+ "Greige PPI",
591
+ "FINISH EPI",
592
+ "FINISH PPI",
593
+ "FINISH GSM",
594
+ "Reed Count",
595
+ "Reed space",
596
+ "Ends per dent",
597
+ "score",
598
+ ]
599
+ ].head(8)
600
+ references["score"] = references["score"].round(3)
601
+
602
+ return {
603
+ "input": {
604
+ "warp_count": warp_in,
605
+ "weft_count": weft_in,
606
+ "finish_epi": finish_epi_in,
607
+ "finish_ppi": finish_ppi_in,
608
+ "weave": weave_in,
609
+ "blend": blend_in,
610
+ "target_gsm": target_gsm_in,
611
+ "loom_type": loom_type_in,
612
+ "count_band": input_count_band,
613
+ "dataset": dataset,
614
+ },
615
+ "recommendation": recommendation,
616
+ "scenarios": {
617
+ "optimistic": optimistic,
618
+ "expected": recommendation,
619
+ "pessimistic": pessimistic,
620
+ },
621
+ "statistics": field_stats,
622
+ "data_quality": {
623
+ "matching_source": source,
624
+ "matches_analyzed": int(len(top)),
625
+ "pre_filter_matches": int(len(chosen)),
626
+ "coefficient_of_variation": cv,
627
+ "confidence": confidence,
628
+ },
629
+ "explanation": (
630
+ f"Recommendation is based on {len(top)} historical rows using {source}. "
631
+ f"Median values are used for robustness, with P25/P75 for optimistic and pessimistic scenarios."
632
+ ),
633
+ "reference_articles": references.to_dict(orient="records"),
634
+ }
635
+
636
+ @lru_cache(maxsize=4)
637
+ def get_validation_report(self, sample_size: int = 250, seed: int = 42) -> dict:
638
+ df = self._ensure().dropna(
639
+ subset=[
640
+ "weave",
641
+ "blend",
642
+ "Greige EPI",
643
+ "Greige PPI",
644
+ "FINISH EPI",
645
+ "FINISH PPI",
646
+ ]
647
+ )
648
+ if df.empty:
649
+ return {"error": "No valid rows for validation"}
650
+
651
+ n = int(min(max(30, sample_size), len(df)))
652
+ sample = df.sample(n=n, random_state=seed)
653
+
654
+ abs_epi = []
655
+ abs_ppi = []
656
+ conf = {"high": 0, "medium": 0, "low": 0, "very_low": 0}
657
+
658
+ for _, row in sample.iterrows():
659
+ pred = self.predict_construction(
660
+ {
661
+ "weave": row["weave"],
662
+ "blend": row["blend"],
663
+ "finish_epi": float(row["FINISH EPI"]),
664
+ "finish_ppi": float(row["FINISH PPI"]),
665
+ "target_gsm": float(row["FINISH GSM"])
666
+ if pd.notna(row["FINISH GSM"])
667
+ else None,
668
+ "loom_type": row.get("loom_type", ""),
669
+ }
670
+ )
671
+ rec = pred.get("recommendation", {})
672
+ if rec.get("greige_epi") is None or rec.get("greige_ppi") is None:
673
+ continue
674
+
675
+ abs_epi.append(abs(float(rec["greige_epi"]) - float(row["Greige EPI"])))
676
+ abs_ppi.append(abs(float(rec["greige_ppi"]) - float(row["Greige PPI"])))
677
+ c = pred.get("data_quality", {}).get("confidence", "very_low")
678
+ conf[c] = conf.get(c, 0) + 1
679
+
680
+ if not abs_epi or not abs_ppi:
681
+ return {"error": "Validation failed to produce comparable predictions"}
682
+
683
+ def _p(values: list[float], q: float) -> float:
684
+ vals = sorted(values)
685
+ idx = max(0, min(len(vals) - 1, int(round((len(vals) - 1) * q))))
686
+ return float(vals[idx])
687
+
688
+ return {
689
+ "sample_size": n,
690
+ "scored_rows": len(abs_epi),
691
+ "mae": {
692
+ "greige_epi": round(float(np.mean(abs_epi)), 3),
693
+ "greige_ppi": round(float(np.mean(abs_ppi)), 3),
694
+ },
695
+ "percentiles_abs_error": {
696
+ "greige_epi_p50": round(_p(abs_epi, 0.50), 3),
697
+ "greige_epi_p90": round(_p(abs_epi, 0.90), 3),
698
+ "greige_ppi_p50": round(_p(abs_ppi, 0.50), 3),
699
+ "greige_ppi_p90": round(_p(abs_ppi, 0.90), 3),
700
+ },
701
+ "confidence_distribution": conf,
702
+ }
703
+
704
+ @lru_cache(maxsize=1)
705
+ def get_relativity_analytics(self) -> dict:
706
+ df = self._ensure()
707
+ weave_stats = (
708
+ df.groupby("weave", dropna=False)
709
+ .agg(
710
+ rows=("weave", "count"),
711
+ epi_change_pct=("epi_change_pct", "median"),
712
+ ppi_change_pct=("ppi_change_pct", "median"),
713
+ width_change_pct=("width_change_pct", "median"),
714
+ )
715
+ .reset_index()
716
+ .sort_values("rows", ascending=False)
717
+ .head(20)
718
+ )
719
+
720
+ blend_stats = (
721
+ df.groupby("blend", dropna=False)
722
+ .agg(
723
+ rows=("blend", "count"),
724
+ epi_change_pct=("epi_change_pct", "median"),
725
+ ppi_change_pct=("ppi_change_pct", "median"),
726
+ )
727
+ .reset_index()
728
+ .sort_values("rows", ascending=False)
729
+ .head(20)
730
+ )
731
+
732
+ corr_cols = [
733
+ "Greige EPI",
734
+ "Greige PPI",
735
+ "FINISH EPI",
736
+ "FINISH PPI",
737
+ "FINISH GSM",
738
+ "FINISH WIDTH",
739
+ "ON LOOM EPI",
740
+ "ON LOOM PPI",
741
+ ]
742
+ corr = df[corr_cols].corr(numeric_only=True).fillna(0).round(3)
743
+
744
+ # Scatter data for chart
745
+ scatter_df = df[["FINISH EPI", "Greige EPI"]].dropna().sample(n=min(500, len(df)))
746
+ scatter_data = scatter_df.rename(columns={"FINISH EPI": "finish_epi", "Greige EPI": "greige_epi"}).to_dict(orient="records")
747
+
748
+ return {
749
+ "by_weave": weave_stats.round(3).to_dict(orient="records"),
750
+ "by_blend": blend_stats.round(3).to_dict(orient="records"),
751
+ "correlation_matrix": corr.to_dict(),
752
+ "epi_correlation": scatter_data
753
+ }
754
+ def get_process_flow(self) -> dict:
755
+ return {
756
+ "steps": [
757
+ {
758
+ "step": 1,
759
+ "title": "Customer Swatch / Requirement Intake",
760
+ "description": "Capture fabric request by swatch or written construction targets.",
761
+ },
762
+ {
763
+ "step": 2,
764
+ "title": "Initial Technical Analysis",
765
+ "description": "Analyze weave, blend, count, finish EPI/PPI, GSM and compare with archive.",
766
+ },
767
+ {
768
+ "step": 3,
769
+ "title": "Archive Match & Feasibility",
770
+ "description": "Find exact/nearest historical article and validate manufacturing feasibility.",
771
+ },
772
+ {
773
+ "step": 4,
774
+ "title": "Construction Recommendation",
775
+ "description": "Predict greige construction and technical sheet parameters from historical behavior.",
776
+ },
777
+ {
778
+ "step": 5,
779
+ "title": "Planning Hand-off",
780
+ "description": "Pass recommendations to yarn and gray planning for execution.",
781
+ },
782
+ ]
783
+ }
784
+
785
+ def get_documentation(self) -> dict:
786
+ return {
787
+ "scope": {
788
+ "phase_1": "Gray fabric engineering and construction recommendation.",
789
+ "phase_2": "Secondary parameters like elongation/recovery and strength metrics.",
790
+ },
791
+ "input_parameters": [
792
+ "warp_count",
793
+ "weft_count",
794
+ "finish_epi",
795
+ "finish_ppi",
796
+ "weave",
797
+ "blend",
798
+ "target_gsm (optional)",
799
+ ],
800
+ "output_parameters": [
801
+ "reed_count",
802
+ "ends_per_dent",
803
+ "reed_space",
804
+ "greige_epi",
805
+ "greige_ppi",
806
+ "finish_epi",
807
+ "finish_ppi",
808
+ "finish_width",
809
+ "target_gsm",
810
+ "loom_type",
811
+ "cover_factor",
812
+ ],
813
+ "formulas": {
814
+ "epi_change_pct": "(FINISH EPI - Greige EPI) / Greige EPI * 100",
815
+ "ppi_change_pct": "(FINISH PPI - Greige PPI) / Greige PPI * 100",
816
+ "width_change_pct": "(Greige Width - Finish Width) / Greige Width * 100",
817
+ },
818
+ "prediction_method": {
819
+ "matching": "hierarchical: exact weave+blend -> weave fallback -> global fallback",
820
+ "central_tendency": "median",
821
+ "scenario_bands": "p25, p50, p75",
822
+ "quality": "CV + match count based confidence",
823
+ },
824
+ }
825
+
826
+ def get_data_source_audit(self) -> dict:
827
+ df = self._ensure()
828
+ required_cols = [
829
+ "WEAVE",
830
+ "Blend Ratio",
831
+ "Greige EPI",
832
+ "Greige PPI",
833
+ "FINISH EPI",
834
+ "FINISH PPI",
835
+ "FINISH GSM",
836
+ "FINISH WIDTH",
837
+ "Reed Count",
838
+ "Ends per dent",
839
+ "Reed space",
840
+ ]
841
+ coverage = {}
842
+ for c in required_cols:
843
+ if c in df.columns:
844
+ coverage[c] = {
845
+ "null_pct": round(float(df[c].isna().mean() * 100), 3),
846
+ "non_null_rows": int(df[c].notna().sum()),
847
+ }
848
+
849
+ return {
850
+ "source_files": {
851
+ "working_24_jan": WORKING_PATH,
852
+ "piece_dyed_article_details": PIECE_PATH,
853
+ },
854
+ "row_counts": {
855
+ "raw_working": self.raw_working_rows,
856
+ "raw_piece_dyed": self.raw_piece_rows,
857
+ "raw_total": self.raw_working_rows + self.raw_piece_rows,
858
+ "used_working": self.used_working_rows,
859
+ "used_piece_dyed": self.used_piece_rows,
860
+ "used_total": int(len(df)),
861
+ },
862
+ "dataset_distribution": {
863
+ "working": int((df["dataset"] == "working").sum()),
864
+ "piece_dyed": int((df["dataset"] == "piece_dyed").sum()),
865
+ },
866
+ "column_coverage": coverage,
867
+ }
868
+
869
+
870
+ def clear_cache(self):
871
+ self.get_dashboard_summary.cache_clear()
872
+ self.get_filters.cache_clear()
873
+ self.get_relativity_analytics.cache_clear()
874
+ self.get_validation_report.cache_clear()
875
+
876
+ data_service = DataService()
backend/requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi==0.135.1
2
+ uvicorn==0.41.0
3
+ pandas==3.0.1
4
+ numpy==2.4.2
5
+ openpyxl==3.1.5
6
+ python-multipart==0.0.22
7
+ pytest==8.4.0
backend/test_perf.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ from app.services.data_service import data_service
3
+
4
+ start = time.time()
5
+ data_service.get_validation_report(sample_size=100)
6
+ print(f"Validation time 1: {time.time() - start:.2f}s")
7
+
8
+ start = time.time()
9
+ data_service.get_validation_report(sample_size=100)
10
+ print(f"Validation time 2: {time.time() - start:.2f}s")
backend/tests/__init__.py ADDED
File without changes
backend/tests/test_calculations.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ from app.services.data_service import DataService
4
+
5
+
6
+ def test_formula_mappings() -> None:
7
+ ds = DataService()
8
+ ds.load_data()
9
+ row = ds.df.dropna(subset=["Greige EPI", "FINISH EPI"]).iloc[0]
10
+ expected = ((row["FINISH EPI"] - row["Greige EPI"]) / row["Greige EPI"]) * 100
11
+ assert math.isfinite(expected)
12
+ assert abs(expected - row["epi_change_pct"]) < 1e-9
13
+
14
+
15
+ def test_prediction_returns_scenarios() -> None:
16
+ ds = DataService()
17
+ ds.load_data()
18
+ seed = ds.df.iloc[0]
19
+ payload = {
20
+ "weave": seed["weave"],
21
+ "blend": seed["blend"],
22
+ "finish_epi": float(seed["FINISH EPI"])
23
+ if seed["FINISH EPI"] == seed["FINISH EPI"]
24
+ else None,
25
+ "finish_ppi": float(seed["FINISH PPI"])
26
+ if seed["FINISH PPI"] == seed["FINISH PPI"]
27
+ else None,
28
+ "target_gsm": float(seed["FINISH GSM"])
29
+ if seed["FINISH GSM"] == seed["FINISH GSM"]
30
+ else None,
31
+ }
32
+ res = ds.predict_construction(payload)
33
+ assert "recommendation" in res
34
+ assert "scenarios" in res
35
+ assert "data_quality" in res
36
+ assert res["data_quality"]["matches_analyzed"] > 0
backend/tests/test_data_source_audit.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.services.data_service import DataService
2
+
3
+
4
+ def test_data_source_audit_has_expected_counts() -> None:
5
+ ds = DataService()
6
+ ds.load_data()
7
+ audit = ds.get_data_source_audit()
8
+
9
+ assert "row_counts" in audit
10
+ rc = audit["row_counts"]
11
+ assert rc["raw_working"] > 9000
12
+ assert rc["raw_piece_dyed"] > 1500
13
+ assert rc["used_total"] > 10000
14
+
15
+ assert "column_coverage" in audit
16
+ cov = audit["column_coverage"]
17
+ assert "Greige EPI" in cov
18
+ assert "FINISH EPI" in cov
19
+ assert cov["Greige EPI"]["non_null_rows"] > 9000
backend/tests/test_formula_integrity.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ from app.services.data_service import DataService
4
+
5
+
6
+ def _close(a: float, b: float, tol: float = 1e-9) -> bool:
7
+ return abs(a - b) <= tol
8
+
9
+
10
+ def test_change_formulas_match_manual_calculation() -> None:
11
+ ds = DataService()
12
+ ds.load_data()
13
+ assert ds.df is not None
14
+
15
+ epi_row = ds.df.dropna(subset=["Greige EPI", "FINISH EPI", "epi_change_pct"]).iloc[
16
+ 0
17
+ ]
18
+ epi_expected = (
19
+ (float(epi_row["FINISH EPI"]) - float(epi_row["Greige EPI"]))
20
+ / float(epi_row["Greige EPI"])
21
+ ) * 100
22
+ assert _close(epi_expected, float(epi_row["epi_change_pct"]))
23
+
24
+ ppi_row = ds.df.dropna(subset=["Greige PPI", "FINISH PPI", "ppi_change_pct"]).iloc[
25
+ 0
26
+ ]
27
+ ppi_expected = (
28
+ (float(ppi_row["FINISH PPI"]) - float(ppi_row["Greige PPI"]))
29
+ / float(ppi_row["Greige PPI"])
30
+ ) * 100
31
+ assert _close(ppi_expected, float(ppi_row["ppi_change_pct"]))
32
+
33
+ width_row = ds.df.dropna(
34
+ subset=["Greige Width in INCH", "FINISH WIDTH", "width_change_pct"]
35
+ ).iloc[0]
36
+ width_expected = (
37
+ (float(width_row["Greige Width in INCH"]) - float(width_row["FINISH WIDTH"]))
38
+ / float(width_row["Greige Width in INCH"])
39
+ ) * 100
40
+ assert _close(width_expected, float(width_row["width_change_pct"]))
41
+
42
+
43
+ def test_prediction_scenarios_are_ordered() -> None:
44
+ ds = DataService()
45
+ ds.load_data()
46
+ assert ds.df is not None
47
+ row = ds.df.iloc[0]
48
+ result = ds.predict_construction(
49
+ {
50
+ "weave": row["weave"],
51
+ "blend": row["blend"],
52
+ "finish_epi": float(row["FINISH EPI"])
53
+ if math.isfinite(float(row["FINISH EPI"]))
54
+ else None,
55
+ "finish_ppi": float(row["FINISH PPI"])
56
+ if math.isfinite(float(row["FINISH PPI"]))
57
+ else None,
58
+ "target_gsm": float(row["FINISH GSM"])
59
+ if math.isfinite(float(row["FINISH GSM"]))
60
+ else None,
61
+ }
62
+ )
63
+
64
+ for key in ["greige_epi", "greige_ppi", "finish_epi", "target_gsm", "finish_width"]:
65
+ o = result["scenarios"]["optimistic"].get(key)
66
+ e = result["scenarios"]["expected"].get(key)
67
+ p = result["scenarios"]["pessimistic"].get(key)
68
+ if o is None or e is None or p is None:
69
+ continue
70
+ assert o <= e <= p
backend/tests/test_prediction_validation.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.services.data_service import DataService
2
+
3
+
4
+ def test_prediction_basic_holdout_mae() -> None:
5
+ ds = DataService()
6
+ ds.load_data()
7
+ assert ds.df is not None
8
+
9
+ df = ds.df.dropna(
10
+ subset=[
11
+ "weave",
12
+ "blend",
13
+ "Greige EPI",
14
+ "Greige PPI",
15
+ "FINISH EPI",
16
+ "FINISH PPI",
17
+ ]
18
+ ).copy()
19
+ assert len(df) > 500
20
+
21
+ sample = df.sample(n=min(120, len(df)), random_state=42)
22
+
23
+ abs_epi_errors = []
24
+ abs_ppi_errors = []
25
+ valid = 0
26
+
27
+ for _, row in sample.iterrows():
28
+ payload = {
29
+ "weave": row["weave"],
30
+ "blend": row["blend"],
31
+ "finish_epi": float(row["FINISH EPI"]),
32
+ "finish_ppi": float(row["FINISH PPI"]),
33
+ "target_gsm": float(row["FINISH GSM"])
34
+ if row["FINISH GSM"] == row["FINISH GSM"]
35
+ else None,
36
+ }
37
+ out = ds.predict_construction(payload)
38
+ rec = out.get("recommendation", {})
39
+
40
+ if rec.get("greige_epi") is None or rec.get("greige_ppi") is None:
41
+ continue
42
+
43
+ valid += 1
44
+ abs_epi_errors.append(abs(float(rec["greige_epi"]) - float(row["Greige EPI"])))
45
+ abs_ppi_errors.append(abs(float(rec["greige_ppi"]) - float(row["Greige PPI"])))
46
+
47
+ assert valid >= 80
48
+
49
+ mae_epi = sum(abs_epi_errors) / len(abs_epi_errors)
50
+ mae_ppi = sum(abs_ppi_errors) / len(abs_ppi_errors)
51
+
52
+ # Broad but meaningful bounds for MVP-level robust estimator
53
+ assert mae_epi < 30
54
+ assert mae_ppi < 12
55
+
56
+
57
+ def test_validation_report_endpoint_logic() -> None:
58
+ ds = DataService()
59
+ ds.load_data()
60
+ report = ds.get_validation_report(sample_size=120, seed=9)
61
+ assert "mae" in report
62
+ assert report["scored_rows"] >= 80
63
+ assert report["mae"]["greige_epi"] < 35
64
+ assert report["mae"]["greige_ppi"] < 15
data/Input & Out Put.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8b71a764d4c14209ba3c8eff8011580736a048b176bb465284a593ccfdd7611a
3
+ size 9417
data/Piece Dyed Artcle details.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d95d9af054823e797a618008199dfc2ebb184118cb95bdab29913e779ac792cd
3
+ size 300302
data/Working 24 Jan.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:98c52c8631ef9a73f9d3487e7650ce702ee21d2da9223bc5d1312bc4cf64e2a6
3
+ size 1746811
frontend/.gitignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ .next
3
+ dist
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ .env
frontend/app/globals.css ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 210 20% 98%;
8
+ --foreground: 222.2 84% 4.9%;
9
+
10
+ --card: 0 0% 100%;
11
+ --card-foreground: 222.2 84% 4.9%;
12
+
13
+ --popover: 0 0% 100%;
14
+ --popover-foreground: 222.2 84% 4.9%;
15
+
16
+ --primary: 221.2 83.2% 53.3%;
17
+ --primary-foreground: 210 40% 98%;
18
+
19
+ --secondary: 210 40% 96.1%;
20
+ --secondary-foreground: 222.2 47.4% 11.2%;
21
+
22
+ --muted: 210 40% 96.1%;
23
+ --muted-foreground: 215.4 16.3% 46.9%;
24
+
25
+ --accent: 210 40% 96.1%;
26
+ --accent-foreground: 222.2 47.4% 11.2%;
27
+
28
+ --destructive: 0 84.2% 60.2%;
29
+ --destructive-foreground: 210 40% 98%;
30
+
31
+ --border: 214.3 31.8% 91.4%;
32
+ --input: 214.3 31.8% 91.4%;
33
+ --ring: 221.2 83.2% 53.3%;
34
+
35
+ --radius: 0.5rem;
36
+ }
37
+
38
+ .dark {
39
+ --background: 222.2 84% 4.9%;
40
+ --foreground: 210 40% 98%;
41
+
42
+ --card: 222.2 84% 4.9%;
43
+ --card-foreground: 210 40% 98%;
44
+
45
+ --popover: 222.2 84% 4.9%;
46
+ --popover-foreground: 210 40% 98%;
47
+
48
+ --primary: 217.2 91.2% 59.8%;
49
+ --primary-foreground: 222.2 47.4% 11.2%;
50
+
51
+ --secondary: 217.2 32.6% 17.5%;
52
+ --secondary-foreground: 210 40% 98%;
53
+
54
+ --muted: 217.2 32.6% 17.5%;
55
+ --muted-foreground: 215 20.2% 65.1%;
56
+
57
+ --accent: 217.2 32.6% 17.5%;
58
+ --accent-foreground: 210 40% 98%;
59
+
60
+ --destructive: 0 62.8% 30.6%;
61
+ --destructive-foreground: 210 40% 98%;
62
+
63
+ --border: 217.2 32.6% 17.5%;
64
+ --input: 217.2 32.6% 17.5%;
65
+ --ring: 212.7 26.8% 83.9%;
66
+ }
67
+ }
68
+
69
+ @layer base {
70
+ * {
71
+ @apply border-border;
72
+ }
73
+ body {
74
+ @apply bg-background text-foreground;
75
+ }
76
+ }
frontend/app/layout.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "PDC Intelligence Dashboard",
6
+ description: "Process development and construction intelligence prototype",
7
+ };
8
+
9
+ export default function RootLayout({
10
+ children,
11
+ }: Readonly<{
12
+ children: React.ReactNode;
13
+ }>) {
14
+ return (
15
+ <html lang="en">
16
+ <body>{children}</body>
17
+ </html>
18
+ );
19
+ }
frontend/app/page.tsx ADDED
@@ -0,0 +1,991 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import axios from "axios";
5
+ import {
6
+ LayoutDashboard,
7
+ Search,
8
+ Calculator,
9
+ BookOpen,
10
+ Box,
11
+ Settings2,
12
+ TrendingUp,
13
+ Activity,
14
+ Search as SearchIcon,
15
+ ChevronRight,
16
+ RefreshCcw,
17
+ Cpu,
18
+ Percent,
19
+ Info,
20
+ } from "lucide-react";
21
+ import {
22
+ BarChart,
23
+ Bar,
24
+ XAxis,
25
+ YAxis,
26
+ CartesianGrid,
27
+ Tooltip,
28
+ ResponsiveContainer,
29
+ } from "recharts";
30
+ import { cn } from "../lib/utils";
31
+
32
+ const API = "/api";
33
+
34
+ type Tab = "dashboard" | "articles" | "calculator" | "docs";
35
+
36
+ function TooltipItem({ label, text }: { label: string; text: string }) {
37
+ const [show, setShow] = useState(false);
38
+ const [pos, setPos] = useState({ top: 0, left: 0 });
39
+
40
+ const handleMouseEnter = (e: React.MouseEvent) => {
41
+ const rect = e.currentTarget.getBoundingClientRect();
42
+ setPos({ top: rect.top, left: rect.left });
43
+ setShow(true);
44
+ };
45
+
46
+ return (
47
+ <span className="relative inline-flex items-center ml-1 align-middle">
48
+ <span
49
+ className="cursor-help text-slate-400 hover:text-indigo-600"
50
+ onMouseEnter={handleMouseEnter}
51
+ onMouseLeave={() => setShow(false)}
52
+ onClick={(e) => {
53
+ const rect = e.currentTarget.getBoundingClientRect();
54
+ setPos({ top: rect.top, left: rect.left });
55
+ setShow(!show);
56
+ }}
57
+ >
58
+ <Info size={14} />
59
+ </span>
60
+ {show && (
61
+ <div
62
+ className="fixed z-[9999] p-2 bg-slate-800 text-white text-xs rounded-lg shadow-xl border border-slate-700 whitespace-normal w-64"
63
+ style={{
64
+ top: pos.top - 10,
65
+ left: pos.left + 20
66
+ }}
67
+ >
68
+ <span className="block text-indigo-300 font-semibold mb-0.5">{label}</span>
69
+ <span>{text}</span>
70
+ </div>
71
+ )}
72
+ </span>
73
+ );
74
+ }
75
+
76
+ export default function HomePage() {
77
+ const [tab, setTab] = useState<Tab>("dashboard");
78
+ const [dashboard, setDashboard] = useState<any>(null);
79
+ const [articles, setArticles] = useState<any>(null);
80
+ const [analytics, setAnalytics] = useState<any>(null);
81
+ const [filters, setFilters] = useState<any>(null);
82
+ const [validation, setValidation] = useState<any>(null);
83
+
84
+ const [articleQuery, setArticleQuery] = useState({
85
+ page: 1,
86
+ limit: 25,
87
+ search: "",
88
+ weave: "",
89
+ blend: "",
90
+ loom_type: "",
91
+ count_band: "",
92
+ });
93
+
94
+ const [predictionInput, setPredictionInput] = useState<any>({
95
+ weave: "",
96
+ blend: "",
97
+ warp_count: "",
98
+ weft_count: "",
99
+ finish_epi: "",
100
+ finish_ppi: "",
101
+ target_gsm: "",
102
+ dataset: "all",
103
+ loom_type: "",
104
+ });
105
+ const [prediction, setPrediction] = useState<any>(null);
106
+ const [predicting, setPredicting] = useState(false);
107
+
108
+ useEffect(() => {
109
+ loadBase();
110
+ loadArticles();
111
+ }, []);
112
+
113
+ const loadBase = async () => {
114
+ try {
115
+ const [d, f, a, v] = await Promise.all([
116
+ axios.get(`${API}/dashboard`),
117
+ axios.get(`${API}/filters`),
118
+ axios.get(`${API}/analytics/relativity`),
119
+ axios.get(`${API}/validation/report?sample_size=100`),
120
+ ]);
121
+ setDashboard(d.data);
122
+ setFilters(f.data);
123
+ setAnalytics(a.data);
124
+ setValidation(v.data);
125
+ } catch (e) {
126
+ console.error(e);
127
+ }
128
+ };
129
+
130
+ const loadArticles = async () => {
131
+ try {
132
+ const qs = new URLSearchParams(articleQuery as any).toString();
133
+ const res = await axios.get(`${API}/articles?${qs}`);
134
+ setArticles(res.data);
135
+ } catch (e) {
136
+ console.error(e);
137
+ }
138
+ };
139
+
140
+ useEffect(() => {
141
+ loadArticles();
142
+ }, [articleQuery]);
143
+
144
+ const handlePredict = async () => {
145
+ setPredicting(true);
146
+ try {
147
+ const res = await axios.post(`${API}/predictions/construction`, predictionInput);
148
+ setPrediction(res.data);
149
+ } catch (e) {
150
+ console.error(e);
151
+ alert("Error generating prediction. Check inputs.");
152
+ }
153
+ setPredicting(false);
154
+ };
155
+
156
+ return (
157
+ <div className="flex h-screen bg-slate-50 font-sans text-slate-900 overflow-hidden">
158
+ <aside className="w-64 bg-slate-900 text-slate-300 flex flex-col transition-all">
159
+ <div className="p-6 flex items-center space-x-3 text-white">
160
+ <Box className="w-8 h-8 text-indigo-400" />
161
+ <h1 className="text-xl font-bold tracking-tight">PDC Intel</h1>
162
+ </div>
163
+
164
+ <nav className="flex-1 px-4 space-y-2 mt-4">
165
+ <NavItem
166
+ active={tab === "dashboard"}
167
+ onClick={() => setTab("dashboard")}
168
+ icon={<LayoutDashboard size={20} />}
169
+ label="Dashboard"
170
+ />
171
+ <NavItem
172
+ active={tab === "calculator"}
173
+ onClick={() => setTab("calculator")}
174
+ icon={<Calculator size={20} />}
175
+ label="Prediction Studio"
176
+ />
177
+ <NavItem
178
+ active={tab === "articles"}
179
+ onClick={() => setTab("articles")}
180
+ icon={<Search size={20} />}
181
+ label="Article Archive"
182
+ />
183
+ <NavItem
184
+ active={tab === "docs"}
185
+ onClick={() => setTab("docs")}
186
+ icon={<BookOpen size={20} />}
187
+ label="PDC Docs"
188
+ />
189
+ </nav>
190
+
191
+ <div className="p-4 border-t border-slate-800 text-xs text-slate-500">
192
+ <p>Phase 1 Prototype</p>
193
+ <p>Vardhman Textiles</p>
194
+ </div>
195
+ </aside>
196
+
197
+ <main className="flex-1 overflow-y-auto bg-slate-50">
198
+ <header className="bg-white border-b border-slate-200 px-8 py-5 flex items-center justify-between sticky top-0 z-10">
199
+ <h2 className="text-2xl font-semibold text-slate-800 capitalize">
200
+ {tab === "calculator"
201
+ ? "Prediction Studio"
202
+ : tab.replace("-", " ")}
203
+ </h2>
204
+ <div className="flex items-center space-x-4 text-sm">
205
+ <span className="flex items-center text-emerald-600 bg-emerald-50 px-3 py-1 rounded-full border border-emerald-200">
206
+ <Activity size={16} className="mr-2" /> API Online
207
+ </span>
208
+ </div>
209
+ </header>
210
+
211
+ <div className="p-8 max-w-7xl mx-auto space-y-8">
212
+ {tab === "dashboard" && dashboard && (
213
+ <div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
214
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
215
+ <StatCard
216
+ title="Total Articles"
217
+ value={dashboard.totals.unique_articles.toLocaleString()}
218
+ icon={<Box className="text-blue-500" />}
219
+ tooltip="Total unique master articles in the historical database. Represents all fabric designs documented."
220
+ />
221
+ <StatCard
222
+ title="Unique Weaves"
223
+ value={dashboard.totals.unique_weaves}
224
+ icon={<Settings2 className="text-purple-500" />}
225
+ tooltip="Distinct weave patterns in database (Plain, Twill, Satin, Dobby, etc). Weave determines fabric texture."
226
+ />
227
+ {validation && (
228
+ <StatCard
229
+ title="Greige EPI MAE"
230
+ value={validation.mae?.greige_epi?.toFixed(2) || "N/A"}
231
+ icon={<TrendingUp className="text-emerald-500" />}
232
+ subtitle="Mean Absolute Error"
233
+ tooltip="Mean Absolute Error - Average prediction error in EPI units. Lower is better. 10.8 means predictions are off by ~11 EPI on average."
234
+ />
235
+ )}
236
+ </div>
237
+
238
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
239
+ <div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
240
+ <h3 className="font-semibold text-lg mb-4 text-slate-800">Top Weaves</h3>
241
+ <div className="h-64">
242
+ <ResponsiveContainer width="100%" height="100%">
243
+ <BarChart
244
+ data={dashboard.top_weaves.map((w: any) => ({
245
+ name: w.weave,
246
+ count: w.count,
247
+ }))}
248
+ >
249
+ <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
250
+ <XAxis
251
+ dataKey="name"
252
+ axisLine={false}
253
+ tickLine={false}
254
+ tick={{ fill: "#64748b", fontSize: 12 }}
255
+ />
256
+ <YAxis
257
+ axisLine={false}
258
+ tickLine={false}
259
+ tick={{ fill: "#64748b", fontSize: 12 }}
260
+ />
261
+ <Tooltip
262
+ cursor={{ fill: "#f1f5f9" }}
263
+ contentStyle={{
264
+ borderRadius: "8px",
265
+ border: "none",
266
+ boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)",
267
+ }}
268
+ />
269
+ <Bar
270
+ dataKey="count"
271
+ fill="#6366f1"
272
+ radius={[4, 4, 0, 0]}
273
+ />
274
+ </BarChart>
275
+ </ResponsiveContainer>
276
+ </div>
277
+ </div>
278
+
279
+ <div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
280
+ <h3 className="font-semibold text-lg mb-4 text-slate-800">Top Blends</h3>
281
+ <div className="space-y-4">
282
+ {dashboard.top_blends.map((b: any, i: number) => (
283
+ <div key={i} className="flex items-center justify-between">
284
+ <span className="text-slate-600 font-medium">{b.blend}</span>
285
+ <div className="flex items-center">
286
+ <div className="w-32 h-2 bg-slate-100 rounded-full mr-3 overflow-hidden">
287
+ <div
288
+ className="h-full bg-indigo-500 rounded-full"
289
+ style={{
290
+ width: `${(b.count / dashboard.totals.unique_articles) * 100}%`,
291
+ }}
292
+ />
293
+ </div>
294
+ <span className="text-slate-900 font-semibold w-12 text-right">
295
+ {b.count}
296
+ </span>
297
+ </div>
298
+ </div>
299
+ ))}
300
+ </div>
301
+ </div>
302
+ </div>
303
+ </div>
304
+ )}
305
+
306
+ {tab === "calculator" && filters && (
307
+ <div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
308
+ <div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
309
+ <div className="bg-slate-50 border-b border-slate-200 p-6">
310
+ <h3 className="text-lg font-semibold text-slate-800 mb-1">
311
+ Target Finished Parameters
312
+ </h3>
313
+ <p className="text-sm text-slate-500">
314
+ Enter desired finish specifications to predict optimal greige construction.
315
+ </p>
316
+ </div>
317
+
318
+ <div className="p-6">
319
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
320
+ <SelectField
321
+ label="Weave"
322
+ value={predictionInput.weave}
323
+ onChange={(v: string) =>
324
+ setPredictionInput({ ...predictionInput, weave: v })
325
+ }
326
+ options={filters.weaves}
327
+ required
328
+ tooltip="The interlacing pattern of threads (Plain, Twill, Satin). Determines fabric texture and appearance."
329
+ />
330
+ <SelectField
331
+ label="Blend"
332
+ value={predictionInput.blend}
333
+ onChange={(v: string) =>
334
+ setPredictionInput({ ...predictionInput, blend: v })
335
+ }
336
+ options={filters.blends}
337
+ required
338
+ tooltip="Yarn composition. 100%CO = 100% Cotton, 65/35 = 65% Polyester 35% Cotton. Determines fabric feel and properties."
339
+ />
340
+ <SelectField
341
+ label="Dataset Focus"
342
+ value={predictionInput.dataset}
343
+ onChange={(v: string) =>
344
+ setPredictionInput({ ...predictionInput, dataset: v })
345
+ }
346
+ options={["all", "working", "piece_dyed"]}
347
+ tooltip="Working = General catalog fabrics. Piece Dyed = Fabrics dyed after weaving (for special colors/patterns)."
348
+ />
349
+ <SelectField
350
+ label="Loom Type"
351
+ value={predictionInput.loom_type}
352
+ onChange={(v: string) =>
353
+ setPredictionInput({ ...predictionInput, loom_type: v })
354
+ }
355
+ options={filters.loom_types}
356
+ tooltip="Type of weaving machine (Airjet, Rapier, etc). Affects fabric quality and cost."
357
+ />
358
+
359
+ <InputField
360
+ label="Warp Count"
361
+ type="number"
362
+ value={predictionInput.warp_count}
363
+ onChange={(e: any) =>
364
+ setPredictionInput({ ...predictionInput, warp_count: e.target.value })
365
+ }
366
+ placeholder="e.g. 40"
367
+ tooltip="Yarn thickness for lengthwise threads. Higher number = thinner yarn (60s is thinner than 40s). Also called V-Count."
368
+ />
369
+ <InputField
370
+ label="Weft Count"
371
+ type="number"
372
+ value={predictionInput.weft_count}
373
+ onChange={(e: any) =>
374
+ setPredictionInput({ ...predictionInput, weft_count: e.target.value })
375
+ }
376
+ placeholder="e.g. 40"
377
+ tooltip="Yarn thickness for widthwise threads. Higher number = thinner yarn. Also called Weft/V-Count."
378
+ />
379
+ <InputField
380
+ label="Finish EPI"
381
+ type="number"
382
+ value={predictionInput.finish_epi}
383
+ onChange={(e: any) =>
384
+ setPredictionInput({ ...predictionInput, finish_epi: e.target.value })
385
+ }
386
+ placeholder="e.g. 150"
387
+ tooltip="Ends Per Inch in finished fabric. Number of lengthwise threads per inch. Higher = tighter/denser fabric."
388
+ />
389
+ <InputField
390
+ label="Finish PPI"
391
+ type="number"
392
+ value={predictionInput.finish_ppi}
393
+ onChange={(e: any) =>
394
+ setPredictionInput({ ...predictionInput, finish_ppi: e.target.value })
395
+ }
396
+ placeholder="e.g. 80"
397
+ tooltip="Picks Per Inch in finished fabric. Number of widthwise threads per inch. Higher = denser weave."
398
+ />
399
+ <InputField
400
+ label="Target GSM"
401
+ type="number"
402
+ value={predictionInput.target_gsm}
403
+ onChange={(e: any) =>
404
+ setPredictionInput({ ...predictionInput, target_gsm: e.target.value })
405
+ }
406
+ placeholder="e.g. 120"
407
+ tooltip="Grams per Square Meter. Fabric weight. HigherGSM = heavier/thicker fabric. Standard shirt is ~120 GSM."
408
+ />
409
+ </div>
410
+
411
+ <div className="mt-8 flex justify-end">
412
+ <button
413
+ onClick={handlePredict}
414
+ disabled={predicting || !predictionInput.weave || !predictionInput.blend}
415
+ className="bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed text-white px-6 py-2.5 rounded-lg font-medium shadow-sm transition-colors flex items-center"
416
+ >
417
+ {predicting ? (
418
+ <RefreshCcw className="animate-spin mr-2" size={18} />
419
+ ) : (
420
+ <Cpu className="mr-2" size={18} />
421
+ )}
422
+ Generate Construction
423
+ </button>
424
+ </div>
425
+ </div>
426
+ </div>
427
+
428
+ {prediction && !prediction.error && (
429
+ <div className="space-y-6">
430
+ <div className="flex items-center justify-between">
431
+ <h3 className="text-xl font-semibold text-slate-800">
432
+ Recommended Greige Construction
433
+ </h3>
434
+ <div className="flex items-center space-x-2 text-sm">
435
+ <span className="text-slate-500">Data Source:</span>
436
+ <span className="font-medium bg-slate-200 text-slate-800 px-2 py-1 rounded">
437
+ {prediction.data_quality?.matching_source}
438
+ </span>
439
+ <span className="text-slate-500 ml-2">Sample Size:</span>
440
+ <span className="font-medium bg-indigo-100 text-indigo-800 px-2 py-1 rounded">
441
+ {prediction.data_quality?.matches_analyzed}
442
+ </span>
443
+ </div>
444
+ </div>
445
+
446
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
447
+ <ResultCard
448
+ title="Greige EPI"
449
+ field={prediction.statistics?.greige_epi}
450
+ tooltip="Recommended ends per inch on loom. Higher than finish EPI due to shrinkage during processing."
451
+ />
452
+ <ResultCard
453
+ title="Greige PPI"
454
+ field={prediction.statistics?.greige_ppi}
455
+ tooltip="Recommended picks per inch on loom. Widthwise thread density before finishing."
456
+ />
457
+ <ResultCard
458
+ title="Reed Space"
459
+ field={prediction.statistics?.reed_space}
460
+ tooltip="Fabric width on loom in inches. Standard is 44, 48, 50, 60 inches. Determines finished fabric width."
461
+ />
462
+ <ResultCard
463
+ title="Reed Count"
464
+ field={prediction.statistics?.reed_count}
465
+ tooltip="Reed instrument size. Number of dents per inch. Determines EPI and fabric density."
466
+ />
467
+ <ResultCard
468
+ title="Ends per Dent"
469
+ field={prediction.statistics?.ends_per_dent}
470
+ tooltip="Threads per reed slot. More threads = higher EPI. Standard is 2-4 ends per dent."
471
+ />
472
+ </div>
473
+
474
+ <div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden mt-8">
475
+ <div className="bg-slate-50 border-b border-slate-200 p-4 px-6">
476
+ <h4 className="font-semibold text-slate-800">
477
+ Nearest Historical Neighbors (Top 5)
478
+ </h4>
479
+ </div>
480
+ <div className="overflow-x-auto">
481
+ <table className="w-full text-sm text-left">
482
+ <thead className="bg-slate-50 text-slate-500 font-medium">
483
+ <tr>
484
+ <th className="px-6 py-3">
485
+ Master Article
486
+ <TooltipItem label="Master Article" text="The primary design reference number for this fabric construction." />
487
+ </th>
488
+ <th className="px-6 py-3">
489
+ Weave
490
+ <TooltipItem label="Weave" text="The interlacing pattern (Plain, Twill, Satin) of this historical match." />
491
+ </th>
492
+ <th className="px-6 py-3">
493
+ Fin EPIxPPI
494
+ <TooltipItem label="Finish EPI x PPI" text="Ends per inch and Picks per inch in the finished state for this article." />
495
+ </th>
496
+ <th className="px-6 py-3">
497
+ Grg EPIxPPI
498
+ <TooltipItem label="Greige EPI x PPI" text="Actual recorded Greige construction on the loom for this historical article." />
499
+ </th>
500
+ <th className="px-6 py-3">
501
+ Reed x Space
502
+ <TooltipItem label="Reed x Space" text="The Reed Count (dents/inch) and Reed Space (width on loom) used for this article." />
503
+ </th>
504
+ <th className="px-6 py-3">
505
+ Distance Score
506
+ <TooltipItem label="Distance Score" text="Match quality: Lower is better. 0.00 is a perfect match of inputs." />
507
+ </th>
508
+ </tr>
509
+ </thead>
510
+ <tbody className="divide-y divide-slate-100">
511
+ {prediction.reference_articles
512
+ ?.slice(0, 5)
513
+ .map((n: any, i: number) => (
514
+ <tr key={i} className="hover:bg-slate-50">
515
+ <td className="px-6 py-4 font-medium text-indigo-600">
516
+ {n.master_article}
517
+ </td>
518
+ <td className="px-6 py-4">{n.weave}</td>
519
+ <td className="px-6 py-4">
520
+ {n["FINISH EPI"]?.toFixed(1) || "-"} x{" "}
521
+ {n["FINISH PPI"] || "-"}
522
+ </td>
523
+ <td className="px-6 py-4 font-semibold">
524
+ {n["Greige EPI"] || "-"} x {n["Greige PPI"] || "-"}
525
+ </td>
526
+ <td className="px-6 py-4">
527
+ {n["Reed Count"] || "-"} x {n["Reed space"] || "-"}
528
+ </td>
529
+ <td className="px-6 py-4">
530
+ <span className="bg-slate-100 text-slate-600 px-2 py-1 rounded text-xs">
531
+ {n.score?.toFixed(2)}
532
+ </span>
533
+ </td>
534
+ </tr>
535
+ ))}
536
+ </tbody>
537
+ </table>
538
+ </div>
539
+ </div>
540
+ </div>
541
+ )}
542
+ {prediction?.error && (
543
+ <div className="bg-red-50 text-red-600 p-4 rounded-lg border border-red-200">
544
+ {prediction.error}
545
+ </div>
546
+ )}
547
+ </div>
548
+ )}
549
+
550
+ {tab === "articles" && articles && filters && (
551
+ <div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden animate-in fade-in duration-500">
552
+ <div className="p-4 border-b border-slate-200 flex flex-wrap gap-4 items-center bg-slate-50">
553
+ <div className="relative flex-1 min-w-[200px]">
554
+ <SearchIcon
555
+ className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"
556
+ size={18}
557
+ />
558
+ <input
559
+ type="text"
560
+ placeholder="Search articles..."
561
+ className="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-sm"
562
+ value={articleQuery.search}
563
+ onChange={(e) =>
564
+ setArticleQuery({ ...articleQuery, search: e.target.value, page: 1 })
565
+ }
566
+ />
567
+ </div>
568
+ <select
569
+ className="border border-slate-200 rounded-lg px-3 py-2 text-sm bg-white"
570
+ value={articleQuery.weave}
571
+ onChange={(e) =>
572
+ setArticleQuery({ ...articleQuery, weave: e.target.value, page: 1 })
573
+ }
574
+ >
575
+ <option value="">All Weaves</option>
576
+ {filters.weaves?.map((w: string) => (
577
+ <option key={w} value={w}>
578
+ {w}
579
+ </option>
580
+ ))}
581
+ </select>
582
+ <select
583
+ className="border border-slate-200 rounded-lg px-3 py-2 text-sm bg-white"
584
+ value={articleQuery.blend}
585
+ onChange={(e) =>
586
+ setArticleQuery({ ...articleQuery, blend: e.target.value, page: 1 })
587
+ }
588
+ >
589
+ <option value="">All Blends</option>
590
+ {filters.blends?.map((b: string) => (
591
+ <option key={b} value={b}>
592
+ {b}
593
+ </option>
594
+ ))}
595
+ </select>
596
+ </div>
597
+
598
+ <div className="overflow-x-auto">
599
+ <table className="w-full text-sm text-left">
600
+ <thead className="bg-slate-50 text-slate-500 font-medium border-b border-slate-200">
601
+ <tr>
602
+ <th className="px-4 py-3">Article</th>
603
+ <th className="px-4 py-3">Weave</th>
604
+ <th className="px-4 py-3">Blend</th>
605
+ <th className="px-4 py-3">Warp/Weft</th>
606
+ <th className="px-4 py-3">Reed</th>
607
+ <th className="px-4 py-3">Fin EPIxPPI</th>
608
+ <th className="px-4 py-3">Grg EPIxPPI</th>
609
+ </tr>
610
+ </thead>
611
+ <tbody className="divide-y divide-slate-100">
612
+ {articles.data?.map((a: any, i: number) => (
613
+ <tr key={i} className="hover:bg-slate-50 cursor-pointer transition-colors">
614
+ <td className="px-4 py-2 font-medium text-indigo-600 text-xs">
615
+ {a.master_article}
616
+ </td>
617
+ <td className="px-4 py-2 text-xs">{a.weave}</td>
618
+ <td className="px-4 py-2 text-xs truncate max-w-[150px]" title={a.blend}>
619
+ {a.blend}
620
+ </td>
621
+ <td className="px-4 py-2 text-xs">
622
+ {a.warp_count || "-"} / {a.weft_count || "-"}
623
+ </td>
624
+ <td className="px-4 py-2 text-xs font-medium">
625
+ {a.reed_count || "-"}
626
+ </td>
627
+ <td className="px-4 py-2 text-xs">
628
+ {a.finish_epi_median?.toFixed(1) || "-"} x{" "}
629
+ {a.finish_ppi_median?.toFixed(1) || "-"}
630
+ </td>
631
+ <td className="px-4 py-2 text-xs font-semibold bg-slate-50/50">
632
+ {a.greige_epi_median?.toFixed(1) || "-"} x{" "}
633
+ {a.greige_ppi_median?.toFixed(1) || "-"}
634
+ </td>
635
+ </tr>
636
+ ))}
637
+ </tbody>
638
+ </table>
639
+ </div>
640
+
641
+ <div className="p-4 border-t border-slate-200 flex items-center justify-between bg-slate-50">
642
+ <span className="text-sm text-slate-500">
643
+ Showing{" "}
644
+ {Math.min(
645
+ (articleQuery.page - 1) * articleQuery.limit + 1,
646
+ articles.total
647
+ )}{" "}
648
+ to {Math.min(articleQuery.page * articleQuery.limit, articles.total)} of{" "}
649
+ {articles.total}
650
+ </span>
651
+ <div className="flex space-x-2">
652
+ <button
653
+ disabled={articleQuery.page === 1}
654
+ onClick={() =>
655
+ setArticleQuery({ ...articleQuery, page: articleQuery.page - 1 })
656
+ }
657
+ className="px-3 py-1 border border-slate-300 rounded bg-white text-sm disabled:opacity-50"
658
+ >
659
+ Prev
660
+ </button>
661
+ <button
662
+ disabled={articleQuery.page >= articles.total_pages}
663
+ onClick={() =>
664
+ setArticleQuery({ ...articleQuery, page: articleQuery.page + 1 })
665
+ }
666
+ className="px-3 py-1 border border-slate-300 rounded bg-white text-sm disabled:opacity-50"
667
+ >
668
+ Next
669
+ </button>
670
+ </div>
671
+ </div>
672
+ </div>
673
+ )}
674
+
675
+ {tab === "docs" && (
676
+ <div className="prose prose-slate max-w-none animate-in fade-in duration-500">
677
+ <div className="bg-white p-8 rounded-xl border border-slate-200 shadow-sm space-y-8">
678
+ <div>
679
+ <h2 className="text-2xl font-bold text-slate-900 mb-4">
680
+ PDC Intelligence Documentation
681
+ </h2>
682
+ <p className="text-slate-600">
683
+ This document explains the algorithms and methodologies used in
684
+ the PDC Intelligence Prediction System for Vardhman Textiles.
685
+ </p>
686
+ </div>
687
+
688
+ <div className="border-l-4 border-indigo-500 pl-6 space-y-8">
689
+ <div>
690
+ <h3 className="text-xl font-semibold text-slate-800 mb-2">
691
+ 1. Dashboard
692
+ </h3>
693
+ <p className="text-slate-600 mb-2">
694
+ The Dashboard provides an overview of the historical database
695
+ used for construction predictions.
696
+ </p>
697
+ <ul className="list-disc pl-5 space-y-1 text-slate-600 text-sm">
698
+ <li>
699
+ <strong>Total Articles:</strong> Unique master articles
700
+ in the system.
701
+ </li>
702
+ <li>
703
+ <strong>Unique Weaves:</strong> Distinct weave patterns
704
+ analyzed.
705
+ </li>
706
+ <li>
707
+ <strong>Greige EPI MAE:</strong> Mean Absolute Error. This
708
+ represents the average difference (in absolute EPI units)
709
+ between the predicted Greige density and the actual
710
+ historical value. Lower is better.
711
+ </li>
712
+ </ul>
713
+ </div>
714
+
715
+ <div>
716
+ <h3 className="text-xl font-semibold text-slate-800 mb-2">
717
+ 2. Prediction Studio (Construction Calculator)
718
+ </h3>
719
+ <p className="text-slate-600 mb-2">
720
+ This module calculates the optimal Greige construction
721
+ parameters to achieve the desired Finished fabric properties.
722
+ </p>
723
+
724
+ <h4 className="font-medium text-slate-700 mt-4 mb-2">
725
+ Input Parameters:
726
+ </h4>
727
+ <ul className="list-disc pl-5 space-y-1 text-slate-600 text-sm">
728
+ <li>
729
+ <strong>Weave:</strong> Pattern (e.g., Plain, Twill, Satin).
730
+ </li>
731
+ <li>
732
+ <strong>Blend:</strong> Yarn composition (e.g., 100%CO,
733
+ 65%PES 35%CO).
734
+ </li>
735
+ <li>
736
+ <strong>Warp/Weft Count:</strong> Yarn linear density
737
+ (V: Value).
738
+ </li>
739
+ <li>
740
+ <strong>Finish EPI/PPI:</strong> Ends/Picks per inch in
741
+ the finished fabric.
742
+ </li>
743
+ <li>
744
+ <strong>Target GSM:</strong> Desired weight per square
745
+ meter.
746
+ </li>
747
+ <li>
748
+ <strong>Dataset:</strong> "Working" (general) or "Piece
749
+ Dyed" (specialized).
750
+ </li>
751
+ </ul>
752
+
753
+ <h4 className="font-medium text-slate-700 mt-4 mb-2">
754
+ Prediction Algorithm:
755
+ </h4>
756
+ <p className="text-slate-600 text-sm mb-2">
757
+ The system uses a hierarchical nearest-neighbor matching
758
+ algorithm:
759
+ </p>
760
+ <ol className="list-decimal pl-5 space-y-1 text-slate-600 text-sm">
761
+ <li>
762
+ <strong>Exact Match:</strong> Filters for articles with
763
+ identical Weave + Blend + Count Band.
764
+ </li>
765
+ <li>
766
+ <strong>Weave Fallback:</strong> If insufficient data,
767
+ relaxes to matching Weave only.
768
+ </li>
769
+ <li>
770
+ <strong>Global Fallback:</strong> Uses entire dataset as
771
+ last resort.
772
+ </li>
773
+ </ol>
774
+
775
+ <h4 className="font-medium text-slate-700 mt-4 mb-2">
776
+ Output Calculation:
777
+ </h4>
778
+ <p className="text-slate-600 text-sm">
779
+ For every matching article, a{" "}
780
+ <em>Distance Score</em> is calculated based on:
781
+ </p>
782
+ <ul className="list-disc pl-5 space-y-1 text-slate-600 text-sm mt-1">
783
+ <li>Exact weave match penalty: +4.0</li>
784
+ <li>Exact blend match penalty: +3.0</li>
785
+ <li>
786
+ Delta-based weighted penalty: |Input - Actual| / Max(Input,
787
+ 1.0) * Weight
788
+ </li>
789
+ </ul>
790
+ <p className="text-slate-600 text-sm mt-2">
791
+ The final recommendations use the <strong>Median</strong> of the top-N
792
+ matches to ensure robustness against outliers.
793
+ </p>
794
+ </div>
795
+
796
+ <div>
797
+ <h3 className="text-xl font-semibold text-slate-800 mb-2">
798
+ 3. Article Archive
799
+ </h3>
800
+ <p className="text-slate-600 mb-2">
801
+ A searchable repository of all historical articles.
802
+ </p>
803
+ <ul className="list-disc pl-5 space-y-1 text-slate-600 text-sm">
804
+ <li>
805
+ <strong>Fuzzy Search:</strong> Matches across Article
806
+ ID, Weave Description, and Blend composition.
807
+ </li>
808
+ <li>
809
+ <strong>Filters:</strong> Narrow down by Weave, Blend, Loom
810
+ type.
811
+ </li>
812
+ <li>
813
+ <strong>Median Aggregation:</strong> For articles with
814
+ multiple variants, values are aggregated using median.
815
+ </li>
816
+ </ul>
817
+ </div>
818
+
819
+ <div>
820
+ <h3 className="text-xl font-semibold text-slate-800 mb-2">
821
+ 4. Key Terms
822
+ </h3>
823
+ <ul className="space-y-2 text-slate-600 text-sm">
824
+ <li>
825
+ <strong>EPI (Ends Per Inch):</strong> Longitudinal thread
826
+ density on loom.
827
+ </li>
828
+ <li>
829
+ <strong>PPI (Picks Per Inch):</strong> Lateral thread
830
+ density on loom.
831
+ </li>
832
+ <li>
833
+ <strong>Reed:</strong> Instrument used to-beat in the
834
+ weft. Reed Count = EPI / Ends per Dent.
835
+ </li>
836
+ <li>
837
+ <strong>GSM:</strong> Grams per Square Meter
838
+ (Fabric Weight).
839
+ </li>
840
+ <li>
841
+ <strong>Count Band:</strong> Grouping for yarn
842
+ thickness (Below 40, 40 & Above).
843
+ </li>
844
+ <li>
845
+ <strong>MAE:</strong> Mean Absolute Error -
846
+ Standard accuracy metric.
847
+ </li>
848
+ <li>
849
+ <strong>Confidence:</strong> Derived from sample
850
+ size (High: 100+, Medium: 50+, Low: 20+, Very
851
+ Low: &lt;20).
852
+ </li>
853
+ </ul>
854
+ </div>
855
+ </div>
856
+ </div>
857
+ </div>
858
+ )}
859
+ </div>
860
+ </main>
861
+ </div>
862
+ );
863
+ }
864
+
865
+ function NavItem({ active, onClick, icon, label }: any) {
866
+ return (
867
+ <button
868
+ onClick={onClick}
869
+ className={cn(
870
+ "w-full flex items-center space-x-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors",
871
+ active
872
+ ? "bg-indigo-600 text-white shadow-md shadow-indigo-900/20"
873
+ : "text-slate-400 hover:bg-slate-800 hover:text-slate-200"
874
+ )}
875
+ >
876
+ {icon}
877
+ <span>{label}</span>
878
+ {active && <ChevronRight size={16} className="ml-auto opacity-50" />}
879
+ </button>
880
+ );
881
+ }
882
+
883
+ function StatCard({ title, value, icon, subtitle, tooltip }: any) {
884
+ return (
885
+ <div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm flex items-start space-x-4">
886
+ <div className="p-3 bg-slate-50 rounded-lg border border-slate-100">{icon}</div>
887
+ <div className="flex-1">
888
+ <div className="text-sm font-medium text-slate-500 flex items-center">
889
+ {title}
890
+ {tooltip && <TooltipItem label={title} text={tooltip} />}
891
+ </div>
892
+ <h3 className="text-2xl font-bold text-slate-800 mt-1">{value}</h3>
893
+ {subtitle && <p className="text-xs text-slate-400 mt-1">{subtitle}</p>}
894
+ </div>
895
+ </div>
896
+ );
897
+ }
898
+
899
+ function InputField({
900
+ label,
901
+ type = "text",
902
+ value,
903
+ onChange,
904
+ placeholder,
905
+ tooltip,
906
+ }: any) {
907
+ return (
908
+ <div className="flex flex-col space-y-1.5">
909
+ <div className="text-sm font-medium text-slate-700 flex items-center">
910
+ {label}
911
+ {tooltip && <TooltipItem label={label} text={tooltip} />}
912
+ </div>
913
+ <input
914
+ type={type}
915
+ value={value}
916
+ onChange={onChange}
917
+ placeholder={placeholder}
918
+ className="px-3 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-sm bg-white text-slate-900"
919
+ />
920
+ </div>
921
+ );
922
+ }
923
+
924
+ function SelectField({
925
+ label,
926
+ value,
927
+ onChange,
928
+ options,
929
+ required,
930
+ tooltip,
931
+ }: any) {
932
+ return (
933
+ <div className="flex flex-col space-y-1.5">
934
+ <div className="text-sm font-medium text-slate-700 flex items-center">
935
+ {label} {required && <span className="text-red-500 ml-1">*</span>}
936
+ {tooltip && <TooltipItem label={label} text={tooltip} />}
937
+ </div>
938
+ <select
939
+ value={value}
940
+ onChange={(e) => onChange(e.target.value)}
941
+ className="px-3 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-sm bg-white text-slate-900"
942
+ >
943
+ <option value="">Select...</option>
944
+ {options?.map((opt: string) => (
945
+ <option key={opt} value={opt}>
946
+ {opt}
947
+ </option>
948
+ ))}
949
+ </select>
950
+ </div>
951
+ );
952
+ }
953
+
954
+ function ResultCard({ title, field, tooltip }: any) {
955
+ if (!field) return null;
956
+ const displayVal =
957
+ field.median !== undefined ? field.median.toFixed(2) : "N/A";
958
+
959
+ return (
960
+ <div className="bg-white p-5 rounded-xl border border-indigo-100 shadow-sm relative overflow-hidden group">
961
+ <div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
962
+ <Percent size={64} className="text-indigo-600" />
963
+ </div>
964
+ <div className="text-sm font-medium text-slate-500 mb-1 flex items-center">
965
+ {title}
966
+ {tooltip && <TooltipItem label={title} text={tooltip} />}
967
+ </div>
968
+ <h3 className="text-3xl font-bold text-indigo-600 mb-3">{displayVal}</h3>
969
+ <div className="grid grid-cols-2 gap-2 text-xs text-slate-500 border-t border-slate-100 pt-3">
970
+ <div>
971
+ <span className="block text-slate-400 flex items-center">
972
+ Min - Max
973
+ <TooltipItem label="Min - Max" text="Range of values in the top matching historical articles. P25 to P75 (interquartile range)." />
974
+ </span>
975
+ <span className="font-medium text-slate-700">
976
+ {field.p25?.toFixed(1) || 0} - {field.p75?.toFixed(1) || 0}
977
+ </span>
978
+ </div>
979
+ <div>
980
+ <span className="block text-slate-400 flex items-center">
981
+ Variance (CV%)
982
+ <TooltipItem label="Variance (CV%)" text="Coefficient of Variation - relative spread. Lower is more consistent. &lt;20% is good, &gt;35% is high variance." />
983
+ </span>
984
+ <span className="font-medium text-slate-700">
985
+ {field.cv?.toFixed(2) || 0}%
986
+ </span>
987
+ </div>
988
+ </div>
989
+ </div>
990
+ );
991
+ }
frontend/components/ui/button.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cn } from "../../lib/utils"
3
+
4
+ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
5
+ variant?: "default" | "outline" | "ghost" | "secondary"
6
+ size?: "default" | "sm" | "lg"
7
+ }
8
+
9
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
10
+ ({ className, variant = "default", size = "default", ...props }, ref) => {
11
+ return (
12
+ <button
13
+ ref={ref}
14
+ className={cn(
15
+ "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
16
+ {
17
+ "bg-primary text-primary-foreground hover:bg-primary/90": variant === "default",
18
+ "border border-input hover:bg-accent hover:text-accent-foreground": variant === "outline",
19
+ "hover:bg-accent hover:text-accent-foreground": variant === "ghost",
20
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80": variant === "secondary",
21
+ "h-10 py-2 px-4": size === "default",
22
+ "h-9 px-3 rounded-md": size === "sm",
23
+ "h-11 px-8 rounded-md": size === "lg",
24
+ },
25
+ className
26
+ )}
27
+ {...props}
28
+ />
29
+ )
30
+ }
31
+ )
32
+ Button.displayName = "Button"
33
+
34
+ export { Button }
frontend/components/ui/card.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cn } from "../../lib/utils"
3
+
4
+ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
5
+ <div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
6
+ ))
7
+ Card.displayName = "Card"
8
+
9
+ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
10
+ <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
11
+ ))
12
+ CardHeader.displayName = "CardHeader"
13
+
14
+ const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(({ className, ...props }, ref) => (
15
+ <h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
16
+ ))
17
+ CardTitle.displayName = "CardTitle"
18
+
19
+ const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(({ className, ...props }, ref) => (
20
+ <p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
21
+ ))
22
+ CardDescription.displayName = "CardDescription"
23
+
24
+ const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
25
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
26
+ ))
27
+ CardContent.displayName = "CardContent"
28
+
29
+ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
30
+ <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
31
+ ))
32
+ CardFooter.displayName = "CardFooter"
33
+
34
+ export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }
frontend/lib/utils.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
frontend/next-env.d.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ import "./.next/dev/types/routes.d.ts";
4
+
5
+ // NOTE: This file should not be edited
6
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
frontend/next.config.mjs ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ async rewrites() {
4
+ return [
5
+ {
6
+ source: "/api/:path*",
7
+ destination: "http://localhost:8000/api/:path*",
8
+ },
9
+ ];
10
+ },
11
+ };
12
+
13
+ export default nextConfig;
frontend/package-lock.json ADDED
@@ -0,0 +1,2754 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "pdc-frontend",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "pdc-frontend",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "axios": "^1.8.4",
12
+ "clsx": "^2.1.1",
13
+ "lucide-react": "^1.8.0",
14
+ "next": "16.1.6",
15
+ "react": "19.2.0",
16
+ "react-dom": "19.2.0",
17
+ "recharts": "^2.15.4",
18
+ "tailwind-merge": "^3.5.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.13.14",
22
+ "@types/react": "^19.0.12",
23
+ "@types/react-dom": "^19.0.4",
24
+ "autoprefixer": "^10.4.27",
25
+ "postcss": "^8.5.9",
26
+ "tailwindcss": "^3.4.19",
27
+ "typescript": "^5.8.2"
28
+ }
29
+ },
30
+ "node_modules/@alloc/quick-lru": {
31
+ "version": "5.2.0",
32
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
33
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
34
+ "dev": true,
35
+ "license": "MIT",
36
+ "engines": {
37
+ "node": ">=10"
38
+ },
39
+ "funding": {
40
+ "url": "https://github.com/sponsors/sindresorhus"
41
+ }
42
+ },
43
+ "node_modules/@babel/runtime": {
44
+ "version": "7.29.2",
45
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
46
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
47
+ "license": "MIT",
48
+ "engines": {
49
+ "node": ">=6.9.0"
50
+ }
51
+ },
52
+ "node_modules/@emnapi/runtime": {
53
+ "version": "1.9.2",
54
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
55
+ "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
56
+ "license": "MIT",
57
+ "optional": true,
58
+ "dependencies": {
59
+ "tslib": "^2.4.0"
60
+ }
61
+ },
62
+ "node_modules/@img/colour": {
63
+ "version": "1.1.0",
64
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
65
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
66
+ "license": "MIT",
67
+ "optional": true,
68
+ "engines": {
69
+ "node": ">=18"
70
+ }
71
+ },
72
+ "node_modules/@img/sharp-darwin-arm64": {
73
+ "version": "0.34.5",
74
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
75
+ "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
76
+ "cpu": [
77
+ "arm64"
78
+ ],
79
+ "license": "Apache-2.0",
80
+ "optional": true,
81
+ "os": [
82
+ "darwin"
83
+ ],
84
+ "engines": {
85
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
86
+ },
87
+ "funding": {
88
+ "url": "https://opencollective.com/libvips"
89
+ },
90
+ "optionalDependencies": {
91
+ "@img/sharp-libvips-darwin-arm64": "1.2.4"
92
+ }
93
+ },
94
+ "node_modules/@img/sharp-darwin-x64": {
95
+ "version": "0.34.5",
96
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
97
+ "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
98
+ "cpu": [
99
+ "x64"
100
+ ],
101
+ "license": "Apache-2.0",
102
+ "optional": true,
103
+ "os": [
104
+ "darwin"
105
+ ],
106
+ "engines": {
107
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
108
+ },
109
+ "funding": {
110
+ "url": "https://opencollective.com/libvips"
111
+ },
112
+ "optionalDependencies": {
113
+ "@img/sharp-libvips-darwin-x64": "1.2.4"
114
+ }
115
+ },
116
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
117
+ "version": "1.2.4",
118
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
119
+ "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
120
+ "cpu": [
121
+ "arm64"
122
+ ],
123
+ "license": "LGPL-3.0-or-later",
124
+ "optional": true,
125
+ "os": [
126
+ "darwin"
127
+ ],
128
+ "funding": {
129
+ "url": "https://opencollective.com/libvips"
130
+ }
131
+ },
132
+ "node_modules/@img/sharp-libvips-darwin-x64": {
133
+ "version": "1.2.4",
134
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
135
+ "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
136
+ "cpu": [
137
+ "x64"
138
+ ],
139
+ "license": "LGPL-3.0-or-later",
140
+ "optional": true,
141
+ "os": [
142
+ "darwin"
143
+ ],
144
+ "funding": {
145
+ "url": "https://opencollective.com/libvips"
146
+ }
147
+ },
148
+ "node_modules/@img/sharp-libvips-linux-arm": {
149
+ "version": "1.2.4",
150
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
151
+ "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
152
+ "cpu": [
153
+ "arm"
154
+ ],
155
+ "license": "LGPL-3.0-or-later",
156
+ "optional": true,
157
+ "os": [
158
+ "linux"
159
+ ],
160
+ "funding": {
161
+ "url": "https://opencollective.com/libvips"
162
+ }
163
+ },
164
+ "node_modules/@img/sharp-libvips-linux-arm64": {
165
+ "version": "1.2.4",
166
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
167
+ "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
168
+ "cpu": [
169
+ "arm64"
170
+ ],
171
+ "license": "LGPL-3.0-or-later",
172
+ "optional": true,
173
+ "os": [
174
+ "linux"
175
+ ],
176
+ "funding": {
177
+ "url": "https://opencollective.com/libvips"
178
+ }
179
+ },
180
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
181
+ "version": "1.2.4",
182
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
183
+ "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
184
+ "cpu": [
185
+ "ppc64"
186
+ ],
187
+ "license": "LGPL-3.0-or-later",
188
+ "optional": true,
189
+ "os": [
190
+ "linux"
191
+ ],
192
+ "funding": {
193
+ "url": "https://opencollective.com/libvips"
194
+ }
195
+ },
196
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
197
+ "version": "1.2.4",
198
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
199
+ "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
200
+ "cpu": [
201
+ "riscv64"
202
+ ],
203
+ "license": "LGPL-3.0-or-later",
204
+ "optional": true,
205
+ "os": [
206
+ "linux"
207
+ ],
208
+ "funding": {
209
+ "url": "https://opencollective.com/libvips"
210
+ }
211
+ },
212
+ "node_modules/@img/sharp-libvips-linux-s390x": {
213
+ "version": "1.2.4",
214
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
215
+ "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
216
+ "cpu": [
217
+ "s390x"
218
+ ],
219
+ "license": "LGPL-3.0-or-later",
220
+ "optional": true,
221
+ "os": [
222
+ "linux"
223
+ ],
224
+ "funding": {
225
+ "url": "https://opencollective.com/libvips"
226
+ }
227
+ },
228
+ "node_modules/@img/sharp-libvips-linux-x64": {
229
+ "version": "1.2.4",
230
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
231
+ "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
232
+ "cpu": [
233
+ "x64"
234
+ ],
235
+ "license": "LGPL-3.0-or-later",
236
+ "optional": true,
237
+ "os": [
238
+ "linux"
239
+ ],
240
+ "funding": {
241
+ "url": "https://opencollective.com/libvips"
242
+ }
243
+ },
244
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
245
+ "version": "1.2.4",
246
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
247
+ "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
248
+ "cpu": [
249
+ "arm64"
250
+ ],
251
+ "license": "LGPL-3.0-or-later",
252
+ "optional": true,
253
+ "os": [
254
+ "linux"
255
+ ],
256
+ "funding": {
257
+ "url": "https://opencollective.com/libvips"
258
+ }
259
+ },
260
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
261
+ "version": "1.2.4",
262
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
263
+ "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
264
+ "cpu": [
265
+ "x64"
266
+ ],
267
+ "license": "LGPL-3.0-or-later",
268
+ "optional": true,
269
+ "os": [
270
+ "linux"
271
+ ],
272
+ "funding": {
273
+ "url": "https://opencollective.com/libvips"
274
+ }
275
+ },
276
+ "node_modules/@img/sharp-linux-arm": {
277
+ "version": "0.34.5",
278
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
279
+ "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
280
+ "cpu": [
281
+ "arm"
282
+ ],
283
+ "license": "Apache-2.0",
284
+ "optional": true,
285
+ "os": [
286
+ "linux"
287
+ ],
288
+ "engines": {
289
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
290
+ },
291
+ "funding": {
292
+ "url": "https://opencollective.com/libvips"
293
+ },
294
+ "optionalDependencies": {
295
+ "@img/sharp-libvips-linux-arm": "1.2.4"
296
+ }
297
+ },
298
+ "node_modules/@img/sharp-linux-arm64": {
299
+ "version": "0.34.5",
300
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
301
+ "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
302
+ "cpu": [
303
+ "arm64"
304
+ ],
305
+ "license": "Apache-2.0",
306
+ "optional": true,
307
+ "os": [
308
+ "linux"
309
+ ],
310
+ "engines": {
311
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
312
+ },
313
+ "funding": {
314
+ "url": "https://opencollective.com/libvips"
315
+ },
316
+ "optionalDependencies": {
317
+ "@img/sharp-libvips-linux-arm64": "1.2.4"
318
+ }
319
+ },
320
+ "node_modules/@img/sharp-linux-ppc64": {
321
+ "version": "0.34.5",
322
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
323
+ "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
324
+ "cpu": [
325
+ "ppc64"
326
+ ],
327
+ "license": "Apache-2.0",
328
+ "optional": true,
329
+ "os": [
330
+ "linux"
331
+ ],
332
+ "engines": {
333
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
334
+ },
335
+ "funding": {
336
+ "url": "https://opencollective.com/libvips"
337
+ },
338
+ "optionalDependencies": {
339
+ "@img/sharp-libvips-linux-ppc64": "1.2.4"
340
+ }
341
+ },
342
+ "node_modules/@img/sharp-linux-riscv64": {
343
+ "version": "0.34.5",
344
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
345
+ "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
346
+ "cpu": [
347
+ "riscv64"
348
+ ],
349
+ "license": "Apache-2.0",
350
+ "optional": true,
351
+ "os": [
352
+ "linux"
353
+ ],
354
+ "engines": {
355
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
356
+ },
357
+ "funding": {
358
+ "url": "https://opencollective.com/libvips"
359
+ },
360
+ "optionalDependencies": {
361
+ "@img/sharp-libvips-linux-riscv64": "1.2.4"
362
+ }
363
+ },
364
+ "node_modules/@img/sharp-linux-s390x": {
365
+ "version": "0.34.5",
366
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
367
+ "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
368
+ "cpu": [
369
+ "s390x"
370
+ ],
371
+ "license": "Apache-2.0",
372
+ "optional": true,
373
+ "os": [
374
+ "linux"
375
+ ],
376
+ "engines": {
377
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
378
+ },
379
+ "funding": {
380
+ "url": "https://opencollective.com/libvips"
381
+ },
382
+ "optionalDependencies": {
383
+ "@img/sharp-libvips-linux-s390x": "1.2.4"
384
+ }
385
+ },
386
+ "node_modules/@img/sharp-linux-x64": {
387
+ "version": "0.34.5",
388
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
389
+ "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
390
+ "cpu": [
391
+ "x64"
392
+ ],
393
+ "license": "Apache-2.0",
394
+ "optional": true,
395
+ "os": [
396
+ "linux"
397
+ ],
398
+ "engines": {
399
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
400
+ },
401
+ "funding": {
402
+ "url": "https://opencollective.com/libvips"
403
+ },
404
+ "optionalDependencies": {
405
+ "@img/sharp-libvips-linux-x64": "1.2.4"
406
+ }
407
+ },
408
+ "node_modules/@img/sharp-linuxmusl-arm64": {
409
+ "version": "0.34.5",
410
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
411
+ "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
412
+ "cpu": [
413
+ "arm64"
414
+ ],
415
+ "license": "Apache-2.0",
416
+ "optional": true,
417
+ "os": [
418
+ "linux"
419
+ ],
420
+ "engines": {
421
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
422
+ },
423
+ "funding": {
424
+ "url": "https://opencollective.com/libvips"
425
+ },
426
+ "optionalDependencies": {
427
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
428
+ }
429
+ },
430
+ "node_modules/@img/sharp-linuxmusl-x64": {
431
+ "version": "0.34.5",
432
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
433
+ "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
434
+ "cpu": [
435
+ "x64"
436
+ ],
437
+ "license": "Apache-2.0",
438
+ "optional": true,
439
+ "os": [
440
+ "linux"
441
+ ],
442
+ "engines": {
443
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
444
+ },
445
+ "funding": {
446
+ "url": "https://opencollective.com/libvips"
447
+ },
448
+ "optionalDependencies": {
449
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
450
+ }
451
+ },
452
+ "node_modules/@img/sharp-wasm32": {
453
+ "version": "0.34.5",
454
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
455
+ "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
456
+ "cpu": [
457
+ "wasm32"
458
+ ],
459
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
460
+ "optional": true,
461
+ "dependencies": {
462
+ "@emnapi/runtime": "^1.7.0"
463
+ },
464
+ "engines": {
465
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
466
+ },
467
+ "funding": {
468
+ "url": "https://opencollective.com/libvips"
469
+ }
470
+ },
471
+ "node_modules/@img/sharp-win32-arm64": {
472
+ "version": "0.34.5",
473
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
474
+ "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
475
+ "cpu": [
476
+ "arm64"
477
+ ],
478
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
479
+ "optional": true,
480
+ "os": [
481
+ "win32"
482
+ ],
483
+ "engines": {
484
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
485
+ },
486
+ "funding": {
487
+ "url": "https://opencollective.com/libvips"
488
+ }
489
+ },
490
+ "node_modules/@img/sharp-win32-ia32": {
491
+ "version": "0.34.5",
492
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
493
+ "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
494
+ "cpu": [
495
+ "ia32"
496
+ ],
497
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
498
+ "optional": true,
499
+ "os": [
500
+ "win32"
501
+ ],
502
+ "engines": {
503
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
504
+ },
505
+ "funding": {
506
+ "url": "https://opencollective.com/libvips"
507
+ }
508
+ },
509
+ "node_modules/@img/sharp-win32-x64": {
510
+ "version": "0.34.5",
511
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
512
+ "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
513
+ "cpu": [
514
+ "x64"
515
+ ],
516
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
517
+ "optional": true,
518
+ "os": [
519
+ "win32"
520
+ ],
521
+ "engines": {
522
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
523
+ },
524
+ "funding": {
525
+ "url": "https://opencollective.com/libvips"
526
+ }
527
+ },
528
+ "node_modules/@jridgewell/gen-mapping": {
529
+ "version": "0.3.13",
530
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
531
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
532
+ "dev": true,
533
+ "license": "MIT",
534
+ "dependencies": {
535
+ "@jridgewell/sourcemap-codec": "^1.5.0",
536
+ "@jridgewell/trace-mapping": "^0.3.24"
537
+ }
538
+ },
539
+ "node_modules/@jridgewell/resolve-uri": {
540
+ "version": "3.1.2",
541
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
542
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
543
+ "dev": true,
544
+ "license": "MIT",
545
+ "engines": {
546
+ "node": ">=6.0.0"
547
+ }
548
+ },
549
+ "node_modules/@jridgewell/sourcemap-codec": {
550
+ "version": "1.5.5",
551
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
552
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
553
+ "dev": true,
554
+ "license": "MIT"
555
+ },
556
+ "node_modules/@jridgewell/trace-mapping": {
557
+ "version": "0.3.31",
558
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
559
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
560
+ "dev": true,
561
+ "license": "MIT",
562
+ "dependencies": {
563
+ "@jridgewell/resolve-uri": "^3.1.0",
564
+ "@jridgewell/sourcemap-codec": "^1.4.14"
565
+ }
566
+ },
567
+ "node_modules/@next/env": {
568
+ "version": "16.1.6",
569
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz",
570
+ "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==",
571
+ "license": "MIT"
572
+ },
573
+ "node_modules/@next/swc-darwin-arm64": {
574
+ "version": "16.1.6",
575
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz",
576
+ "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==",
577
+ "cpu": [
578
+ "arm64"
579
+ ],
580
+ "license": "MIT",
581
+ "optional": true,
582
+ "os": [
583
+ "darwin"
584
+ ],
585
+ "engines": {
586
+ "node": ">= 10"
587
+ }
588
+ },
589
+ "node_modules/@next/swc-darwin-x64": {
590
+ "version": "16.1.6",
591
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz",
592
+ "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==",
593
+ "cpu": [
594
+ "x64"
595
+ ],
596
+ "license": "MIT",
597
+ "optional": true,
598
+ "os": [
599
+ "darwin"
600
+ ],
601
+ "engines": {
602
+ "node": ">= 10"
603
+ }
604
+ },
605
+ "node_modules/@next/swc-linux-arm64-gnu": {
606
+ "version": "16.1.6",
607
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz",
608
+ "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==",
609
+ "cpu": [
610
+ "arm64"
611
+ ],
612
+ "license": "MIT",
613
+ "optional": true,
614
+ "os": [
615
+ "linux"
616
+ ],
617
+ "engines": {
618
+ "node": ">= 10"
619
+ }
620
+ },
621
+ "node_modules/@next/swc-linux-arm64-musl": {
622
+ "version": "16.1.6",
623
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz",
624
+ "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==",
625
+ "cpu": [
626
+ "arm64"
627
+ ],
628
+ "license": "MIT",
629
+ "optional": true,
630
+ "os": [
631
+ "linux"
632
+ ],
633
+ "engines": {
634
+ "node": ">= 10"
635
+ }
636
+ },
637
+ "node_modules/@next/swc-linux-x64-gnu": {
638
+ "version": "16.1.6",
639
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz",
640
+ "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==",
641
+ "cpu": [
642
+ "x64"
643
+ ],
644
+ "license": "MIT",
645
+ "optional": true,
646
+ "os": [
647
+ "linux"
648
+ ],
649
+ "engines": {
650
+ "node": ">= 10"
651
+ }
652
+ },
653
+ "node_modules/@next/swc-linux-x64-musl": {
654
+ "version": "16.1.6",
655
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz",
656
+ "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==",
657
+ "cpu": [
658
+ "x64"
659
+ ],
660
+ "license": "MIT",
661
+ "optional": true,
662
+ "os": [
663
+ "linux"
664
+ ],
665
+ "engines": {
666
+ "node": ">= 10"
667
+ }
668
+ },
669
+ "node_modules/@next/swc-win32-arm64-msvc": {
670
+ "version": "16.1.6",
671
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz",
672
+ "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==",
673
+ "cpu": [
674
+ "arm64"
675
+ ],
676
+ "license": "MIT",
677
+ "optional": true,
678
+ "os": [
679
+ "win32"
680
+ ],
681
+ "engines": {
682
+ "node": ">= 10"
683
+ }
684
+ },
685
+ "node_modules/@next/swc-win32-x64-msvc": {
686
+ "version": "16.1.6",
687
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz",
688
+ "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==",
689
+ "cpu": [
690
+ "x64"
691
+ ],
692
+ "license": "MIT",
693
+ "optional": true,
694
+ "os": [
695
+ "win32"
696
+ ],
697
+ "engines": {
698
+ "node": ">= 10"
699
+ }
700
+ },
701
+ "node_modules/@nodelib/fs.scandir": {
702
+ "version": "2.1.5",
703
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
704
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
705
+ "dev": true,
706
+ "license": "MIT",
707
+ "dependencies": {
708
+ "@nodelib/fs.stat": "2.0.5",
709
+ "run-parallel": "^1.1.9"
710
+ },
711
+ "engines": {
712
+ "node": ">= 8"
713
+ }
714
+ },
715
+ "node_modules/@nodelib/fs.stat": {
716
+ "version": "2.0.5",
717
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
718
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
719
+ "dev": true,
720
+ "license": "MIT",
721
+ "engines": {
722
+ "node": ">= 8"
723
+ }
724
+ },
725
+ "node_modules/@nodelib/fs.walk": {
726
+ "version": "1.2.8",
727
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
728
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
729
+ "dev": true,
730
+ "license": "MIT",
731
+ "dependencies": {
732
+ "@nodelib/fs.scandir": "2.1.5",
733
+ "fastq": "^1.6.0"
734
+ },
735
+ "engines": {
736
+ "node": ">= 8"
737
+ }
738
+ },
739
+ "node_modules/@swc/helpers": {
740
+ "version": "0.5.15",
741
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
742
+ "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
743
+ "license": "Apache-2.0",
744
+ "dependencies": {
745
+ "tslib": "^2.8.0"
746
+ }
747
+ },
748
+ "node_modules/@types/d3-array": {
749
+ "version": "3.2.2",
750
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
751
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
752
+ "license": "MIT"
753
+ },
754
+ "node_modules/@types/d3-color": {
755
+ "version": "3.1.3",
756
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
757
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
758
+ "license": "MIT"
759
+ },
760
+ "node_modules/@types/d3-ease": {
761
+ "version": "3.0.2",
762
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
763
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
764
+ "license": "MIT"
765
+ },
766
+ "node_modules/@types/d3-interpolate": {
767
+ "version": "3.0.4",
768
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
769
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
770
+ "license": "MIT",
771
+ "dependencies": {
772
+ "@types/d3-color": "*"
773
+ }
774
+ },
775
+ "node_modules/@types/d3-path": {
776
+ "version": "3.1.1",
777
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
778
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
779
+ "license": "MIT"
780
+ },
781
+ "node_modules/@types/d3-scale": {
782
+ "version": "4.0.9",
783
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
784
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
785
+ "license": "MIT",
786
+ "dependencies": {
787
+ "@types/d3-time": "*"
788
+ }
789
+ },
790
+ "node_modules/@types/d3-shape": {
791
+ "version": "3.1.8",
792
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
793
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
794
+ "license": "MIT",
795
+ "dependencies": {
796
+ "@types/d3-path": "*"
797
+ }
798
+ },
799
+ "node_modules/@types/d3-time": {
800
+ "version": "3.0.4",
801
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
802
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
803
+ "license": "MIT"
804
+ },
805
+ "node_modules/@types/d3-timer": {
806
+ "version": "3.0.2",
807
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
808
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
809
+ "license": "MIT"
810
+ },
811
+ "node_modules/@types/node": {
812
+ "version": "22.19.17",
813
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
814
+ "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
815
+ "dev": true,
816
+ "license": "MIT",
817
+ "dependencies": {
818
+ "undici-types": "~6.21.0"
819
+ }
820
+ },
821
+ "node_modules/@types/react": {
822
+ "version": "19.2.14",
823
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
824
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
825
+ "dev": true,
826
+ "license": "MIT",
827
+ "peer": true,
828
+ "dependencies": {
829
+ "csstype": "^3.2.2"
830
+ }
831
+ },
832
+ "node_modules/@types/react-dom": {
833
+ "version": "19.2.3",
834
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
835
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
836
+ "dev": true,
837
+ "license": "MIT",
838
+ "peerDependencies": {
839
+ "@types/react": "^19.2.0"
840
+ }
841
+ },
842
+ "node_modules/any-promise": {
843
+ "version": "1.3.0",
844
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
845
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
846
+ "dev": true,
847
+ "license": "MIT"
848
+ },
849
+ "node_modules/anymatch": {
850
+ "version": "3.1.3",
851
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
852
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
853
+ "dev": true,
854
+ "license": "ISC",
855
+ "dependencies": {
856
+ "normalize-path": "^3.0.0",
857
+ "picomatch": "^2.0.4"
858
+ },
859
+ "engines": {
860
+ "node": ">= 8"
861
+ }
862
+ },
863
+ "node_modules/arg": {
864
+ "version": "5.0.2",
865
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
866
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
867
+ "dev": true,
868
+ "license": "MIT"
869
+ },
870
+ "node_modules/asynckit": {
871
+ "version": "0.4.0",
872
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
873
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
874
+ "license": "MIT"
875
+ },
876
+ "node_modules/autoprefixer": {
877
+ "version": "10.4.27",
878
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz",
879
+ "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==",
880
+ "dev": true,
881
+ "funding": [
882
+ {
883
+ "type": "opencollective",
884
+ "url": "https://opencollective.com/postcss/"
885
+ },
886
+ {
887
+ "type": "tidelift",
888
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
889
+ },
890
+ {
891
+ "type": "github",
892
+ "url": "https://github.com/sponsors/ai"
893
+ }
894
+ ],
895
+ "license": "MIT",
896
+ "dependencies": {
897
+ "browserslist": "^4.28.1",
898
+ "caniuse-lite": "^1.0.30001774",
899
+ "fraction.js": "^5.3.4",
900
+ "picocolors": "^1.1.1",
901
+ "postcss-value-parser": "^4.2.0"
902
+ },
903
+ "bin": {
904
+ "autoprefixer": "bin/autoprefixer"
905
+ },
906
+ "engines": {
907
+ "node": "^10 || ^12 || >=14"
908
+ },
909
+ "peerDependencies": {
910
+ "postcss": "^8.1.0"
911
+ }
912
+ },
913
+ "node_modules/axios": {
914
+ "version": "1.15.0",
915
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
916
+ "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
917
+ "license": "MIT",
918
+ "dependencies": {
919
+ "follow-redirects": "^1.15.11",
920
+ "form-data": "^4.0.5",
921
+ "proxy-from-env": "^2.1.0"
922
+ }
923
+ },
924
+ "node_modules/baseline-browser-mapping": {
925
+ "version": "2.10.18",
926
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz",
927
+ "integrity": "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==",
928
+ "license": "Apache-2.0",
929
+ "bin": {
930
+ "baseline-browser-mapping": "dist/cli.cjs"
931
+ },
932
+ "engines": {
933
+ "node": ">=6.0.0"
934
+ }
935
+ },
936
+ "node_modules/binary-extensions": {
937
+ "version": "2.3.0",
938
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
939
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
940
+ "dev": true,
941
+ "license": "MIT",
942
+ "engines": {
943
+ "node": ">=8"
944
+ },
945
+ "funding": {
946
+ "url": "https://github.com/sponsors/sindresorhus"
947
+ }
948
+ },
949
+ "node_modules/braces": {
950
+ "version": "3.0.3",
951
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
952
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
953
+ "dev": true,
954
+ "license": "MIT",
955
+ "dependencies": {
956
+ "fill-range": "^7.1.1"
957
+ },
958
+ "engines": {
959
+ "node": ">=8"
960
+ }
961
+ },
962
+ "node_modules/browserslist": {
963
+ "version": "4.28.2",
964
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
965
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
966
+ "dev": true,
967
+ "funding": [
968
+ {
969
+ "type": "opencollective",
970
+ "url": "https://opencollective.com/browserslist"
971
+ },
972
+ {
973
+ "type": "tidelift",
974
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
975
+ },
976
+ {
977
+ "type": "github",
978
+ "url": "https://github.com/sponsors/ai"
979
+ }
980
+ ],
981
+ "license": "MIT",
982
+ "peer": true,
983
+ "dependencies": {
984
+ "baseline-browser-mapping": "^2.10.12",
985
+ "caniuse-lite": "^1.0.30001782",
986
+ "electron-to-chromium": "^1.5.328",
987
+ "node-releases": "^2.0.36",
988
+ "update-browserslist-db": "^1.2.3"
989
+ },
990
+ "bin": {
991
+ "browserslist": "cli.js"
992
+ },
993
+ "engines": {
994
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
995
+ }
996
+ },
997
+ "node_modules/call-bind-apply-helpers": {
998
+ "version": "1.0.2",
999
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
1000
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
1001
+ "license": "MIT",
1002
+ "dependencies": {
1003
+ "es-errors": "^1.3.0",
1004
+ "function-bind": "^1.1.2"
1005
+ },
1006
+ "engines": {
1007
+ "node": ">= 0.4"
1008
+ }
1009
+ },
1010
+ "node_modules/camelcase-css": {
1011
+ "version": "2.0.1",
1012
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
1013
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
1014
+ "dev": true,
1015
+ "license": "MIT",
1016
+ "engines": {
1017
+ "node": ">= 6"
1018
+ }
1019
+ },
1020
+ "node_modules/caniuse-lite": {
1021
+ "version": "1.0.30001787",
1022
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz",
1023
+ "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==",
1024
+ "funding": [
1025
+ {
1026
+ "type": "opencollective",
1027
+ "url": "https://opencollective.com/browserslist"
1028
+ },
1029
+ {
1030
+ "type": "tidelift",
1031
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1032
+ },
1033
+ {
1034
+ "type": "github",
1035
+ "url": "https://github.com/sponsors/ai"
1036
+ }
1037
+ ],
1038
+ "license": "CC-BY-4.0"
1039
+ },
1040
+ "node_modules/chokidar": {
1041
+ "version": "3.6.0",
1042
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
1043
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
1044
+ "dev": true,
1045
+ "license": "MIT",
1046
+ "dependencies": {
1047
+ "anymatch": "~3.1.2",
1048
+ "braces": "~3.0.2",
1049
+ "glob-parent": "~5.1.2",
1050
+ "is-binary-path": "~2.1.0",
1051
+ "is-glob": "~4.0.1",
1052
+ "normalize-path": "~3.0.0",
1053
+ "readdirp": "~3.6.0"
1054
+ },
1055
+ "engines": {
1056
+ "node": ">= 8.10.0"
1057
+ },
1058
+ "funding": {
1059
+ "url": "https://paulmillr.com/funding/"
1060
+ },
1061
+ "optionalDependencies": {
1062
+ "fsevents": "~2.3.2"
1063
+ }
1064
+ },
1065
+ "node_modules/chokidar/node_modules/glob-parent": {
1066
+ "version": "5.1.2",
1067
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
1068
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
1069
+ "dev": true,
1070
+ "license": "ISC",
1071
+ "dependencies": {
1072
+ "is-glob": "^4.0.1"
1073
+ },
1074
+ "engines": {
1075
+ "node": ">= 6"
1076
+ }
1077
+ },
1078
+ "node_modules/client-only": {
1079
+ "version": "0.0.1",
1080
+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
1081
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
1082
+ "license": "MIT"
1083
+ },
1084
+ "node_modules/clsx": {
1085
+ "version": "2.1.1",
1086
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
1087
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
1088
+ "license": "MIT",
1089
+ "engines": {
1090
+ "node": ">=6"
1091
+ }
1092
+ },
1093
+ "node_modules/combined-stream": {
1094
+ "version": "1.0.8",
1095
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
1096
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
1097
+ "license": "MIT",
1098
+ "dependencies": {
1099
+ "delayed-stream": "~1.0.0"
1100
+ },
1101
+ "engines": {
1102
+ "node": ">= 0.8"
1103
+ }
1104
+ },
1105
+ "node_modules/commander": {
1106
+ "version": "4.1.1",
1107
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
1108
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
1109
+ "dev": true,
1110
+ "license": "MIT",
1111
+ "engines": {
1112
+ "node": ">= 6"
1113
+ }
1114
+ },
1115
+ "node_modules/cssesc": {
1116
+ "version": "3.0.0",
1117
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
1118
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
1119
+ "dev": true,
1120
+ "license": "MIT",
1121
+ "bin": {
1122
+ "cssesc": "bin/cssesc"
1123
+ },
1124
+ "engines": {
1125
+ "node": ">=4"
1126
+ }
1127
+ },
1128
+ "node_modules/csstype": {
1129
+ "version": "3.2.3",
1130
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1131
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1132
+ "license": "MIT"
1133
+ },
1134
+ "node_modules/d3-array": {
1135
+ "version": "3.2.4",
1136
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
1137
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
1138
+ "license": "ISC",
1139
+ "dependencies": {
1140
+ "internmap": "1 - 2"
1141
+ },
1142
+ "engines": {
1143
+ "node": ">=12"
1144
+ }
1145
+ },
1146
+ "node_modules/d3-color": {
1147
+ "version": "3.1.0",
1148
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
1149
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
1150
+ "license": "ISC",
1151
+ "engines": {
1152
+ "node": ">=12"
1153
+ }
1154
+ },
1155
+ "node_modules/d3-ease": {
1156
+ "version": "3.0.1",
1157
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
1158
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
1159
+ "license": "BSD-3-Clause",
1160
+ "engines": {
1161
+ "node": ">=12"
1162
+ }
1163
+ },
1164
+ "node_modules/d3-format": {
1165
+ "version": "3.1.2",
1166
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
1167
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
1168
+ "license": "ISC",
1169
+ "engines": {
1170
+ "node": ">=12"
1171
+ }
1172
+ },
1173
+ "node_modules/d3-interpolate": {
1174
+ "version": "3.0.1",
1175
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
1176
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
1177
+ "license": "ISC",
1178
+ "dependencies": {
1179
+ "d3-color": "1 - 3"
1180
+ },
1181
+ "engines": {
1182
+ "node": ">=12"
1183
+ }
1184
+ },
1185
+ "node_modules/d3-path": {
1186
+ "version": "3.1.0",
1187
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
1188
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
1189
+ "license": "ISC",
1190
+ "engines": {
1191
+ "node": ">=12"
1192
+ }
1193
+ },
1194
+ "node_modules/d3-scale": {
1195
+ "version": "4.0.2",
1196
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
1197
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
1198
+ "license": "ISC",
1199
+ "dependencies": {
1200
+ "d3-array": "2.10.0 - 3",
1201
+ "d3-format": "1 - 3",
1202
+ "d3-interpolate": "1.2.0 - 3",
1203
+ "d3-time": "2.1.1 - 3",
1204
+ "d3-time-format": "2 - 4"
1205
+ },
1206
+ "engines": {
1207
+ "node": ">=12"
1208
+ }
1209
+ },
1210
+ "node_modules/d3-shape": {
1211
+ "version": "3.2.0",
1212
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
1213
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
1214
+ "license": "ISC",
1215
+ "dependencies": {
1216
+ "d3-path": "^3.1.0"
1217
+ },
1218
+ "engines": {
1219
+ "node": ">=12"
1220
+ }
1221
+ },
1222
+ "node_modules/d3-time": {
1223
+ "version": "3.1.0",
1224
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
1225
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
1226
+ "license": "ISC",
1227
+ "dependencies": {
1228
+ "d3-array": "2 - 3"
1229
+ },
1230
+ "engines": {
1231
+ "node": ">=12"
1232
+ }
1233
+ },
1234
+ "node_modules/d3-time-format": {
1235
+ "version": "4.1.0",
1236
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
1237
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
1238
+ "license": "ISC",
1239
+ "dependencies": {
1240
+ "d3-time": "1 - 3"
1241
+ },
1242
+ "engines": {
1243
+ "node": ">=12"
1244
+ }
1245
+ },
1246
+ "node_modules/d3-timer": {
1247
+ "version": "3.0.1",
1248
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
1249
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
1250
+ "license": "ISC",
1251
+ "engines": {
1252
+ "node": ">=12"
1253
+ }
1254
+ },
1255
+ "node_modules/decimal.js-light": {
1256
+ "version": "2.5.1",
1257
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
1258
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
1259
+ "license": "MIT"
1260
+ },
1261
+ "node_modules/delayed-stream": {
1262
+ "version": "1.0.0",
1263
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
1264
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
1265
+ "license": "MIT",
1266
+ "engines": {
1267
+ "node": ">=0.4.0"
1268
+ }
1269
+ },
1270
+ "node_modules/detect-libc": {
1271
+ "version": "2.1.2",
1272
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
1273
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
1274
+ "license": "Apache-2.0",
1275
+ "optional": true,
1276
+ "engines": {
1277
+ "node": ">=8"
1278
+ }
1279
+ },
1280
+ "node_modules/didyoumean": {
1281
+ "version": "1.2.2",
1282
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
1283
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
1284
+ "dev": true,
1285
+ "license": "Apache-2.0"
1286
+ },
1287
+ "node_modules/dlv": {
1288
+ "version": "1.1.3",
1289
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
1290
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
1291
+ "dev": true,
1292
+ "license": "MIT"
1293
+ },
1294
+ "node_modules/dom-helpers": {
1295
+ "version": "5.2.1",
1296
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
1297
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
1298
+ "license": "MIT",
1299
+ "dependencies": {
1300
+ "@babel/runtime": "^7.8.7",
1301
+ "csstype": "^3.0.2"
1302
+ }
1303
+ },
1304
+ "node_modules/dunder-proto": {
1305
+ "version": "1.0.1",
1306
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
1307
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
1308
+ "license": "MIT",
1309
+ "dependencies": {
1310
+ "call-bind-apply-helpers": "^1.0.1",
1311
+ "es-errors": "^1.3.0",
1312
+ "gopd": "^1.2.0"
1313
+ },
1314
+ "engines": {
1315
+ "node": ">= 0.4"
1316
+ }
1317
+ },
1318
+ "node_modules/electron-to-chromium": {
1319
+ "version": "1.5.335",
1320
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz",
1321
+ "integrity": "sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==",
1322
+ "dev": true,
1323
+ "license": "ISC"
1324
+ },
1325
+ "node_modules/es-define-property": {
1326
+ "version": "1.0.1",
1327
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
1328
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
1329
+ "license": "MIT",
1330
+ "engines": {
1331
+ "node": ">= 0.4"
1332
+ }
1333
+ },
1334
+ "node_modules/es-errors": {
1335
+ "version": "1.3.0",
1336
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
1337
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
1338
+ "license": "MIT",
1339
+ "engines": {
1340
+ "node": ">= 0.4"
1341
+ }
1342
+ },
1343
+ "node_modules/es-object-atoms": {
1344
+ "version": "1.1.1",
1345
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
1346
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
1347
+ "license": "MIT",
1348
+ "dependencies": {
1349
+ "es-errors": "^1.3.0"
1350
+ },
1351
+ "engines": {
1352
+ "node": ">= 0.4"
1353
+ }
1354
+ },
1355
+ "node_modules/es-set-tostringtag": {
1356
+ "version": "2.1.0",
1357
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
1358
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
1359
+ "license": "MIT",
1360
+ "dependencies": {
1361
+ "es-errors": "^1.3.0",
1362
+ "get-intrinsic": "^1.2.6",
1363
+ "has-tostringtag": "^1.0.2",
1364
+ "hasown": "^2.0.2"
1365
+ },
1366
+ "engines": {
1367
+ "node": ">= 0.4"
1368
+ }
1369
+ },
1370
+ "node_modules/escalade": {
1371
+ "version": "3.2.0",
1372
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1373
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1374
+ "dev": true,
1375
+ "license": "MIT",
1376
+ "engines": {
1377
+ "node": ">=6"
1378
+ }
1379
+ },
1380
+ "node_modules/eventemitter3": {
1381
+ "version": "4.0.7",
1382
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
1383
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
1384
+ "license": "MIT"
1385
+ },
1386
+ "node_modules/fast-equals": {
1387
+ "version": "5.4.0",
1388
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
1389
+ "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
1390
+ "license": "MIT",
1391
+ "engines": {
1392
+ "node": ">=6.0.0"
1393
+ }
1394
+ },
1395
+ "node_modules/fast-glob": {
1396
+ "version": "3.3.3",
1397
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
1398
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
1399
+ "dev": true,
1400
+ "license": "MIT",
1401
+ "dependencies": {
1402
+ "@nodelib/fs.stat": "^2.0.2",
1403
+ "@nodelib/fs.walk": "^1.2.3",
1404
+ "glob-parent": "^5.1.2",
1405
+ "merge2": "^1.3.0",
1406
+ "micromatch": "^4.0.8"
1407
+ },
1408
+ "engines": {
1409
+ "node": ">=8.6.0"
1410
+ }
1411
+ },
1412
+ "node_modules/fast-glob/node_modules/glob-parent": {
1413
+ "version": "5.1.2",
1414
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
1415
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
1416
+ "dev": true,
1417
+ "license": "ISC",
1418
+ "dependencies": {
1419
+ "is-glob": "^4.0.1"
1420
+ },
1421
+ "engines": {
1422
+ "node": ">= 6"
1423
+ }
1424
+ },
1425
+ "node_modules/fastq": {
1426
+ "version": "1.20.1",
1427
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
1428
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
1429
+ "dev": true,
1430
+ "license": "ISC",
1431
+ "dependencies": {
1432
+ "reusify": "^1.0.4"
1433
+ }
1434
+ },
1435
+ "node_modules/fill-range": {
1436
+ "version": "7.1.1",
1437
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
1438
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
1439
+ "dev": true,
1440
+ "license": "MIT",
1441
+ "dependencies": {
1442
+ "to-regex-range": "^5.0.1"
1443
+ },
1444
+ "engines": {
1445
+ "node": ">=8"
1446
+ }
1447
+ },
1448
+ "node_modules/follow-redirects": {
1449
+ "version": "1.15.11",
1450
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
1451
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
1452
+ "funding": [
1453
+ {
1454
+ "type": "individual",
1455
+ "url": "https://github.com/sponsors/RubenVerborgh"
1456
+ }
1457
+ ],
1458
+ "license": "MIT",
1459
+ "engines": {
1460
+ "node": ">=4.0"
1461
+ },
1462
+ "peerDependenciesMeta": {
1463
+ "debug": {
1464
+ "optional": true
1465
+ }
1466
+ }
1467
+ },
1468
+ "node_modules/form-data": {
1469
+ "version": "4.0.5",
1470
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
1471
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
1472
+ "license": "MIT",
1473
+ "dependencies": {
1474
+ "asynckit": "^0.4.0",
1475
+ "combined-stream": "^1.0.8",
1476
+ "es-set-tostringtag": "^2.1.0",
1477
+ "hasown": "^2.0.2",
1478
+ "mime-types": "^2.1.12"
1479
+ },
1480
+ "engines": {
1481
+ "node": ">= 6"
1482
+ }
1483
+ },
1484
+ "node_modules/fraction.js": {
1485
+ "version": "5.3.4",
1486
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
1487
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
1488
+ "dev": true,
1489
+ "license": "MIT",
1490
+ "engines": {
1491
+ "node": "*"
1492
+ },
1493
+ "funding": {
1494
+ "type": "github",
1495
+ "url": "https://github.com/sponsors/rawify"
1496
+ }
1497
+ },
1498
+ "node_modules/fsevents": {
1499
+ "version": "2.3.3",
1500
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1501
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1502
+ "dev": true,
1503
+ "hasInstallScript": true,
1504
+ "license": "MIT",
1505
+ "optional": true,
1506
+ "os": [
1507
+ "darwin"
1508
+ ],
1509
+ "engines": {
1510
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1511
+ }
1512
+ },
1513
+ "node_modules/function-bind": {
1514
+ "version": "1.1.2",
1515
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1516
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1517
+ "license": "MIT",
1518
+ "funding": {
1519
+ "url": "https://github.com/sponsors/ljharb"
1520
+ }
1521
+ },
1522
+ "node_modules/get-intrinsic": {
1523
+ "version": "1.3.0",
1524
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
1525
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
1526
+ "license": "MIT",
1527
+ "dependencies": {
1528
+ "call-bind-apply-helpers": "^1.0.2",
1529
+ "es-define-property": "^1.0.1",
1530
+ "es-errors": "^1.3.0",
1531
+ "es-object-atoms": "^1.1.1",
1532
+ "function-bind": "^1.1.2",
1533
+ "get-proto": "^1.0.1",
1534
+ "gopd": "^1.2.0",
1535
+ "has-symbols": "^1.1.0",
1536
+ "hasown": "^2.0.2",
1537
+ "math-intrinsics": "^1.1.0"
1538
+ },
1539
+ "engines": {
1540
+ "node": ">= 0.4"
1541
+ },
1542
+ "funding": {
1543
+ "url": "https://github.com/sponsors/ljharb"
1544
+ }
1545
+ },
1546
+ "node_modules/get-proto": {
1547
+ "version": "1.0.1",
1548
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
1549
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
1550
+ "license": "MIT",
1551
+ "dependencies": {
1552
+ "dunder-proto": "^1.0.1",
1553
+ "es-object-atoms": "^1.0.0"
1554
+ },
1555
+ "engines": {
1556
+ "node": ">= 0.4"
1557
+ }
1558
+ },
1559
+ "node_modules/glob-parent": {
1560
+ "version": "6.0.2",
1561
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
1562
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
1563
+ "dev": true,
1564
+ "license": "ISC",
1565
+ "dependencies": {
1566
+ "is-glob": "^4.0.3"
1567
+ },
1568
+ "engines": {
1569
+ "node": ">=10.13.0"
1570
+ }
1571
+ },
1572
+ "node_modules/gopd": {
1573
+ "version": "1.2.0",
1574
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
1575
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
1576
+ "license": "MIT",
1577
+ "engines": {
1578
+ "node": ">= 0.4"
1579
+ },
1580
+ "funding": {
1581
+ "url": "https://github.com/sponsors/ljharb"
1582
+ }
1583
+ },
1584
+ "node_modules/has-symbols": {
1585
+ "version": "1.1.0",
1586
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
1587
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
1588
+ "license": "MIT",
1589
+ "engines": {
1590
+ "node": ">= 0.4"
1591
+ },
1592
+ "funding": {
1593
+ "url": "https://github.com/sponsors/ljharb"
1594
+ }
1595
+ },
1596
+ "node_modules/has-tostringtag": {
1597
+ "version": "1.0.2",
1598
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
1599
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
1600
+ "license": "MIT",
1601
+ "dependencies": {
1602
+ "has-symbols": "^1.0.3"
1603
+ },
1604
+ "engines": {
1605
+ "node": ">= 0.4"
1606
+ },
1607
+ "funding": {
1608
+ "url": "https://github.com/sponsors/ljharb"
1609
+ }
1610
+ },
1611
+ "node_modules/hasown": {
1612
+ "version": "2.0.2",
1613
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
1614
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
1615
+ "license": "MIT",
1616
+ "dependencies": {
1617
+ "function-bind": "^1.1.2"
1618
+ },
1619
+ "engines": {
1620
+ "node": ">= 0.4"
1621
+ }
1622
+ },
1623
+ "node_modules/internmap": {
1624
+ "version": "2.0.3",
1625
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
1626
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
1627
+ "license": "ISC",
1628
+ "engines": {
1629
+ "node": ">=12"
1630
+ }
1631
+ },
1632
+ "node_modules/is-binary-path": {
1633
+ "version": "2.1.0",
1634
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
1635
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
1636
+ "dev": true,
1637
+ "license": "MIT",
1638
+ "dependencies": {
1639
+ "binary-extensions": "^2.0.0"
1640
+ },
1641
+ "engines": {
1642
+ "node": ">=8"
1643
+ }
1644
+ },
1645
+ "node_modules/is-core-module": {
1646
+ "version": "2.16.1",
1647
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
1648
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
1649
+ "dev": true,
1650
+ "license": "MIT",
1651
+ "dependencies": {
1652
+ "hasown": "^2.0.2"
1653
+ },
1654
+ "engines": {
1655
+ "node": ">= 0.4"
1656
+ },
1657
+ "funding": {
1658
+ "url": "https://github.com/sponsors/ljharb"
1659
+ }
1660
+ },
1661
+ "node_modules/is-extglob": {
1662
+ "version": "2.1.1",
1663
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1664
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1665
+ "dev": true,
1666
+ "license": "MIT",
1667
+ "engines": {
1668
+ "node": ">=0.10.0"
1669
+ }
1670
+ },
1671
+ "node_modules/is-glob": {
1672
+ "version": "4.0.3",
1673
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1674
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1675
+ "dev": true,
1676
+ "license": "MIT",
1677
+ "dependencies": {
1678
+ "is-extglob": "^2.1.1"
1679
+ },
1680
+ "engines": {
1681
+ "node": ">=0.10.0"
1682
+ }
1683
+ },
1684
+ "node_modules/is-number": {
1685
+ "version": "7.0.0",
1686
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
1687
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
1688
+ "dev": true,
1689
+ "license": "MIT",
1690
+ "engines": {
1691
+ "node": ">=0.12.0"
1692
+ }
1693
+ },
1694
+ "node_modules/jiti": {
1695
+ "version": "1.21.7",
1696
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
1697
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
1698
+ "dev": true,
1699
+ "license": "MIT",
1700
+ "peer": true,
1701
+ "bin": {
1702
+ "jiti": "bin/jiti.js"
1703
+ }
1704
+ },
1705
+ "node_modules/js-tokens": {
1706
+ "version": "4.0.0",
1707
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1708
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1709
+ "license": "MIT"
1710
+ },
1711
+ "node_modules/lilconfig": {
1712
+ "version": "3.1.3",
1713
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
1714
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
1715
+ "dev": true,
1716
+ "license": "MIT",
1717
+ "engines": {
1718
+ "node": ">=14"
1719
+ },
1720
+ "funding": {
1721
+ "url": "https://github.com/sponsors/antonk52"
1722
+ }
1723
+ },
1724
+ "node_modules/lines-and-columns": {
1725
+ "version": "1.2.4",
1726
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
1727
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
1728
+ "dev": true,
1729
+ "license": "MIT"
1730
+ },
1731
+ "node_modules/lodash": {
1732
+ "version": "4.18.1",
1733
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
1734
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
1735
+ "license": "MIT"
1736
+ },
1737
+ "node_modules/loose-envify": {
1738
+ "version": "1.4.0",
1739
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
1740
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
1741
+ "license": "MIT",
1742
+ "dependencies": {
1743
+ "js-tokens": "^3.0.0 || ^4.0.0"
1744
+ },
1745
+ "bin": {
1746
+ "loose-envify": "cli.js"
1747
+ }
1748
+ },
1749
+ "node_modules/lucide-react": {
1750
+ "version": "1.8.0",
1751
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.8.0.tgz",
1752
+ "integrity": "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==",
1753
+ "license": "ISC",
1754
+ "peerDependencies": {
1755
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
1756
+ }
1757
+ },
1758
+ "node_modules/math-intrinsics": {
1759
+ "version": "1.1.0",
1760
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1761
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
1762
+ "license": "MIT",
1763
+ "engines": {
1764
+ "node": ">= 0.4"
1765
+ }
1766
+ },
1767
+ "node_modules/merge2": {
1768
+ "version": "1.4.1",
1769
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
1770
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
1771
+ "dev": true,
1772
+ "license": "MIT",
1773
+ "engines": {
1774
+ "node": ">= 8"
1775
+ }
1776
+ },
1777
+ "node_modules/micromatch": {
1778
+ "version": "4.0.8",
1779
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
1780
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
1781
+ "dev": true,
1782
+ "license": "MIT",
1783
+ "dependencies": {
1784
+ "braces": "^3.0.3",
1785
+ "picomatch": "^2.3.1"
1786
+ },
1787
+ "engines": {
1788
+ "node": ">=8.6"
1789
+ }
1790
+ },
1791
+ "node_modules/mime-db": {
1792
+ "version": "1.52.0",
1793
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1794
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1795
+ "license": "MIT",
1796
+ "engines": {
1797
+ "node": ">= 0.6"
1798
+ }
1799
+ },
1800
+ "node_modules/mime-types": {
1801
+ "version": "2.1.35",
1802
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1803
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1804
+ "license": "MIT",
1805
+ "dependencies": {
1806
+ "mime-db": "1.52.0"
1807
+ },
1808
+ "engines": {
1809
+ "node": ">= 0.6"
1810
+ }
1811
+ },
1812
+ "node_modules/mz": {
1813
+ "version": "2.7.0",
1814
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
1815
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
1816
+ "dev": true,
1817
+ "license": "MIT",
1818
+ "dependencies": {
1819
+ "any-promise": "^1.0.0",
1820
+ "object-assign": "^4.0.1",
1821
+ "thenify-all": "^1.0.0"
1822
+ }
1823
+ },
1824
+ "node_modules/nanoid": {
1825
+ "version": "3.3.11",
1826
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1827
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1828
+ "funding": [
1829
+ {
1830
+ "type": "github",
1831
+ "url": "https://github.com/sponsors/ai"
1832
+ }
1833
+ ],
1834
+ "license": "MIT",
1835
+ "bin": {
1836
+ "nanoid": "bin/nanoid.cjs"
1837
+ },
1838
+ "engines": {
1839
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1840
+ }
1841
+ },
1842
+ "node_modules/next": {
1843
+ "version": "16.1.6",
1844
+ "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz",
1845
+ "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==",
1846
+ "license": "MIT",
1847
+ "dependencies": {
1848
+ "@next/env": "16.1.6",
1849
+ "@swc/helpers": "0.5.15",
1850
+ "baseline-browser-mapping": "^2.8.3",
1851
+ "caniuse-lite": "^1.0.30001579",
1852
+ "postcss": "8.4.31",
1853
+ "styled-jsx": "5.1.6"
1854
+ },
1855
+ "bin": {
1856
+ "next": "dist/bin/next"
1857
+ },
1858
+ "engines": {
1859
+ "node": ">=20.9.0"
1860
+ },
1861
+ "optionalDependencies": {
1862
+ "@next/swc-darwin-arm64": "16.1.6",
1863
+ "@next/swc-darwin-x64": "16.1.6",
1864
+ "@next/swc-linux-arm64-gnu": "16.1.6",
1865
+ "@next/swc-linux-arm64-musl": "16.1.6",
1866
+ "@next/swc-linux-x64-gnu": "16.1.6",
1867
+ "@next/swc-linux-x64-musl": "16.1.6",
1868
+ "@next/swc-win32-arm64-msvc": "16.1.6",
1869
+ "@next/swc-win32-x64-msvc": "16.1.6",
1870
+ "sharp": "^0.34.4"
1871
+ },
1872
+ "peerDependencies": {
1873
+ "@opentelemetry/api": "^1.1.0",
1874
+ "@playwright/test": "^1.51.1",
1875
+ "babel-plugin-react-compiler": "*",
1876
+ "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
1877
+ "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
1878
+ "sass": "^1.3.0"
1879
+ },
1880
+ "peerDependenciesMeta": {
1881
+ "@opentelemetry/api": {
1882
+ "optional": true
1883
+ },
1884
+ "@playwright/test": {
1885
+ "optional": true
1886
+ },
1887
+ "babel-plugin-react-compiler": {
1888
+ "optional": true
1889
+ },
1890
+ "sass": {
1891
+ "optional": true
1892
+ }
1893
+ }
1894
+ },
1895
+ "node_modules/next/node_modules/postcss": {
1896
+ "version": "8.4.31",
1897
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
1898
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
1899
+ "funding": [
1900
+ {
1901
+ "type": "opencollective",
1902
+ "url": "https://opencollective.com/postcss/"
1903
+ },
1904
+ {
1905
+ "type": "tidelift",
1906
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1907
+ },
1908
+ {
1909
+ "type": "github",
1910
+ "url": "https://github.com/sponsors/ai"
1911
+ }
1912
+ ],
1913
+ "license": "MIT",
1914
+ "dependencies": {
1915
+ "nanoid": "^3.3.6",
1916
+ "picocolors": "^1.0.0",
1917
+ "source-map-js": "^1.0.2"
1918
+ },
1919
+ "engines": {
1920
+ "node": "^10 || ^12 || >=14"
1921
+ }
1922
+ },
1923
+ "node_modules/node-releases": {
1924
+ "version": "2.0.37",
1925
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
1926
+ "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
1927
+ "dev": true,
1928
+ "license": "MIT"
1929
+ },
1930
+ "node_modules/normalize-path": {
1931
+ "version": "3.0.0",
1932
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1933
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1934
+ "dev": true,
1935
+ "license": "MIT",
1936
+ "engines": {
1937
+ "node": ">=0.10.0"
1938
+ }
1939
+ },
1940
+ "node_modules/object-assign": {
1941
+ "version": "4.1.1",
1942
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1943
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1944
+ "license": "MIT",
1945
+ "engines": {
1946
+ "node": ">=0.10.0"
1947
+ }
1948
+ },
1949
+ "node_modules/object-hash": {
1950
+ "version": "3.0.0",
1951
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
1952
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
1953
+ "dev": true,
1954
+ "license": "MIT",
1955
+ "engines": {
1956
+ "node": ">= 6"
1957
+ }
1958
+ },
1959
+ "node_modules/path-parse": {
1960
+ "version": "1.0.7",
1961
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
1962
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
1963
+ "dev": true,
1964
+ "license": "MIT"
1965
+ },
1966
+ "node_modules/picocolors": {
1967
+ "version": "1.1.1",
1968
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1969
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1970
+ "license": "ISC"
1971
+ },
1972
+ "node_modules/picomatch": {
1973
+ "version": "2.3.2",
1974
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
1975
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
1976
+ "dev": true,
1977
+ "license": "MIT",
1978
+ "engines": {
1979
+ "node": ">=8.6"
1980
+ },
1981
+ "funding": {
1982
+ "url": "https://github.com/sponsors/jonschlinkert"
1983
+ }
1984
+ },
1985
+ "node_modules/pify": {
1986
+ "version": "2.3.0",
1987
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
1988
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
1989
+ "dev": true,
1990
+ "license": "MIT",
1991
+ "engines": {
1992
+ "node": ">=0.10.0"
1993
+ }
1994
+ },
1995
+ "node_modules/pirates": {
1996
+ "version": "4.0.7",
1997
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
1998
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
1999
+ "dev": true,
2000
+ "license": "MIT",
2001
+ "engines": {
2002
+ "node": ">= 6"
2003
+ }
2004
+ },
2005
+ "node_modules/postcss": {
2006
+ "version": "8.5.9",
2007
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
2008
+ "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
2009
+ "dev": true,
2010
+ "funding": [
2011
+ {
2012
+ "type": "opencollective",
2013
+ "url": "https://opencollective.com/postcss/"
2014
+ },
2015
+ {
2016
+ "type": "tidelift",
2017
+ "url": "https://tidelift.com/funding/github/npm/postcss"
2018
+ },
2019
+ {
2020
+ "type": "github",
2021
+ "url": "https://github.com/sponsors/ai"
2022
+ }
2023
+ ],
2024
+ "license": "MIT",
2025
+ "peer": true,
2026
+ "dependencies": {
2027
+ "nanoid": "^3.3.11",
2028
+ "picocolors": "^1.1.1",
2029
+ "source-map-js": "^1.2.1"
2030
+ },
2031
+ "engines": {
2032
+ "node": "^10 || ^12 || >=14"
2033
+ }
2034
+ },
2035
+ "node_modules/postcss-import": {
2036
+ "version": "15.1.0",
2037
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
2038
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
2039
+ "dev": true,
2040
+ "license": "MIT",
2041
+ "dependencies": {
2042
+ "postcss-value-parser": "^4.0.0",
2043
+ "read-cache": "^1.0.0",
2044
+ "resolve": "^1.1.7"
2045
+ },
2046
+ "engines": {
2047
+ "node": ">=14.0.0"
2048
+ },
2049
+ "peerDependencies": {
2050
+ "postcss": "^8.0.0"
2051
+ }
2052
+ },
2053
+ "node_modules/postcss-js": {
2054
+ "version": "4.1.0",
2055
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
2056
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
2057
+ "dev": true,
2058
+ "funding": [
2059
+ {
2060
+ "type": "opencollective",
2061
+ "url": "https://opencollective.com/postcss/"
2062
+ },
2063
+ {
2064
+ "type": "github",
2065
+ "url": "https://github.com/sponsors/ai"
2066
+ }
2067
+ ],
2068
+ "license": "MIT",
2069
+ "dependencies": {
2070
+ "camelcase-css": "^2.0.1"
2071
+ },
2072
+ "engines": {
2073
+ "node": "^12 || ^14 || >= 16"
2074
+ },
2075
+ "peerDependencies": {
2076
+ "postcss": "^8.4.21"
2077
+ }
2078
+ },
2079
+ "node_modules/postcss-load-config": {
2080
+ "version": "6.0.1",
2081
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
2082
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
2083
+ "dev": true,
2084
+ "funding": [
2085
+ {
2086
+ "type": "opencollective",
2087
+ "url": "https://opencollective.com/postcss/"
2088
+ },
2089
+ {
2090
+ "type": "github",
2091
+ "url": "https://github.com/sponsors/ai"
2092
+ }
2093
+ ],
2094
+ "license": "MIT",
2095
+ "dependencies": {
2096
+ "lilconfig": "^3.1.1"
2097
+ },
2098
+ "engines": {
2099
+ "node": ">= 18"
2100
+ },
2101
+ "peerDependencies": {
2102
+ "jiti": ">=1.21.0",
2103
+ "postcss": ">=8.0.9",
2104
+ "tsx": "^4.8.1",
2105
+ "yaml": "^2.4.2"
2106
+ },
2107
+ "peerDependenciesMeta": {
2108
+ "jiti": {
2109
+ "optional": true
2110
+ },
2111
+ "postcss": {
2112
+ "optional": true
2113
+ },
2114
+ "tsx": {
2115
+ "optional": true
2116
+ },
2117
+ "yaml": {
2118
+ "optional": true
2119
+ }
2120
+ }
2121
+ },
2122
+ "node_modules/postcss-nested": {
2123
+ "version": "6.2.0",
2124
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
2125
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
2126
+ "dev": true,
2127
+ "funding": [
2128
+ {
2129
+ "type": "opencollective",
2130
+ "url": "https://opencollective.com/postcss/"
2131
+ },
2132
+ {
2133
+ "type": "github",
2134
+ "url": "https://github.com/sponsors/ai"
2135
+ }
2136
+ ],
2137
+ "license": "MIT",
2138
+ "dependencies": {
2139
+ "postcss-selector-parser": "^6.1.1"
2140
+ },
2141
+ "engines": {
2142
+ "node": ">=12.0"
2143
+ },
2144
+ "peerDependencies": {
2145
+ "postcss": "^8.2.14"
2146
+ }
2147
+ },
2148
+ "node_modules/postcss-selector-parser": {
2149
+ "version": "6.1.2",
2150
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
2151
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
2152
+ "dev": true,
2153
+ "license": "MIT",
2154
+ "dependencies": {
2155
+ "cssesc": "^3.0.0",
2156
+ "util-deprecate": "^1.0.2"
2157
+ },
2158
+ "engines": {
2159
+ "node": ">=4"
2160
+ }
2161
+ },
2162
+ "node_modules/postcss-value-parser": {
2163
+ "version": "4.2.0",
2164
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
2165
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
2166
+ "dev": true,
2167
+ "license": "MIT"
2168
+ },
2169
+ "node_modules/prop-types": {
2170
+ "version": "15.8.1",
2171
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
2172
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
2173
+ "license": "MIT",
2174
+ "dependencies": {
2175
+ "loose-envify": "^1.4.0",
2176
+ "object-assign": "^4.1.1",
2177
+ "react-is": "^16.13.1"
2178
+ }
2179
+ },
2180
+ "node_modules/prop-types/node_modules/react-is": {
2181
+ "version": "16.13.1",
2182
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
2183
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
2184
+ "license": "MIT"
2185
+ },
2186
+ "node_modules/proxy-from-env": {
2187
+ "version": "2.1.0",
2188
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
2189
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
2190
+ "license": "MIT",
2191
+ "engines": {
2192
+ "node": ">=10"
2193
+ }
2194
+ },
2195
+ "node_modules/queue-microtask": {
2196
+ "version": "1.2.3",
2197
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
2198
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
2199
+ "dev": true,
2200
+ "funding": [
2201
+ {
2202
+ "type": "github",
2203
+ "url": "https://github.com/sponsors/feross"
2204
+ },
2205
+ {
2206
+ "type": "patreon",
2207
+ "url": "https://www.patreon.com/feross"
2208
+ },
2209
+ {
2210
+ "type": "consulting",
2211
+ "url": "https://feross.org/support"
2212
+ }
2213
+ ],
2214
+ "license": "MIT"
2215
+ },
2216
+ "node_modules/react": {
2217
+ "version": "19.2.0",
2218
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
2219
+ "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
2220
+ "license": "MIT",
2221
+ "peer": true,
2222
+ "engines": {
2223
+ "node": ">=0.10.0"
2224
+ }
2225
+ },
2226
+ "node_modules/react-dom": {
2227
+ "version": "19.2.0",
2228
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
2229
+ "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
2230
+ "license": "MIT",
2231
+ "peer": true,
2232
+ "dependencies": {
2233
+ "scheduler": "^0.27.0"
2234
+ },
2235
+ "peerDependencies": {
2236
+ "react": "^19.2.0"
2237
+ }
2238
+ },
2239
+ "node_modules/react-is": {
2240
+ "version": "18.3.1",
2241
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
2242
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
2243
+ "license": "MIT"
2244
+ },
2245
+ "node_modules/react-smooth": {
2246
+ "version": "4.0.4",
2247
+ "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
2248
+ "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
2249
+ "license": "MIT",
2250
+ "dependencies": {
2251
+ "fast-equals": "^5.0.1",
2252
+ "prop-types": "^15.8.1",
2253
+ "react-transition-group": "^4.4.5"
2254
+ },
2255
+ "peerDependencies": {
2256
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
2257
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2258
+ }
2259
+ },
2260
+ "node_modules/react-transition-group": {
2261
+ "version": "4.4.5",
2262
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
2263
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
2264
+ "license": "BSD-3-Clause",
2265
+ "dependencies": {
2266
+ "@babel/runtime": "^7.5.5",
2267
+ "dom-helpers": "^5.0.1",
2268
+ "loose-envify": "^1.4.0",
2269
+ "prop-types": "^15.6.2"
2270
+ },
2271
+ "peerDependencies": {
2272
+ "react": ">=16.6.0",
2273
+ "react-dom": ">=16.6.0"
2274
+ }
2275
+ },
2276
+ "node_modules/read-cache": {
2277
+ "version": "1.0.0",
2278
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
2279
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
2280
+ "dev": true,
2281
+ "license": "MIT",
2282
+ "dependencies": {
2283
+ "pify": "^2.3.0"
2284
+ }
2285
+ },
2286
+ "node_modules/readdirp": {
2287
+ "version": "3.6.0",
2288
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
2289
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
2290
+ "dev": true,
2291
+ "license": "MIT",
2292
+ "dependencies": {
2293
+ "picomatch": "^2.2.1"
2294
+ },
2295
+ "engines": {
2296
+ "node": ">=8.10.0"
2297
+ }
2298
+ },
2299
+ "node_modules/recharts": {
2300
+ "version": "2.15.4",
2301
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
2302
+ "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==",
2303
+ "license": "MIT",
2304
+ "dependencies": {
2305
+ "clsx": "^2.0.0",
2306
+ "eventemitter3": "^4.0.1",
2307
+ "lodash": "^4.17.21",
2308
+ "react-is": "^18.3.1",
2309
+ "react-smooth": "^4.0.4",
2310
+ "recharts-scale": "^0.4.4",
2311
+ "tiny-invariant": "^1.3.1",
2312
+ "victory-vendor": "^36.6.8"
2313
+ },
2314
+ "engines": {
2315
+ "node": ">=14"
2316
+ },
2317
+ "peerDependencies": {
2318
+ "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
2319
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2320
+ }
2321
+ },
2322
+ "node_modules/recharts-scale": {
2323
+ "version": "0.4.5",
2324
+ "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
2325
+ "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
2326
+ "license": "MIT",
2327
+ "dependencies": {
2328
+ "decimal.js-light": "^2.4.1"
2329
+ }
2330
+ },
2331
+ "node_modules/resolve": {
2332
+ "version": "1.22.12",
2333
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
2334
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
2335
+ "dev": true,
2336
+ "license": "MIT",
2337
+ "dependencies": {
2338
+ "es-errors": "^1.3.0",
2339
+ "is-core-module": "^2.16.1",
2340
+ "path-parse": "^1.0.7",
2341
+ "supports-preserve-symlinks-flag": "^1.0.0"
2342
+ },
2343
+ "bin": {
2344
+ "resolve": "bin/resolve"
2345
+ },
2346
+ "engines": {
2347
+ "node": ">= 0.4"
2348
+ },
2349
+ "funding": {
2350
+ "url": "https://github.com/sponsors/ljharb"
2351
+ }
2352
+ },
2353
+ "node_modules/reusify": {
2354
+ "version": "1.1.0",
2355
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
2356
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
2357
+ "dev": true,
2358
+ "license": "MIT",
2359
+ "engines": {
2360
+ "iojs": ">=1.0.0",
2361
+ "node": ">=0.10.0"
2362
+ }
2363
+ },
2364
+ "node_modules/run-parallel": {
2365
+ "version": "1.2.0",
2366
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
2367
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
2368
+ "dev": true,
2369
+ "funding": [
2370
+ {
2371
+ "type": "github",
2372
+ "url": "https://github.com/sponsors/feross"
2373
+ },
2374
+ {
2375
+ "type": "patreon",
2376
+ "url": "https://www.patreon.com/feross"
2377
+ },
2378
+ {
2379
+ "type": "consulting",
2380
+ "url": "https://feross.org/support"
2381
+ }
2382
+ ],
2383
+ "license": "MIT",
2384
+ "dependencies": {
2385
+ "queue-microtask": "^1.2.2"
2386
+ }
2387
+ },
2388
+ "node_modules/scheduler": {
2389
+ "version": "0.27.0",
2390
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
2391
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
2392
+ "license": "MIT"
2393
+ },
2394
+ "node_modules/semver": {
2395
+ "version": "7.7.4",
2396
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
2397
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
2398
+ "license": "ISC",
2399
+ "optional": true,
2400
+ "bin": {
2401
+ "semver": "bin/semver.js"
2402
+ },
2403
+ "engines": {
2404
+ "node": ">=10"
2405
+ }
2406
+ },
2407
+ "node_modules/sharp": {
2408
+ "version": "0.34.5",
2409
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
2410
+ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
2411
+ "hasInstallScript": true,
2412
+ "license": "Apache-2.0",
2413
+ "optional": true,
2414
+ "dependencies": {
2415
+ "@img/colour": "^1.0.0",
2416
+ "detect-libc": "^2.1.2",
2417
+ "semver": "^7.7.3"
2418
+ },
2419
+ "engines": {
2420
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
2421
+ },
2422
+ "funding": {
2423
+ "url": "https://opencollective.com/libvips"
2424
+ },
2425
+ "optionalDependencies": {
2426
+ "@img/sharp-darwin-arm64": "0.34.5",
2427
+ "@img/sharp-darwin-x64": "0.34.5",
2428
+ "@img/sharp-libvips-darwin-arm64": "1.2.4",
2429
+ "@img/sharp-libvips-darwin-x64": "1.2.4",
2430
+ "@img/sharp-libvips-linux-arm": "1.2.4",
2431
+ "@img/sharp-libvips-linux-arm64": "1.2.4",
2432
+ "@img/sharp-libvips-linux-ppc64": "1.2.4",
2433
+ "@img/sharp-libvips-linux-riscv64": "1.2.4",
2434
+ "@img/sharp-libvips-linux-s390x": "1.2.4",
2435
+ "@img/sharp-libvips-linux-x64": "1.2.4",
2436
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
2437
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
2438
+ "@img/sharp-linux-arm": "0.34.5",
2439
+ "@img/sharp-linux-arm64": "0.34.5",
2440
+ "@img/sharp-linux-ppc64": "0.34.5",
2441
+ "@img/sharp-linux-riscv64": "0.34.5",
2442
+ "@img/sharp-linux-s390x": "0.34.5",
2443
+ "@img/sharp-linux-x64": "0.34.5",
2444
+ "@img/sharp-linuxmusl-arm64": "0.34.5",
2445
+ "@img/sharp-linuxmusl-x64": "0.34.5",
2446
+ "@img/sharp-wasm32": "0.34.5",
2447
+ "@img/sharp-win32-arm64": "0.34.5",
2448
+ "@img/sharp-win32-ia32": "0.34.5",
2449
+ "@img/sharp-win32-x64": "0.34.5"
2450
+ }
2451
+ },
2452
+ "node_modules/source-map-js": {
2453
+ "version": "1.2.1",
2454
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
2455
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
2456
+ "license": "BSD-3-Clause",
2457
+ "engines": {
2458
+ "node": ">=0.10.0"
2459
+ }
2460
+ },
2461
+ "node_modules/styled-jsx": {
2462
+ "version": "5.1.6",
2463
+ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
2464
+ "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
2465
+ "license": "MIT",
2466
+ "dependencies": {
2467
+ "client-only": "0.0.1"
2468
+ },
2469
+ "engines": {
2470
+ "node": ">= 12.0.0"
2471
+ },
2472
+ "peerDependencies": {
2473
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
2474
+ },
2475
+ "peerDependenciesMeta": {
2476
+ "@babel/core": {
2477
+ "optional": true
2478
+ },
2479
+ "babel-plugin-macros": {
2480
+ "optional": true
2481
+ }
2482
+ }
2483
+ },
2484
+ "node_modules/sucrase": {
2485
+ "version": "3.35.1",
2486
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
2487
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
2488
+ "dev": true,
2489
+ "license": "MIT",
2490
+ "dependencies": {
2491
+ "@jridgewell/gen-mapping": "^0.3.2",
2492
+ "commander": "^4.0.0",
2493
+ "lines-and-columns": "^1.1.6",
2494
+ "mz": "^2.7.0",
2495
+ "pirates": "^4.0.1",
2496
+ "tinyglobby": "^0.2.11",
2497
+ "ts-interface-checker": "^0.1.9"
2498
+ },
2499
+ "bin": {
2500
+ "sucrase": "bin/sucrase",
2501
+ "sucrase-node": "bin/sucrase-node"
2502
+ },
2503
+ "engines": {
2504
+ "node": ">=16 || 14 >=14.17"
2505
+ }
2506
+ },
2507
+ "node_modules/supports-preserve-symlinks-flag": {
2508
+ "version": "1.0.0",
2509
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
2510
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
2511
+ "dev": true,
2512
+ "license": "MIT",
2513
+ "engines": {
2514
+ "node": ">= 0.4"
2515
+ },
2516
+ "funding": {
2517
+ "url": "https://github.com/sponsors/ljharb"
2518
+ }
2519
+ },
2520
+ "node_modules/tailwind-merge": {
2521
+ "version": "3.5.0",
2522
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
2523
+ "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==",
2524
+ "license": "MIT",
2525
+ "funding": {
2526
+ "type": "github",
2527
+ "url": "https://github.com/sponsors/dcastil"
2528
+ }
2529
+ },
2530
+ "node_modules/tailwindcss": {
2531
+ "version": "3.4.19",
2532
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
2533
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
2534
+ "dev": true,
2535
+ "license": "MIT",
2536
+ "dependencies": {
2537
+ "@alloc/quick-lru": "^5.2.0",
2538
+ "arg": "^5.0.2",
2539
+ "chokidar": "^3.6.0",
2540
+ "didyoumean": "^1.2.2",
2541
+ "dlv": "^1.1.3",
2542
+ "fast-glob": "^3.3.2",
2543
+ "glob-parent": "^6.0.2",
2544
+ "is-glob": "^4.0.3",
2545
+ "jiti": "^1.21.7",
2546
+ "lilconfig": "^3.1.3",
2547
+ "micromatch": "^4.0.8",
2548
+ "normalize-path": "^3.0.0",
2549
+ "object-hash": "^3.0.0",
2550
+ "picocolors": "^1.1.1",
2551
+ "postcss": "^8.4.47",
2552
+ "postcss-import": "^15.1.0",
2553
+ "postcss-js": "^4.0.1",
2554
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
2555
+ "postcss-nested": "^6.2.0",
2556
+ "postcss-selector-parser": "^6.1.2",
2557
+ "resolve": "^1.22.8",
2558
+ "sucrase": "^3.35.0"
2559
+ },
2560
+ "bin": {
2561
+ "tailwind": "lib/cli.js",
2562
+ "tailwindcss": "lib/cli.js"
2563
+ },
2564
+ "engines": {
2565
+ "node": ">=14.0.0"
2566
+ }
2567
+ },
2568
+ "node_modules/thenify": {
2569
+ "version": "3.3.1",
2570
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
2571
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
2572
+ "dev": true,
2573
+ "license": "MIT",
2574
+ "dependencies": {
2575
+ "any-promise": "^1.0.0"
2576
+ }
2577
+ },
2578
+ "node_modules/thenify-all": {
2579
+ "version": "1.6.0",
2580
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
2581
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
2582
+ "dev": true,
2583
+ "license": "MIT",
2584
+ "dependencies": {
2585
+ "thenify": ">= 3.1.0 < 4"
2586
+ },
2587
+ "engines": {
2588
+ "node": ">=0.8"
2589
+ }
2590
+ },
2591
+ "node_modules/tiny-invariant": {
2592
+ "version": "1.3.3",
2593
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
2594
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
2595
+ "license": "MIT"
2596
+ },
2597
+ "node_modules/tinyglobby": {
2598
+ "version": "0.2.16",
2599
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
2600
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
2601
+ "dev": true,
2602
+ "license": "MIT",
2603
+ "dependencies": {
2604
+ "fdir": "^6.5.0",
2605
+ "picomatch": "^4.0.4"
2606
+ },
2607
+ "engines": {
2608
+ "node": ">=12.0.0"
2609
+ },
2610
+ "funding": {
2611
+ "url": "https://github.com/sponsors/SuperchupuDev"
2612
+ }
2613
+ },
2614
+ "node_modules/tinyglobby/node_modules/fdir": {
2615
+ "version": "6.5.0",
2616
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
2617
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
2618
+ "dev": true,
2619
+ "license": "MIT",
2620
+ "engines": {
2621
+ "node": ">=12.0.0"
2622
+ },
2623
+ "peerDependencies": {
2624
+ "picomatch": "^3 || ^4"
2625
+ },
2626
+ "peerDependenciesMeta": {
2627
+ "picomatch": {
2628
+ "optional": true
2629
+ }
2630
+ }
2631
+ },
2632
+ "node_modules/tinyglobby/node_modules/picomatch": {
2633
+ "version": "4.0.4",
2634
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
2635
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
2636
+ "dev": true,
2637
+ "license": "MIT",
2638
+ "peer": true,
2639
+ "engines": {
2640
+ "node": ">=12"
2641
+ },
2642
+ "funding": {
2643
+ "url": "https://github.com/sponsors/jonschlinkert"
2644
+ }
2645
+ },
2646
+ "node_modules/to-regex-range": {
2647
+ "version": "5.0.1",
2648
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
2649
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
2650
+ "dev": true,
2651
+ "license": "MIT",
2652
+ "dependencies": {
2653
+ "is-number": "^7.0.0"
2654
+ },
2655
+ "engines": {
2656
+ "node": ">=8.0"
2657
+ }
2658
+ },
2659
+ "node_modules/ts-interface-checker": {
2660
+ "version": "0.1.13",
2661
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
2662
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
2663
+ "dev": true,
2664
+ "license": "Apache-2.0"
2665
+ },
2666
+ "node_modules/tslib": {
2667
+ "version": "2.8.1",
2668
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
2669
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
2670
+ "license": "0BSD"
2671
+ },
2672
+ "node_modules/typescript": {
2673
+ "version": "5.9.3",
2674
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
2675
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
2676
+ "dev": true,
2677
+ "license": "Apache-2.0",
2678
+ "bin": {
2679
+ "tsc": "bin/tsc",
2680
+ "tsserver": "bin/tsserver"
2681
+ },
2682
+ "engines": {
2683
+ "node": ">=14.17"
2684
+ }
2685
+ },
2686
+ "node_modules/undici-types": {
2687
+ "version": "6.21.0",
2688
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
2689
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
2690
+ "dev": true,
2691
+ "license": "MIT"
2692
+ },
2693
+ "node_modules/update-browserslist-db": {
2694
+ "version": "1.2.3",
2695
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
2696
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
2697
+ "dev": true,
2698
+ "funding": [
2699
+ {
2700
+ "type": "opencollective",
2701
+ "url": "https://opencollective.com/browserslist"
2702
+ },
2703
+ {
2704
+ "type": "tidelift",
2705
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
2706
+ },
2707
+ {
2708
+ "type": "github",
2709
+ "url": "https://github.com/sponsors/ai"
2710
+ }
2711
+ ],
2712
+ "license": "MIT",
2713
+ "dependencies": {
2714
+ "escalade": "^3.2.0",
2715
+ "picocolors": "^1.1.1"
2716
+ },
2717
+ "bin": {
2718
+ "update-browserslist-db": "cli.js"
2719
+ },
2720
+ "peerDependencies": {
2721
+ "browserslist": ">= 4.21.0"
2722
+ }
2723
+ },
2724
+ "node_modules/util-deprecate": {
2725
+ "version": "1.0.2",
2726
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
2727
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
2728
+ "dev": true,
2729
+ "license": "MIT"
2730
+ },
2731
+ "node_modules/victory-vendor": {
2732
+ "version": "36.9.2",
2733
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
2734
+ "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
2735
+ "license": "MIT AND ISC",
2736
+ "dependencies": {
2737
+ "@types/d3-array": "^3.0.3",
2738
+ "@types/d3-ease": "^3.0.0",
2739
+ "@types/d3-interpolate": "^3.0.1",
2740
+ "@types/d3-scale": "^4.0.2",
2741
+ "@types/d3-shape": "^3.1.0",
2742
+ "@types/d3-time": "^3.0.0",
2743
+ "@types/d3-timer": "^3.0.0",
2744
+ "d3-array": "^3.1.6",
2745
+ "d3-ease": "^3.0.1",
2746
+ "d3-interpolate": "^3.0.1",
2747
+ "d3-scale": "^4.0.2",
2748
+ "d3-shape": "^3.1.0",
2749
+ "d3-time": "^3.0.0",
2750
+ "d3-timer": "^3.0.1"
2751
+ }
2752
+ }
2753
+ }
2754
+ }
frontend/package.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "pdc-frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start"
9
+ },
10
+ "dependencies": {
11
+ "axios": "^1.8.4",
12
+ "clsx": "^2.1.1",
13
+ "lucide-react": "^1.8.0",
14
+ "next": "16.1.6",
15
+ "react": "19.2.0",
16
+ "react-dom": "19.2.0",
17
+ "recharts": "^2.15.4",
18
+ "tailwind-merge": "^3.5.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.13.14",
22
+ "@types/react": "^19.0.12",
23
+ "@types/react-dom": "^19.0.4",
24
+ "autoprefixer": "^10.4.27",
25
+ "postcss": "^8.5.9",
26
+ "tailwindcss": "^3.4.19",
27
+ "typescript": "^5.8.2"
28
+ }
29
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
frontend/tailwind.config.js ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: [
4
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
5
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
6
+ "./lib/**/*.{js,ts,jsx,tsx,mdx}",
7
+ ],
8
+ theme: {
9
+ extend: {
10
+ colors: {
11
+ border: "hsl(var(--border))",
12
+ input: "hsl(var(--input))",
13
+ ring: "hsl(var(--ring))",
14
+ background: "hsl(var(--background))",
15
+ foreground: "hsl(var(--foreground))",
16
+ primary: {
17
+ DEFAULT: "hsl(var(--primary))",
18
+ foreground: "hsl(var(--primary-foreground))",
19
+ },
20
+ secondary: {
21
+ DEFAULT: "hsl(var(--secondary))",
22
+ foreground: "hsl(var(--secondary-foreground))",
23
+ },
24
+ destructive: {
25
+ DEFAULT: "hsl(var(--destructive))",
26
+ foreground: "hsl(var(--destructive-foreground))",
27
+ },
28
+ muted: {
29
+ DEFAULT: "hsl(var(--muted))",
30
+ foreground: "hsl(var(--muted-foreground))",
31
+ },
32
+ accent: {
33
+ DEFAULT: "hsl(var(--accent))",
34
+ foreground: "hsl(var(--accent-foreground))",
35
+ },
36
+ popover: {
37
+ DEFAULT: "hsl(var(--popover))",
38
+ foreground: "hsl(var(--popover-foreground))",
39
+ },
40
+ card: {
41
+ DEFAULT: "hsl(var(--card))",
42
+ foreground: "hsl(var(--card-foreground))",
43
+ },
44
+ },
45
+ borderRadius: {
46
+ lg: "var(--radius)",
47
+ md: "calc(var(--radius) - 2px)",
48
+ sm: "calc(var(--radius) - 4px)",
49
+ },
50
+ },
51
+ },
52
+ plugins: [],
53
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "esnext"
8
+ ],
9
+ "allowJs": true,
10
+ "skipLibCheck": true,
11
+ "strict": true,
12
+ "noEmit": true,
13
+ "esModuleInterop": true,
14
+ "module": "esnext",
15
+ "moduleResolution": "bundler",
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true,
18
+ "jsx": "react-jsx",
19
+ "incremental": true,
20
+ "plugins": [
21
+ {
22
+ "name": "next"
23
+ }
24
+ ],
25
+ "paths": {
26
+ "@/*": [
27
+ "./*"
28
+ ]
29
+ }
30
+ },
31
+ "include": [
32
+ "next-env.d.ts",
33
+ "**/*.ts",
34
+ "**/*.tsx",
35
+ ".next/types/**/*.ts",
36
+ ".next/dev/types/**/*.ts"
37
+ ],
38
+ "exclude": [
39
+ "node_modules"
40
+ ]
41
+ }
frontend/tsconfig.tsbuildinfo ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi==0.135.1
2
+ uvicorn[standard]==0.41.0
3
+ pandas==3.0.1
4
+ numpy==2.4.2
5
+ openpyxl==3.1.5
6
+ python-multipart==0.0.22
run_app.sh ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ BACKEND_DIR="$ROOT_DIR/backend"
6
+ FRONTEND_DIR="$ROOT_DIR/frontend"
7
+
8
+ echo "[PDC] Starting full application"
9
+
10
+ if [ ! -d "$BACKEND_DIR" ] || [ ! -d "$FRONTEND_DIR" ]; then
11
+ echo "[PDC] ERROR: backend or frontend directory missing"
12
+ exit 1
13
+ fi
14
+
15
+ if [ ! -d "$BACKEND_DIR/.venv" ]; then
16
+ echo "[PDC] Creating backend virtual environment"
17
+ python3 -m venv "$BACKEND_DIR/.venv"
18
+ fi
19
+
20
+ echo "[PDC] Installing backend dependencies"
21
+ "$BACKEND_DIR/.venv/bin/pip" install -q -r "$BACKEND_DIR/requirements.txt"
22
+
23
+ echo "[PDC] Running backend test suite"
24
+ "$BACKEND_DIR/.venv/bin/python" -m pytest -q "$BACKEND_DIR/tests"
25
+
26
+ echo "[PDC] Installing frontend dependencies"
27
+ cd "$FRONTEND_DIR"
28
+ npm install
29
+
30
+ cleanup() {
31
+ echo "[PDC] Stopping services"
32
+ if [ -n "${BACKEND_PID:-}" ] && kill -0 "$BACKEND_PID" 2>/dev/null; then
33
+ kill "$BACKEND_PID" || true
34
+ fi
35
+ if [ -n "${FRONTEND_PID:-}" ] && kill -0 "$FRONTEND_PID" 2>/dev/null; then
36
+ kill "$FRONTEND_PID" || true
37
+ fi
38
+ }
39
+
40
+ trap cleanup EXIT INT TERM
41
+
42
+ echo "[PDC] Launching backend on http://localhost:8000"
43
+ cd "$BACKEND_DIR"
44
+ "$BACKEND_DIR/.venv/bin/uvicorn" app.main:app --host 0.0.0.0 --port 8000 --reload &
45
+ BACKEND_PID=$!
46
+
47
+ echo "[PDC] Launching frontend on http://localhost:3000"
48
+ cd "$FRONTEND_DIR"
49
+ npm run dev -- --port 3000 &
50
+ FRONTEND_PID=$!
51
+
52
+ echo "[PDC] Application is running"
53
+ echo "[PDC] Frontend: http://localhost:3000"
54
+ echo "[PDC] Backend: http://localhost:8000"
55
+
56
+ wait "$BACKEND_PID" "$FRONTEND_PID"