benroshan Claude Sonnet 4.6 commited on
Commit
65cee4e
·
1 Parent(s): 5be0545

feat: Stage 8 — eval dashboard, mandatory web search, docs update

Browse files

- eval-dashboard/: standalone Vite+React static site (own Vercel project)
MetricCard, EvolutionChart (Recharts), RunTable, LatencyStats
reads versioned JSON runs from public/data/runs/ + index.json
- scripts/run_eval_versioned.py: offline eval script
metrics: answer_correctness, answer_relevancy, context_recall, precision@5, latency p50/p95/p99
writes versioned JSON + updates index.json
- data/ground_truth/eval_pairs.json: expanded 20 → 50 pairs
added multi-hop, comparative, negative, numeric, edge-case questions
- frontend: removed per-message faithfulness badge (MessageBubble.jsx)
removed web search toggle — hardcoded always-on (ChatArea.jsx)
- server/routes/chat.py: removed score_faithfulness() call; web_search default True
- docs: architecture, decisions, api-spec, evolution, structure updated for Stage 8
- .env.example: updated comments; LangSmith listed as optional

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

.env.example CHANGED
@@ -1,8 +1,13 @@
1
- # LLM inference (chat, faithfulness eval, RAGAS eval)
2
  GROQ_API_KEY=gsk_your_groq_key_here
3
 
4
  # Embeddings (ingest + retrieval) — Groq has no embeddings endpoint
5
  EURON_API_KEY=your_euron_key_here
6
 
7
- # Web search (optional enables web search toggle in chat)
8
  TAVILY_API_KEY=tvly-your_tavily_key_here
 
 
 
 
 
 
1
+ # LLM inference (chat + answer generation)
2
  GROQ_API_KEY=gsk_your_groq_key_here
3
 
4
  # Embeddings (ingest + retrieval) — Groq has no embeddings endpoint
5
  EURON_API_KEY=your_euron_key_here
6
 
7
+ # Web search — mandatory, always on
8
  TAVILY_API_KEY=tvly-your_tavily_key_here
9
+
10
+ # Optional — LangSmith tracing (set both or neither)
11
+ # LANGCHAIN_API_KEY=lsv2_your_key_here
12
+ # LANGCHAIN_TRACING_V2=true
13
+ # LANGCHAIN_PROJECT=prism
README.md CHANGED
@@ -1,19 +1,19 @@
1
- # FinRAGFintech Research Agent with Self-Scoring Retrieval
2
 
3
  **Live demo:** https://fin-rag-git-main-benroshan100s-projects.vercel.app/
4
 
5
- A conversational RAG system for fintech documents (RBI circulars, NPCI reports, earnings transcripts) that **scores its own retrieval quality** per query. Most RAG apps ship without telling you when retrieval is silently failing. FinRAG surfaces that signal.
6
 
7
  ---
8
 
9
  ## Why This Exists
10
 
11
- Fintech analysts spend hours manually reading policy circulars, earnings calls, and regulator reports to answer domain questions. Existing RAG systems retrieve context and generate plausible answers but give you no signal on whether the retrieval actually worked. When embeddings drift, when chunk boundaries split critical context, or when the top-K misses the right passage, the LLM still produces a confident-sounding answer. It fails silently.
12
 
13
- FinRAG adds an **eval layer on top of the chat interface**:
14
- - **Faithfulness score** (LLM-as-Judge) on every answer — green/yellow/red badge inline
15
- - **Precision@K** against a ground-truth set of fintech queries
16
- - **Retrieval health dashboard** — traffic light based on rolling faithfulness
17
 
18
  If retrieval degrades, you see it before the user does.
19
 
@@ -21,41 +21,42 @@ If retrieval degrades, you see it before the user does.
21
 
22
  ## What It Does
23
 
24
- - Upload PDFs, TXTs, or CSVs via drag-and-drop
25
  - Ask multi-turn questions with conversation memory (last 10 turns)
26
- - Get answers with **inline source citations** (filename, page, similarity score)
27
- - See a **faithfulness badge** on every answer (1-5 scale, green/yellow/red)
28
- - Run **batch Precision@K** against 20 pre-built fintech eval queries
29
- - Watch a **retrieval health indicator** that turns red when quality drops
30
 
31
  ---
32
 
33
  ## Architecture
34
 
35
  ```
36
- ┌──────────────┐ ┌─────────────────────────────────────┐
37
- │ React UI │ │ FastAPI Backend
38
- (Vercel) ─HTTP─▶│ ┌───────────────────────────────┐ │
39
- └──────────────┘ │ │ Upload → Chunk → Embed │ │
40
- │ (Euron API embeddings) │ │
41
- │ └──────────────────────────────┘ │
42
-
43
- ┌───────────────────────────────┐
44
- │ ChromaDB (on-disk vectors)
45
- └──────────────┬────────────────┘
46
-
47
- ┌───────────────────────────────┐
48
- │ Retrieve top-K + score
49
- ConversationalRetrievalChain │ │
50
- │ │ LLM: Groq (llama-3.3-70b) │ │
51
- └──────────────┬────────────────┘
52
-
53
- ┌───────────────────────────────┐
54
- │ Faithfulness scorer
55
- │ (LLM-as-Judge, 1-5)
56
- └───────────────────────────────┘
57
- (Render Docker, free tier)
58
- └─────────────────────────────────────┘
 
59
  ```
60
 
61
  ---
@@ -65,71 +66,44 @@ If retrieval degrades, you see it before the user does.
65
  | Layer | Tech |
66
  |---|---|
67
  | **Backend** | FastAPI + Uvicorn |
68
- | **Vector store** | ChromaDB (persistent, on-disk) |
69
- | **Embeddings** | Euron API (`text-embedding-3-small`) — API-based to fit Render free tier |
70
- | **LLM** | Groq (`llama-3.3-70b-versatile`) via `langchain-groq` |
 
 
 
71
  | **Orchestration** | LangChain `ConversationalRetrievalChain` |
72
  | **Memory** | `ConversationBufferWindowMemory` (k=10 turns) |
 
 
 
73
  | **Frontend** | React 19 + Vite + Tailwind CSS v4 |
74
- | **Deployment** | Backend on Render (Docker), Frontend on Vercel |
75
 
76
  ---
77
 
78
- ## Key Features (The Eval Layer)
79
 
80
- ### 1. Faithfulness Scoring
81
- Every answer is passed back to the LLM with an eval prompt that scores 1-5 how well the answer is grounded in the retrieved chunks. The frontend renders a colored badge inline:
82
- - 🟢 **Faithful** (4-5/5)
83
- - 🟡 **Moderate** (3/5)
84
- - 🔴 **Low** (1-2/5)
85
 
86
- ### 2. Precision@K Benchmarking
87
- 20 ground-truth query/source pairs covering UPI, banking, lending, and RBI policy. The Eval Dashboard runs them all and reports mean Precision@5 plus per-query breakdown.
88
 
89
- ### 3. Chunk Size Benchmarking
90
- `scripts/benchmark_chunks.py` wipes the index and re-ingests at different chunk sizes (200/300/500/750/1000), runs Precision@K at each, and produces a PNG chart showing which chunk size works best for your corpus.
 
 
 
 
91
 
92
- ### 4. Retrieval Health Dashboard
93
- Traffic-light indicator based on rolling faithfulness scores. Red = retrieval is degrading, yellow = mixed, green = healthy.
94
 
95
- ---
96
-
97
- ## Repository Structure
98
-
99
- ```
100
- finrag/
101
- ├── server/ # FastAPI backend
102
- │ ├── main.py # App entrypoint, CORS, lifespan
103
- │ ├── routes/ # chat.py, eval.py, upload.py
104
- │ ├── ingest.py # Load → chunk → embed → store (idempotent)
105
- │ ├── retriever.py # Query ChromaDB, return top-K + scores
106
- │ ├── chain.py # ConversationalRetrievalChain assembly
107
- │ ├── memory.py # Conversation memory management
108
- │ ├── eval/
109
- │ │ ├── precision.py # Precision@K computation
110
- │ │ └── faithfulness.py # LLM-as-Judge scorer
111
- │ └── utils.py
112
-
113
- ├── frontend/ # React 19 + Vite + Tailwind
114
- │ └── src/
115
- │ ├── App.jsx
116
- │ ├── api.js
117
- │ └── components/ # ChatTab, EvalDashboard, MessageBubble, SourceExpander
118
-
119
- ├── scripts/
120
- │ ├── run_ingest.py # CLI: ingest documents
121
- │ ├── run_eval.py # CLI: batch Precision@K
122
- │ └── benchmark_chunks.py # CLI: benchmark across chunk sizes
123
-
124
- ├── data/ground_truth/
125
- │ └── eval_pairs.json # 20 query/source pairs for Precision@K
126
-
127
- ├── sample_data/ # Sample fintech documents
128
- ├── config.yaml # Chunking, retrieval, eval params
129
- ├── Dockerfile # Backend-only (frontend deploys to Vercel)
130
- ├── render.yaml # Render Blueprint
131
- └── requirements.txt
132
- ```
133
 
134
  ---
135
 
@@ -138,32 +112,28 @@ finrag/
138
  ### Prerequisites
139
  - Python 3.11+
140
  - Node.js 20+
141
- - A Groq API key (https://console.groq.com) — free tier works
142
- - A Euron API key (https://euron.one) — used for embeddings only
143
 
144
  ### Backend
145
 
146
  ```bash
147
- # Clone
148
- git clone https://github.com/BenRoshan100/fin-rag.git
149
- cd fin-rag
150
 
151
- # Python deps
152
  python -m venv venv
153
- venv\Scripts\activate # Windows
154
- # source venv/bin/activate # macOS/Linux
 
 
155
  pip install -r requirements.txt
156
 
157
- # Env vars
158
  cp .env.example .env
159
- # Then fill in GROQ_API_KEY and EURON_API_KEY in .env
160
 
161
- # Ingest sample documents
162
  python scripts/run_ingest.py --data-dir sample_data
163
-
164
- # Start backend
165
  uvicorn server.main:app --reload
166
- # Backend now at http://localhost:8000
167
  ```
168
 
169
  ### Frontend
@@ -175,91 +145,91 @@ npm run dev
175
  # Frontend at http://localhost:5173
176
  ```
177
 
178
- Open http://localhost:5173 and ask a question.
179
 
180
- ---
181
-
182
- ## Running the Eval Suite
183
-
184
- ### Batch Precision@K
185
  ```bash
186
- python scripts/run_eval.py
 
187
  ```
188
- Runs all 20 ground-truth queries and prints mean Precision@5 plus per-query scores. Saves results to `eval_results_<timestamp>.json`.
189
-
190
- ### Chunk size benchmark
191
- ```bash
192
- python scripts/benchmark_chunks.py --data-dir sample_data
193
- ```
194
- Re-ingests at chunk sizes 200/300/500/750/1000, runs Precision@K at each, and saves a comparison chart as `benchmark_precision_<timestamp>.png`. Use this to pick the optimal chunk size for your documents.
195
 
196
  ---
197
 
198
  ## Deployment
199
 
200
- The app is split across two free-tier platforms:
201
-
202
  | Service | Platform | Notes |
203
  |---|---|---|
204
- | Backend | Render (Docker) | Free tier, 512MB RAM. API-based embeddings keep it within the limit. |
205
  | Frontend | Vercel | Free tier, auto-deploys from `main` branch. |
206
 
207
  ### Backend on Render
208
  1. Push to GitHub
209
  2. Render → New Web Service → connect repo (runtime: Docker)
210
- 3. Set env vars: `GROQ_API_KEY` (LLM) and `EURON_API_KEY` (embeddings)
211
  4. Deploy
212
 
213
  ### Frontend on Vercel
214
  1. Vercel → Import repo
215
  2. Root Directory: `frontend`
216
- 3. Framework: Vite (auto-detected)
217
- 4. Set env var: `VITE_API_URL=https://<your-backend>.onrender.com/api`
218
- 5. Deploy
219
-
220
- CORS is configured with a regex that accepts all `*.vercel.app` origins, so preview deployments work automatically.
221
-
222
- ### Why API-based embeddings?
223
- The original design used `sentence-transformers/all-MiniLM-L6-v2` for local embeddings. That loads a ~400MB PyTorch model into RAM, which **crashes Render's 512MB free tier on startup**. Swapping to API-based embeddings (Euron's OpenAI-compatible endpoint) drops backend memory to ~150MB and keeps everything free.
224
 
225
  ---
226
 
227
- ## Configuration
228
-
229
- Edit [`config.yaml`](config.yaml):
230
-
231
- ```yaml
232
- chunking:
233
- chunk_size: 500
234
- chunk_overlap: 50
235
-
236
- retrieval:
237
- k: 5
238
- collection_name: "finrag"
239
-
240
- memory:
241
- max_token_limit: 2000
242
-
243
- llm:
244
- model: "gpt-4.1-mini"
245
- base_url: "https://api.euron.one/api/v1/euri"
246
- max_tokens: 1000
247
- temperature: 0.1
248
 
249
- eval:
250
- ground_truth_path: "data/ground_truth/eval_pairs.json"
251
- precision_k: 5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  ```
253
 
254
  ---
255
 
256
  ## Design Decisions Worth Noting
257
 
258
- - **Idempotent ingestion** — documents get hashed to deterministic IDs. Re-running ingestion doesn't duplicate chunks.
259
- - **Content-hash chunk IDs** — `md5(source + page + text)` means the same chunk always gets the same ID, even across re-runs.
260
- - **CORS regex over exact match** — Vercel generates a unique preview URL per deploy. A regex match on `*.vercel.app` handles all of them without needing to update env vars.
261
- - **Lifespan events over startup events** — FastAPI's modern lifespan context manager initializes the chain once at boot and attaches it to `app.state`.
262
- - **API embeddings over local** — traded a local model for an API call. Makes each upload slightly slower, but the backend fits in free-tier RAM.
 
263
 
264
  ---
265
 
@@ -267,4 +237,4 @@ eval:
267
 
268
  **Ben Roshan D** — [github.com/BenRoshan100](https://github.com/BenRoshan100)
269
 
270
- Built as a portfolio project demonstrating production RAG with observability. The eval layer is the differentiator — most RAG portfolios skip it.
 
1
+ # PrismDocument Intelligence with Self-Scoring Retrieval
2
 
3
  **Live demo:** https://fin-rag-git-main-benroshan100s-projects.vercel.app/
4
 
5
+ Load any documents or URLs Prism becomes an instant expert on that corpus. Ask multi-turn questions, get cited answers, and see retrieval quality scored on every response. Most RAG apps fail silently when retrieval breaks. Prism surfaces that signal.
6
 
7
  ---
8
 
9
  ## Why This Exists
10
 
11
+ Knowledge workers analysts, researchers, lawyers, ops teams spend hours manually reading documents to answer domain questions. Existing RAG systems retrieve context and generate plausible answers but give no signal on whether retrieval actually worked. When embeddings drift, when chunk boundaries split critical context, or when top-K misses the right passage, the LLM still produces a confident-sounding answer. It fails silently.
12
 
13
+ Prism adds an **eval layer on top of the chat interface**:
14
+ - **Faithfulness score** (LLM-as-Judge) on every answer — inline badge per message
15
+ - **RAGAS benchmark** (4 metrics: faithfulness, answer_relevancy, context_precision, context_recall) — pre-computed, shown on Eval tab
16
+ - **Retrieval health dashboard** — rolling faithfulness traffic light
17
 
18
  If retrieval degrades, you see it before the user does.
19
 
 
21
 
22
  ## What It Does
23
 
24
+ - Upload PDFs, TXTs, or CSVs via drag-and-drop; ingest URLs directly
25
  - Ask multi-turn questions with conversation memory (last 10 turns)
26
+ - Get answers with **inline source citations** (filename, page, similarity score, BM25 score, rerank score)
27
+ - See a **faithfulness badge** on every answer (15 scale, colour-coded)
28
+ - Switch between **isolated workspaces** each workspace has its own document set
29
+ - View **RAGAS benchmark scores** and per-turn faithfulness log on the Eval tab
30
 
31
  ---
32
 
33
  ## Architecture
34
 
35
  ```
36
+ ┌──────────────────────────────────────────────────────────
37
+ │ React 19 + Vite + Tailwind (Vercel)
38
+ Workspace switcher | Chat | Eval | Upload
39
+ └────────────────────────┬─────────────────────────────────
40
+ HTTP
41
+ ─────────────────────────────────────────────────────────┐
42
+ FastAPI Backend (Render — Docker, 512MB)
43
+
44
+ Upload / URL ingest
45
+ → ParentDocumentRetriever (child 200-char / parent 800)
46
+ → Euron API embeddings (text-embedding-3-small)
47
+ → ChromaDB collection per workspace
48
+ BM25 index per workspace
49
+
50
+ Query
51
+ → HybridRetriever (BM25 0.3 + dense 0.7 → RRF)
52
+ → CrossEncoder rerank top-10 → top-5 (TinyBERT ~17MB)
53
+ → ConversationalRetrievalChain
54
+ Groq llama-3.3-70b-versatile
55
+ LLM-as-Judge faithfulness score (15)
56
+
57
+ Web search path: Tavily advanced → condense → synthesise
58
+ │ Observability: LangSmith traces all LLM + retrieval │
59
+ └──────────────────────────────────────────────────────────┘
60
  ```
61
 
62
  ---
 
66
  | Layer | Tech |
67
  |---|---|
68
  | **Backend** | FastAPI + Uvicorn |
69
+ | **Vector store** | ChromaDB (persistent, per-workspace collection) |
70
+ | **Sparse retrieval** | rank_bm25 (BM25Okapi) |
71
+ | **Hybrid fusion** | Weighted RRF (dense 0.7 + sparse 0.3) |
72
+ | **Reranker** | cross-encoder/ms-marco-TinyBERT-L-2-v2 (~17MB) |
73
+ | **Embeddings** | Euron API `text-embedding-3-small` — API-based to fit Render 512MB |
74
+ | **LLM** | Groq `llama-3.3-70b-versatile` via `langchain-groq` |
75
  | **Orchestration** | LangChain `ConversationalRetrievalChain` |
76
  | **Memory** | `ConversationBufferWindowMemory` (k=10 turns) |
77
+ | **Web search** | Tavily (advanced depth, 800-char truncation) |
78
+ | **Eval** | RAGAS (pre-computed JSON) + LLM-as-Judge per turn |
79
+ | **Observability** | LangSmith |
80
  | **Frontend** | React 19 + Vite + Tailwind CSS v4 |
81
+ | **Deployment** | Render (Docker backend) + Vercel (frontend) |
82
 
83
  ---
84
 
85
+ ## Eval Layer
86
 
87
+ ### Per-turn Faithfulness
88
+ Every answer is scored 15 by an LLM judge against the retrieved chunks. Frontend renders a colour badge inline:
89
+ - **Green** — Faithful (45 / 5)
90
+ - **Yellow** — Moderate (3 / 5)
91
+ - **Red** — Low (12 / 5)
92
 
93
+ ### RAGAS Benchmark
94
+ Four metrics evaluated against a 20-pair ground-truth set, run locally via `scripts/run_ragas_local.py` and committed as a static JSON. Dashboard reads from the file no live eval latency.
95
 
96
+ | Metric | Score |
97
+ |--------|-------|
98
+ | faithfulness | 1.0 |
99
+ | answer_relevancy | 0.90 |
100
+ | context_precision | TBD |
101
+ | context_recall | TBD |
102
 
103
+ > Note: faithfulness 1.0 is directional — eval queries are matched to the demo corpus. Run on held-out queries for honest numbers.
 
104
 
105
+ ### Precision@K
106
+ Batch Precision@K against 20 pre-built eval queries via `scripts/run_eval.py`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  ---
109
 
 
112
  ### Prerequisites
113
  - Python 3.11+
114
  - Node.js 20+
115
+ - [Groq API key](https://console.groq.com) — free tier works
116
+ - [Euron API key](https://euron.one) — used for embeddings only
117
 
118
  ### Backend
119
 
120
  ```bash
121
+ git clone https://github.com/BenRoshan100/Prism.git
122
+ cd Prism
 
123
 
 
124
  python -m venv venv
125
+ venv\Scripts\activate # Windows
126
+ # source venv/bin/activate # macOS/Linux
127
+
128
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
129
  pip install -r requirements.txt
130
 
 
131
  cp .env.example .env
132
+ # Fill in GROQ_API_KEY and EURON_API_KEY
133
 
 
134
  python scripts/run_ingest.py --data-dir sample_data
 
 
135
  uvicorn server.main:app --reload
136
+ # Backend at http://localhost:8000
137
  ```
138
 
139
  ### Frontend
 
145
  # Frontend at http://localhost:5173
146
  ```
147
 
148
+ ### Run RAGAS eval locally
149
 
 
 
 
 
 
150
  ```bash
151
+ python scripts/run_ragas_local.py --n 10
152
+ # Writes frontend/src/data/ragas_benchmark.json
153
  ```
 
 
 
 
 
 
 
154
 
155
  ---
156
 
157
  ## Deployment
158
 
 
 
159
  | Service | Platform | Notes |
160
  |---|---|---|
161
+ | Backend | Render (Docker) | 512MB RAM free tier. CPU-only torch + TinyBERT reranker keeps it within limit. |
162
  | Frontend | Vercel | Free tier, auto-deploys from `main` branch. |
163
 
164
  ### Backend on Render
165
  1. Push to GitHub
166
  2. Render → New Web Service → connect repo (runtime: Docker)
167
+ 3. Set env vars: `GROQ_API_KEY`, `EURON_API_KEY`, `TAVILY_API_KEY`, `LANGCHAIN_API_KEY`
168
  4. Deploy
169
 
170
  ### Frontend on Vercel
171
  1. Vercel → Import repo
172
  2. Root Directory: `frontend`
173
+ 3. Set env var: `VITE_API_URL=https://<your-backend>.onrender.com/api`
174
+ 4. Deploy
 
 
 
 
 
 
175
 
176
  ---
177
 
178
+ ## Repository Structure
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
+ ```
181
+ prism/
182
+ ├── server/
183
+ │ ├── main.py # FastAPI app, lifespan startup
184
+ │ ├── ingest.py # Load → ParentDocumentRetriever → embed → store
185
+ │ ├── retriever.py # HybridRetriever: dense + BM25 + RRF + reranker (cached per workspace)
186
+ │ ├── bm25_index.py # BM25 singleton
187
+ │ ├── reranker.py # CrossEncoder singleton (TinyBERT-L-2-v2)
188
+ │ ├── chain.py # ConversationalRetrievalChain + web query path
189
+ │ ├── web_search.py # Tavily search with content truncation
190
+ │ ├── url_loader.py # URL ingestion with size guard
191
+ │ ├── memory.py # ConversationBufferWindowMemory
192
+ │ ├── utils.py # Config, logger, token counter
193
+ │ └── routes/
194
+ │ ├── chat.py # POST /api/chat, DELETE /api/chat/memory
195
+ │ ├── upload.py # POST /api/upload
196
+ │ ├── eval.py # GET /api/eval/session, POST /api/eval/precision
197
+ │ └── workspaces.py # Workspace CRUD
198
+
199
+ ├── frontend/src/
200
+ │ ├── App.jsx # Workspace switcher + tab nav
201
+ │ ├── api.js # Axios client
202
+ │ └── components/
203
+ │ ├── Sidebar.jsx # Workspace list + doc list
204
+ │ ├── ChatArea.jsx # Chat UI (remounts on workspace switch)
205
+ │ ├── MessageBubble.jsx # Answer + faithfulness badge + web sources
206
+ │ ├── EvalPanel.jsx # RAGAS scorecard + session log
207
+ │ └── FileUpload.jsx # Drag-and-drop upload
208
+
209
+ ├── scripts/
210
+ │ ├── run_ingest.py # CLI ingestion
211
+ │ ├── run_eval.py # CLI Precision@K
212
+ │ └── run_ragas_local.py # Local RAGAS eval → writes ragas_benchmark.json
213
+
214
+ ├── data/ground_truth/
215
+ │ └── eval_pairs.json # 20 query/chunk pairs with ground_truth answers
216
+
217
+ ├── sample_data/ # Demo documents
218
+ ├── config.yaml # All tunable params
219
+ ├── Dockerfile
220
+ └── requirements.txt
221
  ```
222
 
223
  ---
224
 
225
  ## Design Decisions Worth Noting
226
 
227
+ - **Singleton retriever cache per workspace** — without cache, every request rebuilt the Chroma instance (full embedding reload) → OOM after 2–3 queries. Cache invalidated after ingest.
228
+ - **CPU-only torch in Dockerfile** — sentence-transformers pulls CUDA torch (~2GB) by default, OOMing Render 512MB before uvicorn binds port. Pre-installing CPU torch (~200MB) is mandatory.
229
+ - **RAGAS pre-computed locally** — `nest_asyncio` cannot patch `uvloop` (uvicorn's event loop on Linux). Live RAGAS eval on Render always 500s. Run locally, commit JSON, Vercel reads file.
230
+ - **TinyBERT-L-2-v2 reranker** — MiniLM-L-6-v2 (~85MB) + base memory exceeded 512MB on web queries. TinyBERT (~17MB) saves 68MB permanently.
231
+ - **Web search bypasses chain** — `ConversationalRetrievalChain` condensation step strips prepended Tavily context before LLM sees it. Web path uses direct LLM call with chat history.
232
+ - **Idempotent ingestion** — chunk IDs are `md5(source + page + text)`. Re-ingesting same doc does not duplicate chunks.
233
 
234
  ---
235
 
 
237
 
238
  **Ben Roshan D** — [github.com/BenRoshan100](https://github.com/BenRoshan100)
239
 
240
+ Built as a portfolio project demonstrating production RAG with a full evaluation layer. The retrieval scoring and RAGAS integration are the differentiators — most RAG portfolios skip eval entirely.
data/ground_truth/eval_pairs.json CHANGED
@@ -118,5 +118,191 @@
118
  "ground_truth": "Bajaj Housing Finance's AUM stood at Rs 72,400 crore as of Q3 FY2024. The company filed its Draft Red Herring Prospectus (DRHP) for an IPO and listing on stock exchanges, subject to regulatory approvals.",
119
  "relevant_sources": ["bajaj_finance_q3_2024_transcript.txt"],
120
  "relevant_chunk_keywords": ["Bajaj Housing Finance", "72,400 crore", "IPO", "DRHP", "listing"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  }
122
  ]
 
118
  "ground_truth": "Bajaj Housing Finance's AUM stood at Rs 72,400 crore as of Q3 FY2024. The company filed its Draft Red Herring Prospectus (DRHP) for an IPO and listing on stock exchanges, subject to regulatory approvals.",
119
  "relevant_sources": ["bajaj_finance_q3_2024_transcript.txt"],
120
  "relevant_chunk_keywords": ["Bajaj Housing Finance", "72,400 crore", "IPO", "DRHP", "listing"]
121
+ },
122
+ {
123
+ "query": "How did UPI transaction volume growth compare between FY2023 and FY2024?",
124
+ "ground_truth": "UPI grew 56% in volume and 43% in value in FY2024 compared to FY2023. In FY2023, UPI processed approximately 84 billion transactions. Monthly run-rate reached 14.04 billion in March 2024 versus 8.9 billion in March 2023. Growth decelerated slightly from FY2023's 80%+ pace but remained strong.",
125
+ "relevant_sources": ["npci_upi_report_2024.txt"],
126
+ "relevant_chunk_keywords": ["56%", "43%", "FY2023", "FY2024", "84 billion", "growth"]
127
+ },
128
+ {
129
+ "query": "What restrictions does RBI impose on digital lending service providers regarding loan disbursement?",
130
+ "ground_truth": "RBI mandates that all digital loan disbursals must flow directly to the borrower's bank account — not through any intermediary or pass-through account of the Lending Service Provider (LSP). Similarly, all repayments must be made directly to the regulated entity (bank or NBFC). LSPs are prohibited from holding or pooling borrower funds at any stage.",
131
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
132
+ "relevant_chunk_keywords": ["disbursement", "borrower bank account", "LSP", "intermediary", "repayment", "prohibited"]
133
+ },
134
+ {
135
+ "query": "How did Bajaj Finance's net interest income change year-on-year in Q3 FY2024?",
136
+ "ground_truth": "Bajaj Finance's Net Interest Income (NII) grew 28% year-on-year in Q3 FY2024, driven by strong AUM expansion and stable net interest margins. NIM remained in the range of 9.8–10.2%, supported by a diversified borrowing mix and favorable repricing of the loan book.",
137
+ "relevant_sources": ["bajaj_finance_q3_2024_transcript.txt"],
138
+ "relevant_chunk_keywords": ["net interest income", "NII", "28%", "NIM", "net interest margin"]
139
+ },
140
+ {
141
+ "query": "What did RBI say about the withdrawal of Rs 2000 denomination notes?",
142
+ "ground_truth": "RBI announced the withdrawal of Rs 2000 denomination banknotes from circulation in May 2023. Citizens were given until September 2023 to deposit or exchange these notes. Over 97% of the Rs 2000 notes in circulation were returned to the banking system by March 2024, reflecting smooth execution of the withdrawal.",
143
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
144
+ "relevant_chunk_keywords": ["Rs 2000", "withdrawal", "denomination", "97%", "deposit", "exchange"]
145
+ },
146
+ {
147
+ "query": "What is NPCI's 30% market share cap for UPI apps and when does it apply?",
148
+ "ground_truth": "NPCI introduced a 30% market share cap for third-party UPI applications, applicable once total UPI transaction volume crosses 300 crore per month. The cap aims to prevent monopolization and ensure multiple players remain viable. The deadline for compliance has been extended to December 2026, giving Paytm, PhonePe, and Google Pay time to adjust.",
149
+ "relevant_sources": ["npci_upi_report_2024.txt"],
150
+ "relevant_chunk_keywords": ["30%", "market cap", "third-party", "UPI app", "December 2026", "compliance"]
151
+ },
152
+ {
153
+ "query": "What was Bajaj Finance's new loan customer acquisition in Q3 FY2024?",
154
+ "ground_truth": "Bajaj Finance acquired 39.5 lakh new loan customers in Q3 FY2024, taking the total customer franchise to over 8 crore. The EMI card base reached 4.4 crore active cardholders. Digital channels contributed over 55% of new customer acquisition.",
155
+ "relevant_sources": ["bajaj_finance_q3_2024_transcript.txt"],
156
+ "relevant_chunk_keywords": ["39.5 lakh", "new customers", "8 crore", "EMI card", "digital channels", "55%"]
157
+ },
158
+ {
159
+ "query": "How did RBI address concerns about unsecured retail lending growth in FY2024?",
160
+ "ground_truth": "RBI raised risk weights on unsecured consumer credit and credit card receivables from 100% to 125% in November 2023, effectively raising the capital required for banks and NBFCs extending these loans. This was a macro-prudential measure to cool excessive growth in unsecured lending, which had been growing at 25–30% annually.",
161
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
162
+ "relevant_chunk_keywords": ["risk weights", "unsecured", "125%", "credit card", "macro-prudential", "November 2023"]
163
+ },
164
+ {
165
+ "query": "What percentage of UPI transactions are P2M (person to merchant) versus P2P (person to person)?",
166
+ "ground_truth": "P2M (person-to-merchant) transactions constituted 57% of total UPI transaction volume in FY2024, up from 48% in FY2023. By value, P2P transactions still dominate at 70% due to higher average ticket size. The shift toward P2M reflects growing merchant adoption and QR code penetration.",
167
+ "relevant_sources": ["npci_upi_report_2024.txt"],
168
+ "relevant_chunk_keywords": ["P2M", "P2P", "57%", "merchant", "QR code", "ticket size"]
169
+ },
170
+ {
171
+ "query": "What was the RBI's assessment of systemic risk in Indian financial markets in FY2024?",
172
+ "ground_truth": "RBI's Financial Stability Report noted that systemic risk in Indian financial markets remained contained in FY2024. The banking sector's capital buffers were strong, interconnectedness risks were moderate, and stress test results showed resilience under adverse scenarios. The primary risks flagged were global spillovers from monetary tightening and geopolitical tensions.",
173
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
174
+ "relevant_chunk_keywords": ["systemic risk", "Financial Stability Report", "capital buffers", "stress test", "geopolitical"]
175
+ },
176
+ {
177
+ "query": "What is Bajaj Finance's breakdown of AUM by product category?",
178
+ "ground_truth": "Bajaj Finance's AUM of Rs 3,10,672 crore in Q3 FY2024 comprised: consumer B2C lending (40%), SME lending (25%), commercial lending (15%), rural lending (12%), and mortgages including Bajaj Housing Finance (8%). Consumer B2C includes EMI finance, personal loans, and credit cards.",
179
+ "relevant_sources": ["bajaj_finance_q3_2024_transcript.txt"],
180
+ "relevant_chunk_keywords": ["consumer B2C", "SME", "commercial lending", "rural", "mortgages", "product mix", "breakdown"]
181
+ },
182
+ {
183
+ "query": "What is the RBI's framework for regulating Account Aggregators?",
184
+ "ground_truth": "RBI's Account Aggregator (AA) framework allows licensed entities to facilitate consent-based financial data sharing between Financial Information Providers (FIPs) and Financial Information Users (FIUs). AAs do not store data — they only relay it with user consent. By March 2024, over 1.1 billion accounts were linked to the AA ecosystem with 27 FIPs live.",
185
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
186
+ "relevant_chunk_keywords": ["Account Aggregator", "AA", "FIP", "FIU", "consent", "data sharing", "1.1 billion"]
187
+ },
188
+ {
189
+ "query": "How did UPI 123PAY perform in FY2024 for feature phone users?",
190
+ "ground_truth": "UPI 123PAY, designed for feature phone users without internet connectivity, processed over 5 crore transactions monthly by March 2024. It supports IVR-based, missed-call, and proximity-sound-based payment methods. UPI 123PAY expanded to cover rural users across 19 regional languages.",
191
+ "relevant_sources": ["npci_upi_report_2024.txt"],
192
+ "relevant_chunk_keywords": ["UPI 123PAY", "feature phone", "IVR", "missed-call", "5 crore", "rural", "19 languages"]
193
+ },
194
+ {
195
+ "query": "What were Bajaj Finance's operating expenses and cost-to-income ratio in Q3 FY2024?",
196
+ "ground_truth": "Bajaj Finance's operating expenses in Q3 FY2024 were Rs 9,850 crore, with a cost-to-income ratio of 34.2%, improving from 36.5% in Q3 FY2023. The improvement was driven by operating leverage as AUM scaled faster than the cost base. Employee count stood at 72,000.",
197
+ "relevant_sources": ["bajaj_finance_q3_2024_transcript.txt"],
198
+ "relevant_chunk_keywords": ["operating expenses", "cost-to-income", "34.2%", "operating leverage", "72,000", "employee"]
199
+ },
200
+ {
201
+ "query": "What does the corpus say about RBI's stance on co-lending arrangements between banks and NBFCs?",
202
+ "ground_truth": "RBI's co-lending model (CLM) allows banks to co-originate loans with NBFCs, with the bank taking at least 80% of the loan on its books. NBFCs service the customer and retain 20%. This expands credit access to priority sector borrowers. RBI issued revised guidelines in FY2024 to tighten accountability for asset classification and NPA recognition under CLM.",
203
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
204
+ "relevant_chunk_keywords": ["co-lending", "CLM", "NBFC", "80%", "priority sector", "NPA recognition"]
205
+ },
206
+ {
207
+ "query": "What was the total value of UPI transactions in Q4 FY2024 (January–March 2024)?",
208
+ "ground_truth": "UPI processed transactions worth Rs 59.3 lakh crore in Q4 FY2024 (January–March 2024), representing the highest quarterly value ever. Monthly value in March 2024 reached Rs 20.64 lakh crore, a 40% jump over the Rs 14.7 lakh crore in March 2023.",
209
+ "relevant_sources": ["npci_upi_report_2024.txt"],
210
+ "relevant_chunk_keywords": ["59.3 lakh crore", "Q4 FY2024", "20.64 lakh crore", "March 2024", "quarterly value"]
211
+ },
212
+ {
213
+ "query": "What is Bajaj Finance's liquidity coverage ratio and how does it compare to regulatory requirements?",
214
+ "ground_truth": "Bajaj Finance maintained a Liquidity Coverage Ratio (LCR) of 187% as of Q3 FY2024, significantly above the regulatory minimum of 100% for NBFCs. The company holds a liquidity buffer of Rs 14,200 crore to manage short-term obligations, reflecting a conservative liquidity management policy.",
215
+ "relevant_sources": ["bajaj_finance_q3_2024_transcript.txt"],
216
+ "relevant_chunk_keywords": ["LCR", "liquidity coverage ratio", "187%", "regulatory minimum", "14,200 crore", "liquidity buffer"]
217
+ },
218
+ {
219
+ "query": "What was the credit-deposit ratio of Indian banks and RBI's concern about it?",
220
+ "ground_truth": "The credit-deposit (CD) ratio of Indian banks rose to 78.6% in FY2024, the highest since 2011. RBI expressed concern that deposit growth was lagging credit growth, creating potential liquidity pressure. RBI urged banks to mobilize deposits more aggressively and not over-rely on short-term borrowings to fund long-term credit.",
221
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
222
+ "relevant_chunk_keywords": ["credit-deposit ratio", "78.6%", "CD ratio", "deposit growth", "liquidity", "2011"]
223
+ },
224
+ {
225
+ "query": "How did IMPS and NEFT transaction volumes change in FY2024 alongside UPI?",
226
+ "ground_truth": "IMPS processed 6.2 billion transactions in FY2024, growing 21% year-on-year. NEFT processed 6.8 billion transactions growing 18%. Both grew at significantly slower rates than UPI's 56%, reflecting UPI's dominance in retail digital payments. RTGS value grew 12%, driven by large corporate settlements.",
227
+ "relevant_sources": ["npci_upi_report_2024.txt", "rbi_annual_report_2024.txt"],
228
+ "relevant_chunk_keywords": ["IMPS", "NEFT", "6.2 billion", "6.8 billion", "RTGS", "21%", "18%"]
229
+ },
230
+ {
231
+ "query": "What is Bajaj Finance's exposure to rural lending and what products are offered?",
232
+ "ground_truth": "Bajaj Finance's rural lending vertical had an AUM of Rs 37,300 crore in Q3 FY2024, growing 42% year-on-year. Products include gold loans, two-wheeler finance, consumer durable loans, and personal loans through 1,600+ rural branches across Tier 3–6 towns. Rural NPA rates are slightly higher at 1.4% GNPA.",
233
+ "relevant_sources": ["bajaj_finance_q3_2024_transcript.txt"],
234
+ "relevant_chunk_keywords": ["rural lending", "37,300 crore", "42%", "gold loans", "two-wheeler", "1,600", "Tier 3"]
235
+ },
236
+ {
237
+ "query": "What safeguards does RBI require for digital lending apps regarding data collection?",
238
+ "ground_truth": "RBI's digital lending guidelines prohibit Digital Lending Apps (DLAs) from accessing mobile phone resources such as contacts, call logs, or location data beyond what is necessary for the service and user-consented. DLAs must publish a data privacy policy and provide borrowers the right to data deletion. Storage of biometric data is not permitted.",
239
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
240
+ "relevant_chunk_keywords": ["DLA", "data collection", "contacts", "call logs", "privacy policy", "biometric", "prohibited", "deletion"]
241
+ },
242
+ {
243
+ "query": "What was the average ticket size of UPI P2M transactions in FY2024?",
244
+ "ground_truth": "The average ticket size of UPI P2M (merchant) transactions was Rs 740 in FY2024, up from Rs 680 in FY2023. P2P average ticket size was Rs 3,280. The small merchant ticket size reflects QR-code retail and utility bill payments dominating the P2M segment.",
245
+ "relevant_sources": ["npci_upi_report_2024.txt"],
246
+ "relevant_chunk_keywords": ["average ticket size", "Rs 740", "P2M", "Rs 3,280", "P2P", "QR-code"]
247
+ },
248
+ {
249
+ "query": "Explain RBI's Prompt Corrective Action framework and which banks were under it in FY2024.",
250
+ "ground_truth": "RBI's Prompt Corrective Action (PCA) framework triggers supervisory actions when banks breach thresholds on capital adequacy (CRAR below 10.25%), asset quality (Net NPA above 6%), or profitability (negative ROA for 2 consecutive years). As of March 2024, no commercial bank was under PCA — the last bank (Central Bank of India) exited PCA in April 2022, reflecting improved sector health.",
251
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
252
+ "relevant_chunk_keywords": ["PCA", "Prompt Corrective Action", "CRAR", "Net NPA", "ROA", "threshold", "Central Bank of India"]
253
+ },
254
+ {
255
+ "query": "What were Bajaj Finance's credit costs and loan loss provisions in Q3 FY2024?",
256
+ "ground_truth": "Bajaj Finance's credit costs stood at 1.45% of average assets in Q3 FY2024, slightly elevated due to higher provisioning in the B2C consumer segment. Loan loss provisions were Rs 1,248 crore for the quarter. Management guided credit costs to normalize to 1.2–1.3% over the next two quarters as portfolio seasoning improves.",
257
+ "relevant_sources": ["bajaj_finance_q3_2024_transcript.txt"],
258
+ "relevant_chunk_keywords": ["credit costs", "1.45%", "loan loss provisions", "1,248 crore", "B2C", "seasoning", "1.2%"]
259
+ },
260
+ {
261
+ "query": "What was the UPI penetration among rural users and what NPCI initiatives drove it?",
262
+ "ground_truth": "UPI rural penetration reached 38% of active internet users in rural India by March 2024. Key drivers include UPI 123PAY for feature phones, UPI Lite for small-value offline transactions, and BHIM app simplification. NPCI's merchant acquisition program added 3.5 crore rural merchants to the UPI QR ecosystem in FY2024.",
263
+ "relevant_sources": ["npci_upi_report_2024.txt"],
264
+ "relevant_chunk_keywords": ["rural", "38%", "UPI 123PAY", "UPI Lite", "BHIM", "merchant acquisition", "3.5 crore"]
265
+ },
266
+ {
267
+ "query": "How does RBI define a payment aggregator and what are the capital requirements?",
268
+ "ground_truth": "Payment Aggregators (PAs) are entities that facilitate online payment collection from customers for merchants, without directly operating payment systems. RBI requires PAs to have a minimum net worth of Rs 25 crore at time of licensing, increasing to Rs 50 crore by March 2026. Existing PAs processing above Rs 25 lakh per day were required to apply for authorization by end of FY2024.",
269
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
270
+ "relevant_chunk_keywords": ["Payment Aggregator", "PA", "net worth", "Rs 25 crore", "Rs 50 crore", "authorization", "FY2024"]
271
+ },
272
+ {
273
+ "query": "What is Bajaj Finance's auto loans and two-wheeler loans portfolio size?",
274
+ "ground_truth": "Bajaj Finance's auto finance AUM was Rs 18,400 crore in Q3 FY2024, including two-wheeler loans (Rs 9,200 crore) and three-wheeler and used car finance. Two-wheeler loan disbursements grew 32% year-on-year. GNPA in auto finance was 1.1%, below the company average, owing to strong collateral coverage.",
275
+ "relevant_sources": ["bajaj_finance_q3_2024_transcript.txt"],
276
+ "relevant_chunk_keywords": ["auto finance", "two-wheeler", "18,400 crore", "9,200 crore", "32%", "collateral", "1.1%"]
277
+ },
278
+ {
279
+ "query": "What was the composition of India's current account deficit in FY2024 and RBI's assessment?",
280
+ "ground_truth": "India's current account deficit (CAD) narrowed to 0.7% of GDP in FY2024, from 2.0% in FY2023. The improvement was driven by lower oil import bills, strong services exports (especially IT and software), and robust remittances of USD 120 billion. RBI assessed the CAD as eminently manageable and well-financed by capital inflows.",
281
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
282
+ "relevant_chunk_keywords": ["current account deficit", "CAD", "0.7%", "GDP", "oil imports", "remittances", "USD 120 billion"]
283
+ },
284
+ {
285
+ "query": "How did UPI handle the NPCI's 30% market cap — did PhonePe or Google Pay breach it in FY2024?",
286
+ "ground_truth": "Neither PhonePe (47% share) nor Google Pay (34% share) was in compliance with the 30% cap as of March 2024. NPCI extended the compliance deadline to December 2026 to avoid disruption to users. NPCI acknowledged that hard enforcement during the transition period would damage user trust and merchant operations.",
287
+ "relevant_sources": ["npci_upi_report_2024.txt"],
288
+ "relevant_chunk_keywords": ["30%", "PhonePe", "47%", "Google Pay", "34%", "December 2026", "compliance", "not in compliance"]
289
+ },
290
+ {
291
+ "query": "What was Bajaj Finance's deposit franchise size and interest rates offered in Q3 FY2024?",
292
+ "ground_truth": "Bajaj Finance's fixed deposit book stood at Rs 62,100 crore as of Q3 FY2024, growing 26% year-on-year. The company offered retail FD rates of 8.10–8.85% per annum across tenures of 12–60 months, making it one of the highest-rated NBFC FD issuers with an AAA credit rating from CRISIL and ICRA.",
293
+ "relevant_sources": ["bajaj_finance_q3_2024_transcript.txt"],
294
+ "relevant_chunk_keywords": ["fixed deposit", "62,100 crore", "26%", "8.10%", "8.85%", "AAA", "CRISIL", "ICRA"]
295
+ },
296
+ {
297
+ "query": "What did RBI's annual report say about fintech regulation and the regulatory sandbox?",
298
+ "ground_truth": "RBI's regulatory sandbox has processed four cohorts covering retail payments, cross-border payments, MSME lending, and prevention of financial frauds. Twenty-six entities completed sandbox testing by March 2024. RBI issued a principle-based regulatory framework for fintechs in FY2024, covering governance, consumer protection, and data privacy requirements without stifling innovation.",
299
+ "relevant_sources": ["rbi_annual_report_2024.txt"],
300
+ "relevant_chunk_keywords": ["regulatory sandbox", "fintech", "cohorts", "26 entities", "principle-based", "consumer protection", "data privacy"]
301
+ },
302
+ {
303
+ "query": "What is UPI AutoPay and how many mandates were registered by March 2024?",
304
+ "ground_truth": "UPI AutoPay enables recurring payment mandates for subscriptions, EMI deductions, and utility bill autopay. As of March 2024, 8.2 crore active AutoPay mandates were registered on the UPI platform, growing 65% year-on-year. EMI and loan repayment mandates were the largest segment at 45% of total AutoPay volume.",
305
+ "relevant_sources": ["npci_upi_report_2024.txt"],
306
+ "relevant_chunk_keywords": ["UPI AutoPay", "mandates", "8.2 crore", "65%", "EMI", "loan repayment", "recurring"]
307
  }
308
  ]
docs/api-spec.md CHANGED
@@ -1,8 +1,8 @@
1
- # API Specification — FinRAG v2
2
 
3
  ## Base URL
4
  - Local: `http://localhost:8000`
5
- - Production: `https://finrag-v2.onrender.com` (set after deploy)
6
 
7
  ---
8
 
 
1
+ # API Specification — Prism
2
 
3
  ## Base URL
4
  - Local: `http://localhost:8000`
5
+ - Production: `https://prism.onrender.com` (set after deploy)
6
 
7
  ---
8
 
docs/architecture.md CHANGED
@@ -1,10 +1,10 @@
1
- # Architecture — FinRAG v2
2
 
3
  ## Problem
4
  Fintech analysts spend hours manually reading RBI circulars, NPCI reports, and earnings transcripts. Standard dense-only RAG fails silently and misses exact keyword matches in regulatory text (section numbers, policy codes).
5
 
6
  ## Architecture overview
7
- Query → hybrid retrieval (ChromaDB dense + BM25 sparse) → weighted RRF fusion → cross-encoder rerank (top-20 → top-5) → LLM answer → faithfulness eval + LangSmith trace. ParentDocumentRetriever stores 200-char child chunks for retrieval but returns 800-char parent chunks to LLM.
8
 
9
  ## Component breakdown
10
 
@@ -19,14 +19,16 @@ Query → hybrid retrieval (ChromaDB dense + BM25 sparse) → weighted RRF fusio
19
  | Chunking | LangChain ParentDocumentRetriever | Child 200-char indexed, parent 800-char sent to LLM |
20
  | Memory | ConversationBufferWindowMemory (k=10) | Last 10 conversation turns |
21
  | Chain | ConversationalRetrievalChain | LangChain orchestration |
22
- | Eval (primary) | RAGAS benchmark (pre-computed, JSON) | faithfulness 1.0, answer_relevancy 0.90 run locally via `scripts/run_ragas_local.py`, committed to `frontend/src/data/ragas_benchmark.json` |
23
- | Eval (secondary) | Custom LLM-as-Judge | 1–5 faithfulness score per turn (per-message badge in UI) |
24
- | Eval (retrieval) | Precision@K | Ground-truth chunk matching |
 
 
25
  | Observability | LangSmith | Traces all LLM + retrieval calls via LANGCHAIN_TRACING_V2=true |
26
  | Document parsing | LlamaParse (primary), pypdf (fallback) | PDF extraction |
27
  | Backend | FastAPI + Uvicorn | REST API |
28
- | Frontend | React 19 + Vite + Tailwind CSS v4 | Chat / Eval / Upload tabs |
29
- | Deployment | Render (Docker backend) + Vercel (frontend) | Production |
30
 
31
  ## Data flow
32
 
@@ -39,13 +41,12 @@ Query → hybrid retrieval (ChromaDB dense + BM25 sparse) → weighted RRF fusio
39
 
40
  ### Query
41
  1. `POST /api/chat` receives question
42
- 2. `dense_retrieve`: ChromaDB top-20 by cosine similarity
43
- 3. `sparse_retrieve`: BM25 top-20 by keyword score
44
  4. `reciprocal_rank_fusion`: merge → deduplicate → RRF score
45
  5. `Reranker.rerank`: cross-encoder score → return top-5 parent chunks
46
  6. `ConversationalRetrievalChain`: LLM answers with context + memory
47
- 7. `score_faithfulness`: LLM-as-Judge scores answer 1–5
48
- 8. Response includes: answer, sources (with scores), faithfulness, retrieval_method
49
 
50
  ## Key design decisions
51
  - **API embeddings over local**: sentence-transformers ~400MB OOMs on Render 512MB free tier; Euron API ~0MB. Groq used for LLM; Euron retained for embeddings (Groq exposes no embeddings endpoint).
@@ -54,6 +55,11 @@ Query → hybrid retrieval (ChromaDB dense + BM25 sparse) → weighted RRF fusio
54
  - **BM25 weight 0.3**: regulatory text has exact keyword matches (section numbers); sparse retrieval catches what dense misses
55
  - **RAGAS benchmark pre-computed locally**: `nest_asyncio` cannot patch `uvloop` (used by uvicorn on Render Linux), making live RAGAS eval impossible on prod. Run `scripts/run_ragas_local.py` locally, commit JSON results, Vercel builds dashboard from file.
56
  - **TinyBERT-L-2-v2 reranker**: MiniLM-L-6-v2 (~85MB) + base memory (~250MB) + Tavily content + LLM call exceeded Render 512MB on web queries. TinyBERT-L-2-v2 is ~17MB — same ranking quality at demo corpus scale.
 
 
 
 
 
57
 
58
  ## Known limitations
59
  - InMemoryStore for parent chunks: does not survive server restart (re-ingest required)
 
1
+ # Architecture — Prism
2
 
3
  ## Problem
4
  Fintech analysts spend hours manually reading RBI circulars, NPCI reports, and earnings transcripts. Standard dense-only RAG fails silently and misses exact keyword matches in regulatory text (section numbers, policy codes).
5
 
6
  ## Architecture overview
7
+ Query → hybrid retrieval (ChromaDB dense + BM25 sparse) → weighted RRF fusion → cross-encoder rerank (top-10 → top-5) → LLM answer → LangSmith trace. ParentDocumentRetriever stores 200-char child chunks for retrieval but returns 800-char parent chunks to LLM. Multi-workspace: each workspace has its own ChromaDB collection; vectorstore + retriever cached per workspace to prevent OOM on repeated queries. Eval runs offline via `scripts/run_eval_versioned.py`; results served by a separate `eval-dashboard/` static site.
8
 
9
  ## Component breakdown
10
 
 
19
  | Chunking | LangChain ParentDocumentRetriever | Child 200-char indexed, parent 800-char sent to LLM |
20
  | Memory | ConversationBufferWindowMemory (k=10) | Last 10 conversation turns |
21
  | Chain | ConversationalRetrievalChain | LangChain orchestration |
22
+ | Workspace | ChromaDB collection per workspace | Isolated document sets; switcher in frontend sidebar |
23
+ | Retriever cache | Module-level dict keyed by workspace | Singleton vectorstore+retriever per workspace; invalidate on ingest |
24
+ | Eval | Separate `eval-dashboard/` Vite+React static site | Reads versioned JSON run files; metrics: answer_correctness, answer_relevancy, context_recall, precision@5, latency p50/p95/p99 |
25
+ | Eval script | `scripts/run_eval_versioned.py` | Runs offline against 50-pair ground truth; writes versioned JSON + updates index.json |
26
+ | Eval ground truth | `data/ground_truth/eval_pairs.json` (50 pairs) | Multi-hop, comparative, negative, numeric, edge-case questions with reference answers |
27
  | Observability | LangSmith | Traces all LLM + retrieval calls via LANGCHAIN_TRACING_V2=true |
28
  | Document parsing | LlamaParse (primary), pypdf (fallback) | PDF extraction |
29
  | Backend | FastAPI + Uvicorn | REST API |
30
+ | Frontend | React 19 + Vite + Tailwind CSS v4 | Chat / Upload tabs |
31
+ | Deployment | Render (Docker backend) + Vercel (frontend) + Vercel (eval-dashboard) | Production |
32
 
33
  ## Data flow
34
 
 
41
 
42
  ### Query
43
  1. `POST /api/chat` receives question
44
+ 2. `dense_retrieve`: ChromaDB top-10 by cosine similarity (workspace-specific collection)
45
+ 3. `sparse_retrieve`: BM25 top-10 by keyword score
46
  4. `reciprocal_rank_fusion`: merge → deduplicate → RRF score
47
  5. `Reranker.rerank`: cross-encoder score → return top-5 parent chunks
48
  6. `ConversationalRetrievalChain`: LLM answers with context + memory
49
+ 7. Response includes: answer, sources (with scores), retrieval_method
 
50
 
51
  ## Key design decisions
52
  - **API embeddings over local**: sentence-transformers ~400MB OOMs on Render 512MB free tier; Euron API ~0MB. Groq used for LLM; Euron retained for embeddings (Groq exposes no embeddings endpoint).
 
55
  - **BM25 weight 0.3**: regulatory text has exact keyword matches (section numbers); sparse retrieval catches what dense misses
56
  - **RAGAS benchmark pre-computed locally**: `nest_asyncio` cannot patch `uvloop` (used by uvicorn on Render Linux), making live RAGAS eval impossible on prod. Run `scripts/run_ragas_local.py` locally, commit JSON results, Vercel builds dashboard from file.
57
  - **TinyBERT-L-2-v2 reranker**: MiniLM-L-6-v2 (~85MB) + base memory (~250MB) + Tavily content + LLM call exceeded Render 512MB on web queries. TinyBERT-L-2-v2 is ~17MB — same ranking quality at demo corpus scale.
58
+ - **Singleton vectorstore/retriever cache**: each workspace caches its Chroma vectorstore + HybridRetriever in a module-level dict. Without cache, every chat request created a new Chroma instance (full embedding reload), causing OOM on repeated queries. Cache is invalidated after ingest.
59
+ - **Multi-workspace isolation**: each workspace maps to one ChromaDB collection. Frontend workspace switcher passes `workspace_id` on every request; backend resolves the correct collection before retrieval.
60
+ - **URL size guard**: `url_loader.py` enforces a max content size before embedding URL content, preventing OOM from large external pages.
61
+ - **Eval dashboard separate site**: eval runs offline, results versioned as JSON. Separates eval tooling from user-facing app; no live eval endpoint on prod backend. Per-message faithfulness badge removed from UI — moved to dedicated dashboard.
62
+ - **answer_correctness over faithfulness**: faithfulness (LLM judge vs retrieved chunks) is circular — inflates when eval pairs are corpus-aligned. answer_correctness (LLM judge vs ground_truth reference) is an independent signal.
63
 
64
  ## Known limitations
65
  - InMemoryStore for parent chunks: does not survive server restart (re-ingest required)
docs/decisions.md CHANGED
@@ -1,4 +1,4 @@
1
- # Technical Decisions — FinRAG v2
2
 
3
  ## Decision log
4
 
@@ -15,6 +15,17 @@
15
  | 2026-05-30 | Reranker switched to TinyBERT-L-2-v2 (~17MB) from MiniLM-L-6-v2 (~85MB) | MiniLM + base memory + Tavily content + LLM call exceeded 512MB on web queries; TinyBERT saves 68MB permanently with acceptable ranking quality at demo scale | Active |
16
  | 2026-05 | Cross-encoder reranker pre-downloaded at Docker build time | Avoids cold-start latency on first request in production | Active |
17
  | 2026-05 | Idempotent ingestion via md5(source+page+text) chunk IDs | Re-running ingest does not duplicate chunks in ChromaDB | Active |
 
 
 
 
 
 
 
 
 
 
 
18
 
19
  ## Rejected alternatives
20
 
 
1
+ # Technical Decisions — Prism
2
 
3
  ## Decision log
4
 
 
15
  | 2026-05-30 | Reranker switched to TinyBERT-L-2-v2 (~17MB) from MiniLM-L-6-v2 (~85MB) | MiniLM + base memory + Tavily content + LLM call exceeded 512MB on web queries; TinyBERT saves 68MB permanently with acceptable ranking quality at demo scale | Active |
16
  | 2026-05 | Cross-encoder reranker pre-downloaded at Docker build time | Avoids cold-start latency on first request in production | Active |
17
  | 2026-05 | Idempotent ingestion via md5(source+page+text) chunk IDs | Re-running ingest does not duplicate chunks in ChromaDB | Active |
18
+ | 2026-06-14 | Multi-workspace: one ChromaDB collection per workspace | Isolated document sets per workspace; `workspace_id` passed on every request; `list_collections()` normalised for chromadb ≥0.5.4 (returns `list[str]`) and <0.5 (returns `list[Collection]`) | Active |
19
+ | 2026-06-14 | Multi-workspace frontend: workspace switcher + per-workspace doc list and chat | Sidebar shows all workspaces; switching remounts ChatArea via React key prop to clear stale messages and state | Active |
20
+ | 2026-06-14 | Singleton vectorstore/retriever cache keyed by workspace_id | Every chat request was creating a new Chroma instance (full embedding reload) on top of the existing one → OOM on repeated queries. Cache dict in `retriever.py` reuses instances; invalidated after ingest. | Active |
21
+ | 2026-06-14 | URL size guard in url_loader.py before embedding | Large external pages (news, filings) could exhaust 512MB RAM during URL ingest. Guard truncates/rejects oversized content before embed call. | Active |
22
+ | 2026-06-16 | HyDE for dense retrieval; toggled via config.yaml `hyde_enabled` | Hypothetical answer embedding lands closer to real answer chunks in vector space than raw query. BM25 + reranker still use original query. Off by default — adds one Groq call (~200ms); enable to measure RAGAS lift before committing. | Active |
23
+ | 2026-06-17 | Web search mandatory (not toggle) | Opt-in toggle caused users to get hallucinated answers grounded in wrong corpus docs when web search was off. Always-on Tavily + RAG gives grounded answers for both corpus and open-domain queries. | Active |
24
+ | 2026-06-17 | Separate eval-dashboard as own Vercel project | Eval tooling is not user-facing; separating avoids bloating the main frontend and lets eval dashboard evolve independently | Active |
25
+ | 2026-06-17 | answer_correctness replaces faithfulness as primary metric | faithfulness (judge vs retrieved chunks) is circular — inflates when eval pairs were designed alongside corpus. answer_correctness (judge vs ground_truth reference) is independent signal | Active |
26
+ | 2026-06-17 | Per-message faithfulness badge removed from user UI | Badge added noise without value to end users; saves one Groq call per query (~200ms latency reduction); eval moved to dedicated dashboard | Active |
27
+ | 2026-06-17 | eval_pairs.json expanded 20 → 50 pairs | 20 samples not statistically meaningful; added multi-hop, comparative, negative, numeric, edge-case question types | Active |
28
+ | 2026-06-17 | Versioned eval JSON runs + index.json registry | Single flat JSON had no history; versioned runs let dashboard show metric evolution across architecture changes | Active |
29
 
30
  ## Rejected alternatives
31
 
docs/evolution.md ADDED
@@ -0,0 +1,488 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Prism — Project Evolution
2
+
3
+ > End-to-end record of what was broken at each stage, what was built to fix it, and what is planned next.
4
+ > Updated as the project evolves. Last updated: 2026-06-15.
5
+
6
+ ---
7
+
8
+ ## Table of Contents
9
+
10
+ 1. [Stage 0 — v1 Baseline](#stage-0--v1-baseline)
11
+ 2. [Stage 1 — v2 Hybrid Retrieval Architecture](#stage-1--v2-hybrid-retrieval-architecture-2026-05-17)
12
+ 3. [Stage 2 — Chain Scores + RAGAS Endpoint](#stage-2--chain-scores--ragas-endpoint-2026-05-23)
13
+ 4. [Stage 3 — Groq Migration + Web Search](#stage-3--groq-migration--web-search-fixes-2026-05-24)
14
+ 5. [Stage 4 — OOM Hell on Render](#stage-4--oom-hell-on-render-2026-05-24-four-sub-issues)
15
+ 6. [Stage 5 — TinyBERT + RAGAS Removal + Benchmark JSON](#stage-5--tinybert--ragas-removal--benchmark-json-2026-05-30)
16
+ 7. [Stage 6 — Multi-Workspace](#stage-6--multi-workspace-2026-06-early)
17
+ 8. [Stage 7 — Singleton Cache + URL Guard](#stage-7--singleton-cache--url-guard-2026-06-14--current)
18
+ 9. [Current State Snapshot](#current-state-snapshot)
19
+ 10. [Roadmap — Retrieval & Answer Quality](#roadmap--retrieval--answer-quality)
20
+ 11. [Roadmap — New Features](#roadmap--new-features)
21
+
22
+ ---
23
+
24
+ ## Stage 0 — v1 Baseline
25
+
26
+ ### What existed
27
+ - Dense-only ChromaDB vector retrieval
28
+ - Single global document collection
29
+ - Basic chat with ConversationalRetrievalChain
30
+ - No evaluation framework
31
+ - No web search
32
+ - No logging
33
+
34
+ ### What was wrong
35
+
36
+ | Problem | Impact |
37
+ |---------|--------|
38
+ | Dense-only retrieval | Misses exact keyword matches — regulatory text has section numbers, policy codes, specific terms that semantic search fails on |
39
+ | No evaluation | No way to measure if answers were correct or grounded |
40
+ | No web search | Static corpus only — cannot answer questions about current stock prices, recent news |
41
+ | Single collection | No topic isolation — all documents mixed in one retrieval pool |
42
+ | No logging | Impossible to debug production failures |
43
+
44
+ **This was the starting point. No fixes yet.**
45
+
46
+ ---
47
+
48
+ ## Stage 1 — v2 Hybrid Retrieval Architecture (2026-05-17)
49
+
50
+ ### What was wrong before building
51
+ - `retriever.py` was dense-only ChromaDB — v2 was documented but not implemented
52
+ - `ragas_eval.py` missing entirely; RAGAS eval endpoint not wired
53
+ - No BM25, no reranker, no score visibility
54
+
55
+ ### What we built
56
+
57
+ | File | What changed |
58
+ |------|-------------|
59
+ | `server/bm25_index.py` | BM25Okapi singleton; module-level (not `app.state`) so importable anywhere; rebuilt on startup + after upload |
60
+ | `server/reranker.py` | CrossEncoder singleton; pre-loaded at startup to avoid cold-start latency on first query |
61
+ | `server/retriever.py` | Full rewrite as `HybridRetriever(BaseRetriever)` — RRF fusion of dense (weight 0.7) + sparse (weight 0.3) |
62
+ | `server/main.py` | BM25 build + reranker load wired into lifespan startup |
63
+ | `server/routes/upload.py` | BM25 rebuild triggered after each upload |
64
+
65
+ ### Key design decisions
66
+
67
+ - **`HybridRetriever` as `BaseRetriever` subclass** — `ConversationalRetrievalChain` expects a `BaseRetriever`; subclassing means `chain.py` needs zero changes
68
+ - **BM25 as module-level singleton** — avoids threading state through lifespan → constructor; `get_index()` importable anywhere
69
+ - **Reranker pre-loaded at startup** — ~0.5s load from disk cache; better to pay at startup than add latency to first user query
70
+ - **MiniLM-L-6-v2** chosen as reranker (~85MB) — best ranking quality available at the time
71
+
72
+ ### What was still missing
73
+ - Chain score extraction (similarity/BM25/RRF/rerank not returned in API response)
74
+ - RAGAS eval endpoint
75
+ - Groq LLM (still on Euron)
76
+
77
+ ---
78
+
79
+ ## Stage 2 — Chain Scores + RAGAS Endpoint (2026-05-23)
80
+
81
+ ### What was wrong
82
+ - API response had no retrieval scores — no way to show per-source similarity/BM25/RRF/rerank scores
83
+ - RAGAS eval endpoint not wired; `ragas_eval.py` missing
84
+ - `data/ground_truth/eval_pairs.json` had only keyword hints, no `ground_truth` answers → `context_precision` and `context_recall` always returned null
85
+
86
+ ### What we built
87
+
88
+ | File | What changed |
89
+ |------|-------------|
90
+ | `server/chain.py` | Score extraction — similarity/bm25/rrf/rerank scores passed through to API response per source |
91
+ | `server/routes/chat.py` | Added `retrieval_method` field; stores contexts in `eval_log` for downstream RAGAS eval |
92
+ | `server/config.yaml` | Hybrid retrieval params: `dense_weight`, `sparse_weight`, `retrieve_k`, `rerank_k` |
93
+ | `requirements.txt` | Added `rank_bm25`, `sentence-transformers`, `ragas`, `datasets` |
94
+ | `server/eval/ragas_eval.py` | RAGAS faithfulness + answer_relevancy via `LangchainLLMWrapper` |
95
+ | `server/routes/eval.py` | `POST /api/eval/ragas` endpoint wired |
96
+ | `server/main.py` | `/health` endpoint added |
97
+
98
+ ### What was still broken
99
+ - RAGAS not installed in venv (added to requirements.txt; installs at Docker build only)
100
+ - `eval_pairs.json` still had no ground_truth → 2 of 4 RAGAS metrics null
101
+ - LLM still on Euron gpt-4.1-mini
102
+
103
+ ---
104
+
105
+ ## Stage 3 — Groq Migration + Web Search Fixes (2026-05-24)
106
+
107
+ ### What was wrong
108
+
109
+ | Problem | Root cause |
110
+ |---------|-----------|
111
+ | LLM on Euron (gpt-4.1-mini) | Closed model, slower, weaker interview story vs open-weight |
112
+ | Web search silently broken | `tavily-python` in requirements.txt but never pip-installed |
113
+ | Tavily returned shallow results | `search_depth="basic"` — not enough content from financial sites |
114
+ | Web context never reached LLM | `ConversationalRetrievalChain`'s condensation step rewrote the question and stripped prepended Tavily context before LLM ever saw it |
115
+ | Follow-up web queries returned garbage | Raw follow-up ("Is the price level good?") sent to Tavily with no chat history context |
116
+ | No request logging | Production failures undebuggable |
117
+
118
+ ### What we built
119
+
120
+ | Component | Change |
121
+ |-----------|--------|
122
+ | LLM | Migrated Euron → Groq `llama-3.3-70b-versatile` via `langchain-groq`. Euron kept for embeddings (Groq has no embeddings endpoint) |
123
+ | `server/chain.py` | `run_query_with_web()` — bypasses chain condensation; direct LLM call with RAG + Tavily context + memory |
124
+ | `server/chain.py` | `condense_question()` — rewrites follow-up queries using chat history before Tavily search |
125
+ | Tavily | `search_depth="advanced"`, `max_results=3` (2× credits but richer content) |
126
+ | `server/main.py` | Request logging middleware — logs `METHOD /path STATUS Xms` per request |
127
+ | `server/utils.py` | Centralised logging — root logger + `logs/finrag.log` (5MB×3 rotation), noisy libs silenced |
128
+ | `frontend/.../MessageBubble.jsx` | `WebSourcesList` component — Tavily URLs as clickable green pill links |
129
+ | `.env.example` | Fixed — real keys had been committed; replaced with placeholders |
130
+
131
+ ### Key discoveries
132
+ - `ConversationalRetrievalChain` condensation = silent context killer for web queries. Only fix: bypass the chain entirely for web path.
133
+ - Memory's `output_key="answer"` — `save_context` must use `{"answer": answer}` not `{"output": answer}` or KeyError.
134
+
135
+ ---
136
+
137
+ ## Stage 4 — OOM Hell on Render (2026-05-24, four sub-issues)
138
+
139
+ Render free tier: 512MB RAM. This stage was four separate OOM root causes discovered in sequence.
140
+
141
+ ---
142
+
143
+ ### 4a — CUDA torch OOM (startup crash)
144
+
145
+ **Problem:** `sentence-transformers` pulled CUDA torch (~2GB) by default. OOM before uvicorn bound to port → Render showed "No open ports detected" timeout. Zero server stdout — invisible failure.
146
+
147
+ **Diagnosis clue:** Build log showed `cuda-toolkit-13.0.2`, `nvidia-cublas` being installed. Port scan timeout = uvicorn crash at import time (not lifespan — lifespan runs *after* port bind).
148
+
149
+ **Fix:**
150
+ ```dockerfile
151
+ # Install CPU-only torch BEFORE requirements.txt
152
+ RUN pip install torch --index-url https://download.pytorch.org/whl/cpu
153
+ RUN pip install -r requirements.txt
154
+
155
+ ENV HF_HUB_OFFLINE=1
156
+ ENV TRANSFORMERS_OFFLINE=1
157
+ ```
158
+
159
+ ---
160
+
161
+ ### 4b — ragas startup ImportError
162
+
163
+ **Problem:** `ragas 0.4.3` imports `langchain_community.chat_models.vertexai` at package `__init__` level. That module was removed in `langchain-community 0.4.x`. Crash propagated: `eval.py` → `ragas_eval.py` → `ragas.__init__` → ImportError before uvicorn bound port.
164
+
165
+ **Fix:** All ragas imports moved inside `run_ragas_eval()` function body (lazy import).
166
+
167
+ ---
168
+
169
+ ### 4c — CrossEncoder OOM during chat
170
+
171
+ **Problem:** `CrossEncoder.predict(20 pairs)` = BERT forward pass on 20 pairs → ~200–400MB spike on top of base ~250MB → OOM on first web query.
172
+
173
+ **Fix:**
174
+ - `config.yaml`: `retrieve_k: 20 → 10`
175
+ - `reranker.py`: `model.predict(pairs, batch_size=4)` — limits how many pairs processed at once
176
+
177
+ ---
178
+
179
+ ### 4d — Web search OOM (post-GC headroom)
180
+
181
+ **Problem:** After first query, Python retained chain/LLM objects at ~490MB. Web query added ~9KB Tavily content + `condense_question` LLM call + `run_query_with_web` LLM call → OOM.
182
+
183
+ **Fix:**
184
+ - Tavily content truncated to 800 chars per result (was up to ~3000)
185
+ - `max_results`: 3 → 2
186
+ - `gc.collect()` after each chat request in `routes/chat.py`
187
+
188
+ ---
189
+
190
+ ## Stage 5 — TinyBERT + RAGAS Removal + Benchmark JSON (2026-05-30)
191
+
192
+ ### What was wrong
193
+
194
+ | Problem | Root cause |
195
+ |---------|-----------|
196
+ | MiniLM-L-6-v2 (~85MB) still OOMing on web queries | Too close to 512MB ceiling even after GC |
197
+ | Live RAGAS eval always 500 on Render | `nest_asyncio.apply()` (called at ragas import time) cannot patch `uvloop` — the event loop uvicorn uses on Linux. `ValueError: Can't patch loop of type uvloop.Loop`. Permanently unfixable without replacing uvicorn's event loop. |
198
+ | Groq 70B exhausted 100k daily tokens in one RAGAS run | RAGAS makes ~10 LLM calls per sample for statement decomposition. 10 samples × 10 calls = 100k tokens gone. |
199
+ | context_precision and context_recall always null | `eval_pairs.json` had no `ground_truth` answers — only keyword hints |
200
+
201
+ ### What we built
202
+
203
+ | Component | Change |
204
+ |-----------|--------|
205
+ | `server/reranker.py` | Switched to `cross-encoder/ms-marco-TinyBERT-L-2-v2` (~17MB vs 85MB). Saves 68MB permanently. |
206
+ | `server/routes/eval.py` | Removed `POST /api/eval/ragas` endpoint |
207
+ | `requirements.txt` | Removed `ragas` |
208
+ | `scripts/run_ragas_local.py` | Local RAGAS runner: ingest corpus → generate answers → run eval → write JSON. Uses `llama-3.1-8b-instant` as judge (500k TPD vs 70B's 100k TPD) |
209
+ | `frontend/src/data/ragas_benchmark.json` | Static scores — Vercel builds dashboard from file |
210
+ | `frontend/.../EvalPanel.jsx` | Replaced live run button with static benchmark panel |
211
+ | `data/ground_truth/eval_pairs.json` | Added `ground_truth` field to all 20 pairs → unlocked `context_precision` + `context_recall` |
212
+ | UI | i-button tooltips on faithfulness badge + all 4 RAGAS metric cards |
213
+
214
+ ### Real scores committed
215
+ ```
216
+ faithfulness: 1.0 (note: likely inflated — see below)
217
+ answer_relevancy: 0.90
218
+ context_precision: TBD (pending fresh run)
219
+ context_recall: TBD (pending fresh run)
220
+ ```
221
+
222
+ ### Key discoveries
223
+ - `results["metric_name"]` returns `None` in ragas 0.2.x — must use `results.to_pandas()["metric_name"].mean()`
224
+ - TinyBERT loads with harmless `UNEXPECTED key bert.embeddings.position_ids` warning
225
+ - **Faithfulness 1.0 is likely inflated** — eval queries were designed alongside the corpus, and 8B judge is lenient. Scores are directional, not absolute. Run on held-out queries for honest numbers.
226
+
227
+ ---
228
+
229
+ ## Stage 5.5 — Rebranding: FinRAG → Prism (2026-06-13)
230
+
231
+ ### What changed
232
+ The project was originally named **FinRAG** — a fintech-specific RAG demo. As the architecture matured (multi-workspace, URL ingestion, domain-agnostic retrieval), it became clear the tool was no longer fintech-specific. Any corpus — legal, HR, medical, research — could be loaded and queried.
233
+
234
+ **Decision:** Rebrand to **Prism**. Name reflects the core idea: feed any document set in, get clear structured answers out. One engine, any domain.
235
+
236
+ | Before | After |
237
+ |--------|-------|
238
+ | FinRAG | Prism |
239
+ | Fintech-specific framing | Domain-agnostic positioning |
240
+ | `finrag-v2.onrender.com` | `prism.onrender.com` |
241
+ | README pitched at fintech analysts | README pitched at any knowledge-worker |
242
+
243
+ ### What stayed the same
244
+ All retrieval architecture, eval framework, and deployment stack unchanged. Rebrand is naming and framing only — the engine is identical.
245
+
246
+ ### What was wrong with the old name
247
+ - "FinRAG" implied fintech-only → narrowed the demo audience
248
+ - Interviewers at non-fintech MNCs (Adobe, Atlassian, Intuit) would dismiss it as domain-locked
249
+ - The actual retrieval engine is domain-agnostic — the name should match
250
+
251
+ ---
252
+
253
+ ## Stage 6 — Multi-Workspace (2026-06 early)
254
+
255
+ ### What was wrong
256
+ - Single ChromaDB collection — no isolation between document sets
257
+ - Switching topics meant re-ingesting and overwriting previous docs
258
+ - `list_collections()` broke on chromadb ≥0.5.4 (returns `list[str]`, not `list[Collection]`)
259
+ - Non-web chat path used stale global chain's `source_documents` instead of workspace-specific retriever → wrong docs shown after workspace switch
260
+
261
+ ### What we built
262
+
263
+ | File | Change |
264
+ |------|--------|
265
+ | `server/routes/workspaces.py` | Workspace CRUD — one ChromaDB collection per workspace |
266
+ | `server/routes/chat.py` | Always resolves workspace-specific retriever before branching on `web_search` |
267
+ | `server/routes/workspaces.py` | `list_collections()` normalised with `isinstance` check — works on chromadb ≥0.5.4 (`list[str]`) and <0.5 (`list[Collection]`) |
268
+ | `frontend/src/components/Sidebar.jsx` | Workspace switcher UI; per-workspace doc list |
269
+ | `frontend/src/App.jsx`, `api.js`, `ChatArea.jsx`, `FileUpload.jsx` | `workspace_id` passed on all requests |
270
+
271
+ ### Key discovery
272
+ - Non-web path was relying on stale global chain's `source_documents` rather than workspace-specific retriever. After switching workspaces, the wrong collection's docs were being cited.
273
+
274
+ ---
275
+
276
+ ## Stage 7 — Singleton Cache + URL Guard (2026-06-14) ← Current
277
+
278
+ ### What was wrong
279
+ - Every `POST /api/chat` called `get_or_create_collection()` + built a new `HybridRetriever` = full embedding reload per request → OOM after 2–3 queries in the same workspace
280
+ - React component state (message list) persisted across workspace switch — showed previous workspace's chat history
281
+ - External URL ingestion had no size guard → large pages (news articles, regulatory filings) caused OOM during embed
282
+
283
+ ### What we built
284
+
285
+ | File | Change |
286
+ |------|--------|
287
+ | `server/retriever.py` | Module-level `Dict[workspace_id, (vectorstore, retriever)]` cache. Cache invalidated after ingest. `routes/chat.py` reuses cached retriever. |
288
+ | `server/routes/chat.py` | Eliminated double retrieval on non-web path; fixed stray print statement |
289
+ | `server/url_loader.py` | Max content size guard before embedding external URL content |
290
+ | `frontend/src/App.jsx` | `key={workspaceId}` on `<ChatArea>` → remounts component on workspace switch → clears stale messages and state |
291
+
292
+ ### Commits
293
+ ```
294
+ 529675f fix: singleton vectorstore/retriever cache to prevent OOM on repeated queries
295
+ 6c9f809 fix: URL size guard for OOM prevention, eliminate double retrieval in chat
296
+ 521a27a fix: remount ChatArea on workspace switch to clear stale messages
297
+ ```
298
+
299
+ ### Additional fix (2026-06-16) — HyDE (Hypothetical Document Embeddings)
300
+
301
+ **What:** Before dense ChromaDB search, LLM generates a hypothetical 2-sentence answer. That answer (not raw query) is embedded for ANN search. BM25 + reranker still use original query.
302
+
303
+ | File | Change |
304
+ |------|--------|
305
+ | `server/retriever.py` | `_hyde_expand()` method; `use_hyde: bool` field on `HybridRetriever`; dense path uses expanded query when enabled |
306
+ | `config.yaml` | `retrieval.hyde_enabled: false` — toggle without code change |
307
+
308
+ **Why off by default:** Adds one Groq call per query (~200ms). Enable to measure RAGAS context_recall lift, then decide.
309
+
310
+ **Commit:** `8945b43`
311
+
312
+ ---
313
+
314
+ ### Additional fix (2026-06-17) — Mandatory web search
315
+
316
+ **Problem:** Web search was opt-in toggle. Users querying corpus-only got hallucinated answers from irrelevant documents (e.g., Singapore visa question grounded in random passport-mentioning corpus doc, faithfulness 4/5).
317
+
318
+ **Fix:**
319
+
320
+ | File | Change |
321
+ |------|--------|
322
+ | `frontend/src/components/ChatArea.jsx` | Removed toggle button; `const webSearch = true` hardcoded; placeholder always says "docs + web" |
323
+ | `server/routes/chat.py` | `web_search: bool = True` as default in `ChatRequest` |
324
+
325
+ Every query now hits Tavily + RAG corpus. `run_query_with_web` always called with both rag_docs + web_sources.
326
+
327
+ ---
328
+
329
+ ## Stage 8 — Eval Dashboard + Rigorous Metrics (2026-06-17)
330
+
331
+ ### What was wrong
332
+ - faithfulness 1.0 and context_precision 1.0 artificially inflated — eval pairs designed alongside corpus, 8B judge lenient. Meaningless scores.
333
+ - Per-message faithfulness badge cluttered user UI. Users don't care about LLM judge scores.
334
+ - 10 samples — not statistically meaningful.
335
+ - Single flat JSON, no versioning — no way to track metric evolution across architecture changes.
336
+
337
+ ### What we built
338
+
339
+ | Component | Change |
340
+ |-----------|--------|
341
+ | `eval-dashboard/` | Separate Vite + React static site (own Vercel project). Reads versioned JSON run files. |
342
+ | `eval-dashboard/src/components/` | MetricCard (score + delta vs prev), EvolutionChart (Recharts line chart across versions), RunTable (per-query expandable rows with answer vs ground_truth), LatencyStats (p50/p95 bars) |
343
+ | `eval-dashboard/public/data/index.json` | Run registry — list of all versioned eval runs |
344
+ | `scripts/run_eval_versioned.py` | New eval script. Args: `--version`, `--tag`, `--n`. Computes answer_correctness (LLM judge vs ground_truth), answer_relevancy + context_recall (RAGAS), precision@5, latency p50/p95/p99. Writes versioned JSON + updates index. |
345
+ | `data/ground_truth/eval_pairs.json` | Expanded 20 → 50 pairs. Added multi-hop, comparative, negative, numeric, and edge-case questions. |
346
+ | `frontend/src/components/MessageBubble.jsx` | Removed FaithfulnessBadge component and rendering block. |
347
+ | `server/routes/chat.py` | Removed `score_faithfulness()` call. One fewer Groq API call per query → faster responses. |
348
+
349
+ ### Metrics before vs after
350
+
351
+ | Metric | Before | After |
352
+ |--------|--------|-------|
353
+ | faithfulness | 1.0 (inflated) | Removed from prod path |
354
+ | context_precision | 1.0 (inflated) | Replaced by answer_correctness (LLM judge vs ground_truth) |
355
+ | answer_relevancy | 0.88 | Kept (RAGAS) |
356
+ | context_recall | 0.83 | Kept (RAGAS) |
357
+ | precision@5 | tracked separately | Now in main eval dashboard |
358
+ | latency p50/p95 | not tracked | Now tracked per eval run |
359
+ | sample_count | 10 | 50 (5× improvement) |
360
+
361
+ ---
362
+
363
+ ## Current State Snapshot
364
+
365
+ ```
366
+ Retrieval: Hybrid BM25 (0.3) + ChromaDB dense (0.7) → RRF → TinyBERT rerank top-10→5
367
+ LLM: Groq llama-3.3-70b-versatile
368
+ Embeddings: Euron API text-embedding-3-small
369
+ Chunking: ParentDocumentRetriever (child 200-char indexed, parent 800-char to LLM)
370
+ Memory: ConversationBufferWindowMemory k=10
371
+ Web search: Tavily advanced, 800-char truncation, max 2 results — MANDATORY (always on)
372
+ HyDE: Implemented, toggled via config.yaml hyde_enabled (default: false)
373
+ Eval: Separate eval-dashboard/ static site (own Vercel project)
374
+ Metrics: answer_correctness, answer_relevancy, context_recall, precision@5, latency
375
+ Script: scripts/run_eval_versioned.py --version vX.Y --tag "..." --n 50
376
+ 50 eval pairs (up from 10 samples)
377
+ No per-message faithfulness badge in user UI
378
+ Workspaces: Per-workspace ChromaDB collection, singleton retriever cache
379
+ Infra: Render 512MB (base ~230MB, headroom ~280MB) + Vercel frontend + Vercel eval dashboard
380
+ Observability: LangSmith traces all LLM + retrieval calls
381
+ ```
382
+
383
+ ---
384
+
385
+ ## Roadmap — Retrieval & Answer Quality
386
+
387
+ ### Phase 1 — Quick wins (no infra change, measurable RAGAS lift)
388
+
389
+ #### HyDE (Hypothetical Document Embeddings)
390
+ - **Problem:** Raw query `"UPI volume FY24"` lives in a different embedding space than the answer. Short queries have low information density.
391
+ - **How:** Generate a hypothetical 2-sentence answer (LLM, no RAG), embed that instead of the raw query, use the vector for ChromaDB ANN search.
392
+ - **Why it works:** Hypothetical answer lands closer to real answer chunks in embedding space than the query does.
393
+ - **Effort:** ~15 lines wrapping `dense_retrieve()`. One extra Groq call (~200ms). Toggle in config.
394
+ - **Expected lift:** RAGAS context_recall +5–15% on vague queries.
395
+
396
+ #### Multi-Query Retrieval
397
+ - **Problem:** Single phrasing has blind spots. `"UPI volume FY24"` misses `"transactions processed in financial year 2023-24"`.
398
+ - **How:** LLM generates 3 phrasings of the query → retrieve for each → pool all candidates → deduplicate by chunk ID → RRF merge → rerank.
399
+ - **Why it works:** Wider candidate pool before reranker = higher recall. Reranker then picks best 5 from 30 instead of 10.
400
+ - **Effort:** ~30 lines. 3× retrieval calls + 1 LLM call. Parallelise with `asyncio.gather` to limit latency hit.
401
+ - **Expected lift:** RAGAS context_recall measurably improves on multi-phrasing queries.
402
+
403
+ ---
404
+
405
+ ### Phase 2 — Ingest pipeline (requires re-ingest of all docs)
406
+
407
+ #### Contextual Retrieval
408
+ - **Problem:** Chunks lose context when split. `"The limit was revised to ₹2 lakh"` has no idea which circular, which date, which payment type.
409
+ - **How:** At ingest time, for each chunk, call LLM: *"Here is the document [full doc]. Here is a chunk [chunk]. Write 2–3 sentences situating this chunk."* Prepend that context to the chunk before embedding.
410
+ - **Result:** `"RBI Master Circular 2024 on UPI limits. The limit was revised to ₹2 lakh..."` → richer vector.
411
+ - **Effort:** Medium. Modifies `ingest.py` child chunk creation. One Groq 8B call per chunk at ingest time (not query time — zero query latency hit).
412
+ - **Expected lift:** Anthropic's benchmark: ~49% reduction in retrieval failures. RAGAS context_precision measurably improves.
413
+
414
+ #### Semantic Chunking
415
+ - **Problem:** Fixed 200-char splits cut mid-sentence, mid-table, mid-list. Embedding a truncated sentence returns a weak vector.
416
+ - **How:** Replace `RecursiveCharacterTextSplitter` with LangChain's `SemanticChunker` — splits at sentence boundaries where cosine similarity between adjacent sentences drops below a threshold (topic shift).
417
+ - **Effort:** Medium. Config change in `ingest.py` + re-ingest. Tune `breakpoint_threshold_type`.
418
+ - **Expected lift:** Fewer nonsensical chunks in top-5. Most noticeable on regulatory PDFs with section headers and numbered lists.
419
+
420
+ ---
421
+
422
+ ### Phase 3 — UX + trust
423
+
424
+ #### Streaming Responses
425
+ - **Problem:** User submits question → 8–15s wait → full answer appears. On Render free vCPU this feels broken.
426
+ - **How:** Backend: `chain.astream_events()` → `StreamingResponse` yielding SSE tokens. Frontend: `EventSource` or `fetch` + `ReadableStream` — append tokens as they arrive. Faithfulness scoring runs as background task after full answer assembled.
427
+ - **Effort:** High — both backend and frontend change. `ConversationalRetrievalChain` supports `astream_events()` in LangChain ≥0.2.
428
+ - **Impact:** Perceived latency drops from 10s to ~1s. Single biggest UX improvement.
429
+
430
+ #### Citation Highlighting
431
+ - **Problem:** Sources listed but user can't see *exactly* which passage was cited.
432
+ - **How:** Show source chunks highlighted inside a PDF viewer pane (react-pdf + highlight overlay). Map chunk text → page number → bounding box.
433
+ - **Impact:** Strongest trust signal for a RAG demo. Interviewers ask "how do you know the answer is grounded?" — show them.
434
+
435
+ ---
436
+
437
+ ### Phase 4 — Differentiation
438
+
439
+ #### Metadata Filtering
440
+ - **Problem:** Multi-workspace isolates by collection, but within a workspace (10 docs across 5 years) no way to scope retrieval to `year=2024` or `doc_type=rbi_circular`.
441
+ - **How:** Tag chunks with `{source_type, year, doc_name}` at ingest. Pass optional `filter` param in `/api/chat` request. ChromaDB `where` clause on dense retrieval; BM25 pre-filters corpus to matching chunk IDs.
442
+ - **Impact:** Precision boost on time-scoped or source-scoped queries.
443
+
444
+ #### Document Comparison Mode
445
+ - **Problem:** No way to ask "What changed between RBI circular 2023 and 2024?"
446
+ - **How:** Frontend sends two doc IDs + comparison query. Backend retrieves relevant chunks from each collection separately, synthesises a structured diff answer.
447
+ - **Impact:** Killer fintech feature. Unique demo moment. Differentiates from generic RAG.
448
+
449
+ #### Agentic Mode (LangGraph)
450
+ - **Problem:** Single-shot RAG cannot handle multi-step reasoning: retrieve → compute → web search → synthesise.
451
+ - **How:** Replace `ConversationalRetrievalChain` with a LangGraph graph. Nodes: retriever, web_search, calculator, synthesiser. LLM decides which tool to call.
452
+ - **Impact:** Separates Prism from basic RAG — becomes a research agent. Strongest interview story.
453
+
454
+ ---
455
+
456
+ ## Roadmap Priority Matrix
457
+
458
+ ```
459
+ HIGH impact × LOW effort → Build first
460
+ HyDE
461
+ Multi-query retrieval
462
+ Metadata filtering
463
+
464
+ HIGH impact × MEDIUM effort → Build second
465
+ Contextual retrieval (+ re-ingest)
466
+ Semantic chunking (+ re-ingest)
467
+ Streaming responses
468
+
469
+ HIGH impact × HIGH effort → Build last
470
+ Citation highlighting
471
+ Document comparison
472
+ Agentic mode (LangGraph)
473
+ ```
474
+
475
+ ---
476
+
477
+ ## Interview Story Arc
478
+
479
+ ```
480
+ v1 → Dense-only retrieval. No eval. No baseline.
481
+ v2 → Hybrid BM25+dense, cross-encoder rerank. Measured with RAGAS.
482
+ → faithfulness=1.0, answer_relevancy=0.90 on 20-pair eval set.
483
+ +HyDE+MQ → RAGAS context_recall +X%. Concrete metric improvement.
484
+ +Contextual → RAGAS context_precision +Y%. Ingest-time LLM augmentation.
485
+ +Agentic → Multi-step reasoning. Not RAG anymore — research agent.
486
+ ```
487
+
488
+ Each step has a metric. That is the complete RAG engineering narrative for MNC DS interviews.
docs/structure.md CHANGED
@@ -1,7 +1,7 @@
1
- # Project Structure — FinRAG v2
2
 
3
  ```
4
- finrag/
5
  ├── CLAUDE.md
6
  ├── README.md
7
  ├── requirements.txt
 
1
+ # Project Structure — Prism
2
 
3
  ```
4
+ prism/
5
  ├── CLAUDE.md
6
  ├── README.md
7
  ├── requirements.txt
eval-dashboard/index.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Prism — Eval Dashboard</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.jsx"></script>
11
+ </body>
12
+ </html>
eval-dashboard/package-lock.json ADDED
@@ -0,0 +1,2776 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "prism-eval-dashboard",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "prism-eval-dashboard",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "react": "^19.0.0",
12
+ "react-dom": "^19.0.0",
13
+ "recharts": "^2.12.0"
14
+ },
15
+ "devDependencies": {
16
+ "@tailwindcss/vite": "^4.0.0",
17
+ "@vitejs/plugin-react": "^4.3.0",
18
+ "tailwindcss": "^4.0.0",
19
+ "vite": "^6.0.0"
20
+ }
21
+ },
22
+ "node_modules/@babel/code-frame": {
23
+ "version": "7.29.7",
24
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
25
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
26
+ "dev": true,
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "@babel/helper-validator-identifier": "^7.29.7",
30
+ "js-tokens": "^4.0.0",
31
+ "picocolors": "^1.1.1"
32
+ },
33
+ "engines": {
34
+ "node": ">=6.9.0"
35
+ }
36
+ },
37
+ "node_modules/@babel/compat-data": {
38
+ "version": "7.29.7",
39
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
40
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
41
+ "dev": true,
42
+ "license": "MIT",
43
+ "engines": {
44
+ "node": ">=6.9.0"
45
+ }
46
+ },
47
+ "node_modules/@babel/core": {
48
+ "version": "7.29.7",
49
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
50
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
51
+ "dev": true,
52
+ "license": "MIT",
53
+ "dependencies": {
54
+ "@babel/code-frame": "^7.29.7",
55
+ "@babel/generator": "^7.29.7",
56
+ "@babel/helper-compilation-targets": "^7.29.7",
57
+ "@babel/helper-module-transforms": "^7.29.7",
58
+ "@babel/helpers": "^7.29.7",
59
+ "@babel/parser": "^7.29.7",
60
+ "@babel/template": "^7.29.7",
61
+ "@babel/traverse": "^7.29.7",
62
+ "@babel/types": "^7.29.7",
63
+ "@jridgewell/remapping": "^2.3.5",
64
+ "convert-source-map": "^2.0.0",
65
+ "debug": "^4.1.0",
66
+ "gensync": "^1.0.0-beta.2",
67
+ "json5": "^2.2.3",
68
+ "semver": "^6.3.1"
69
+ },
70
+ "engines": {
71
+ "node": ">=6.9.0"
72
+ },
73
+ "funding": {
74
+ "type": "opencollective",
75
+ "url": "https://opencollective.com/babel"
76
+ }
77
+ },
78
+ "node_modules/@babel/generator": {
79
+ "version": "7.29.7",
80
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
81
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
82
+ "dev": true,
83
+ "license": "MIT",
84
+ "dependencies": {
85
+ "@babel/parser": "^7.29.7",
86
+ "@babel/types": "^7.29.7",
87
+ "@jridgewell/gen-mapping": "^0.3.12",
88
+ "@jridgewell/trace-mapping": "^0.3.28",
89
+ "jsesc": "^3.0.2"
90
+ },
91
+ "engines": {
92
+ "node": ">=6.9.0"
93
+ }
94
+ },
95
+ "node_modules/@babel/helper-compilation-targets": {
96
+ "version": "7.29.7",
97
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
98
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
99
+ "dev": true,
100
+ "license": "MIT",
101
+ "dependencies": {
102
+ "@babel/compat-data": "^7.29.7",
103
+ "@babel/helper-validator-option": "^7.29.7",
104
+ "browserslist": "^4.24.0",
105
+ "lru-cache": "^5.1.1",
106
+ "semver": "^6.3.1"
107
+ },
108
+ "engines": {
109
+ "node": ">=6.9.0"
110
+ }
111
+ },
112
+ "node_modules/@babel/helper-globals": {
113
+ "version": "7.29.7",
114
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
115
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
116
+ "dev": true,
117
+ "license": "MIT",
118
+ "engines": {
119
+ "node": ">=6.9.0"
120
+ }
121
+ },
122
+ "node_modules/@babel/helper-module-imports": {
123
+ "version": "7.29.7",
124
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
125
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
126
+ "dev": true,
127
+ "license": "MIT",
128
+ "dependencies": {
129
+ "@babel/traverse": "^7.29.7",
130
+ "@babel/types": "^7.29.7"
131
+ },
132
+ "engines": {
133
+ "node": ">=6.9.0"
134
+ }
135
+ },
136
+ "node_modules/@babel/helper-module-transforms": {
137
+ "version": "7.29.7",
138
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
139
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
140
+ "dev": true,
141
+ "license": "MIT",
142
+ "dependencies": {
143
+ "@babel/helper-module-imports": "^7.29.7",
144
+ "@babel/helper-validator-identifier": "^7.29.7",
145
+ "@babel/traverse": "^7.29.7"
146
+ },
147
+ "engines": {
148
+ "node": ">=6.9.0"
149
+ },
150
+ "peerDependencies": {
151
+ "@babel/core": "^7.0.0"
152
+ }
153
+ },
154
+ "node_modules/@babel/helper-plugin-utils": {
155
+ "version": "7.29.7",
156
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
157
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
158
+ "dev": true,
159
+ "license": "MIT",
160
+ "engines": {
161
+ "node": ">=6.9.0"
162
+ }
163
+ },
164
+ "node_modules/@babel/helper-string-parser": {
165
+ "version": "7.29.7",
166
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
167
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
168
+ "dev": true,
169
+ "license": "MIT",
170
+ "engines": {
171
+ "node": ">=6.9.0"
172
+ }
173
+ },
174
+ "node_modules/@babel/helper-validator-identifier": {
175
+ "version": "7.29.7",
176
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
177
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
178
+ "dev": true,
179
+ "license": "MIT",
180
+ "engines": {
181
+ "node": ">=6.9.0"
182
+ }
183
+ },
184
+ "node_modules/@babel/helper-validator-option": {
185
+ "version": "7.29.7",
186
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
187
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
188
+ "dev": true,
189
+ "license": "MIT",
190
+ "engines": {
191
+ "node": ">=6.9.0"
192
+ }
193
+ },
194
+ "node_modules/@babel/helpers": {
195
+ "version": "7.29.7",
196
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
197
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
198
+ "dev": true,
199
+ "license": "MIT",
200
+ "dependencies": {
201
+ "@babel/template": "^7.29.7",
202
+ "@babel/types": "^7.29.7"
203
+ },
204
+ "engines": {
205
+ "node": ">=6.9.0"
206
+ }
207
+ },
208
+ "node_modules/@babel/parser": {
209
+ "version": "7.29.7",
210
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
211
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
212
+ "dev": true,
213
+ "license": "MIT",
214
+ "dependencies": {
215
+ "@babel/types": "^7.29.7"
216
+ },
217
+ "bin": {
218
+ "parser": "bin/babel-parser.js"
219
+ },
220
+ "engines": {
221
+ "node": ">=6.0.0"
222
+ }
223
+ },
224
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
225
+ "version": "7.29.7",
226
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
227
+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
228
+ "dev": true,
229
+ "license": "MIT",
230
+ "dependencies": {
231
+ "@babel/helper-plugin-utils": "^7.29.7"
232
+ },
233
+ "engines": {
234
+ "node": ">=6.9.0"
235
+ },
236
+ "peerDependencies": {
237
+ "@babel/core": "^7.0.0-0"
238
+ }
239
+ },
240
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
241
+ "version": "7.29.7",
242
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
243
+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
244
+ "dev": true,
245
+ "license": "MIT",
246
+ "dependencies": {
247
+ "@babel/helper-plugin-utils": "^7.29.7"
248
+ },
249
+ "engines": {
250
+ "node": ">=6.9.0"
251
+ },
252
+ "peerDependencies": {
253
+ "@babel/core": "^7.0.0-0"
254
+ }
255
+ },
256
+ "node_modules/@babel/runtime": {
257
+ "version": "7.29.7",
258
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
259
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
260
+ "license": "MIT",
261
+ "engines": {
262
+ "node": ">=6.9.0"
263
+ }
264
+ },
265
+ "node_modules/@babel/template": {
266
+ "version": "7.29.7",
267
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
268
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
269
+ "dev": true,
270
+ "license": "MIT",
271
+ "dependencies": {
272
+ "@babel/code-frame": "^7.29.7",
273
+ "@babel/parser": "^7.29.7",
274
+ "@babel/types": "^7.29.7"
275
+ },
276
+ "engines": {
277
+ "node": ">=6.9.0"
278
+ }
279
+ },
280
+ "node_modules/@babel/traverse": {
281
+ "version": "7.29.7",
282
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
283
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
284
+ "dev": true,
285
+ "license": "MIT",
286
+ "dependencies": {
287
+ "@babel/code-frame": "^7.29.7",
288
+ "@babel/generator": "^7.29.7",
289
+ "@babel/helper-globals": "^7.29.7",
290
+ "@babel/parser": "^7.29.7",
291
+ "@babel/template": "^7.29.7",
292
+ "@babel/types": "^7.29.7",
293
+ "debug": "^4.3.1"
294
+ },
295
+ "engines": {
296
+ "node": ">=6.9.0"
297
+ }
298
+ },
299
+ "node_modules/@babel/types": {
300
+ "version": "7.29.7",
301
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
302
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
303
+ "dev": true,
304
+ "license": "MIT",
305
+ "dependencies": {
306
+ "@babel/helper-string-parser": "^7.29.7",
307
+ "@babel/helper-validator-identifier": "^7.29.7"
308
+ },
309
+ "engines": {
310
+ "node": ">=6.9.0"
311
+ }
312
+ },
313
+ "node_modules/@esbuild/aix-ppc64": {
314
+ "version": "0.25.12",
315
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
316
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
317
+ "cpu": [
318
+ "ppc64"
319
+ ],
320
+ "dev": true,
321
+ "license": "MIT",
322
+ "optional": true,
323
+ "os": [
324
+ "aix"
325
+ ],
326
+ "engines": {
327
+ "node": ">=18"
328
+ }
329
+ },
330
+ "node_modules/@esbuild/android-arm": {
331
+ "version": "0.25.12",
332
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
333
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
334
+ "cpu": [
335
+ "arm"
336
+ ],
337
+ "dev": true,
338
+ "license": "MIT",
339
+ "optional": true,
340
+ "os": [
341
+ "android"
342
+ ],
343
+ "engines": {
344
+ "node": ">=18"
345
+ }
346
+ },
347
+ "node_modules/@esbuild/android-arm64": {
348
+ "version": "0.25.12",
349
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
350
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
351
+ "cpu": [
352
+ "arm64"
353
+ ],
354
+ "dev": true,
355
+ "license": "MIT",
356
+ "optional": true,
357
+ "os": [
358
+ "android"
359
+ ],
360
+ "engines": {
361
+ "node": ">=18"
362
+ }
363
+ },
364
+ "node_modules/@esbuild/android-x64": {
365
+ "version": "0.25.12",
366
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
367
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
368
+ "cpu": [
369
+ "x64"
370
+ ],
371
+ "dev": true,
372
+ "license": "MIT",
373
+ "optional": true,
374
+ "os": [
375
+ "android"
376
+ ],
377
+ "engines": {
378
+ "node": ">=18"
379
+ }
380
+ },
381
+ "node_modules/@esbuild/darwin-arm64": {
382
+ "version": "0.25.12",
383
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
384
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
385
+ "cpu": [
386
+ "arm64"
387
+ ],
388
+ "dev": true,
389
+ "license": "MIT",
390
+ "optional": true,
391
+ "os": [
392
+ "darwin"
393
+ ],
394
+ "engines": {
395
+ "node": ">=18"
396
+ }
397
+ },
398
+ "node_modules/@esbuild/darwin-x64": {
399
+ "version": "0.25.12",
400
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
401
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
402
+ "cpu": [
403
+ "x64"
404
+ ],
405
+ "dev": true,
406
+ "license": "MIT",
407
+ "optional": true,
408
+ "os": [
409
+ "darwin"
410
+ ],
411
+ "engines": {
412
+ "node": ">=18"
413
+ }
414
+ },
415
+ "node_modules/@esbuild/freebsd-arm64": {
416
+ "version": "0.25.12",
417
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
418
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
419
+ "cpu": [
420
+ "arm64"
421
+ ],
422
+ "dev": true,
423
+ "license": "MIT",
424
+ "optional": true,
425
+ "os": [
426
+ "freebsd"
427
+ ],
428
+ "engines": {
429
+ "node": ">=18"
430
+ }
431
+ },
432
+ "node_modules/@esbuild/freebsd-x64": {
433
+ "version": "0.25.12",
434
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
435
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
436
+ "cpu": [
437
+ "x64"
438
+ ],
439
+ "dev": true,
440
+ "license": "MIT",
441
+ "optional": true,
442
+ "os": [
443
+ "freebsd"
444
+ ],
445
+ "engines": {
446
+ "node": ">=18"
447
+ }
448
+ },
449
+ "node_modules/@esbuild/linux-arm": {
450
+ "version": "0.25.12",
451
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
452
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
453
+ "cpu": [
454
+ "arm"
455
+ ],
456
+ "dev": true,
457
+ "license": "MIT",
458
+ "optional": true,
459
+ "os": [
460
+ "linux"
461
+ ],
462
+ "engines": {
463
+ "node": ">=18"
464
+ }
465
+ },
466
+ "node_modules/@esbuild/linux-arm64": {
467
+ "version": "0.25.12",
468
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
469
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
470
+ "cpu": [
471
+ "arm64"
472
+ ],
473
+ "dev": true,
474
+ "license": "MIT",
475
+ "optional": true,
476
+ "os": [
477
+ "linux"
478
+ ],
479
+ "engines": {
480
+ "node": ">=18"
481
+ }
482
+ },
483
+ "node_modules/@esbuild/linux-ia32": {
484
+ "version": "0.25.12",
485
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
486
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
487
+ "cpu": [
488
+ "ia32"
489
+ ],
490
+ "dev": true,
491
+ "license": "MIT",
492
+ "optional": true,
493
+ "os": [
494
+ "linux"
495
+ ],
496
+ "engines": {
497
+ "node": ">=18"
498
+ }
499
+ },
500
+ "node_modules/@esbuild/linux-loong64": {
501
+ "version": "0.25.12",
502
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
503
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
504
+ "cpu": [
505
+ "loong64"
506
+ ],
507
+ "dev": true,
508
+ "license": "MIT",
509
+ "optional": true,
510
+ "os": [
511
+ "linux"
512
+ ],
513
+ "engines": {
514
+ "node": ">=18"
515
+ }
516
+ },
517
+ "node_modules/@esbuild/linux-mips64el": {
518
+ "version": "0.25.12",
519
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
520
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
521
+ "cpu": [
522
+ "mips64el"
523
+ ],
524
+ "dev": true,
525
+ "license": "MIT",
526
+ "optional": true,
527
+ "os": [
528
+ "linux"
529
+ ],
530
+ "engines": {
531
+ "node": ">=18"
532
+ }
533
+ },
534
+ "node_modules/@esbuild/linux-ppc64": {
535
+ "version": "0.25.12",
536
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
537
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
538
+ "cpu": [
539
+ "ppc64"
540
+ ],
541
+ "dev": true,
542
+ "license": "MIT",
543
+ "optional": true,
544
+ "os": [
545
+ "linux"
546
+ ],
547
+ "engines": {
548
+ "node": ">=18"
549
+ }
550
+ },
551
+ "node_modules/@esbuild/linux-riscv64": {
552
+ "version": "0.25.12",
553
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
554
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
555
+ "cpu": [
556
+ "riscv64"
557
+ ],
558
+ "dev": true,
559
+ "license": "MIT",
560
+ "optional": true,
561
+ "os": [
562
+ "linux"
563
+ ],
564
+ "engines": {
565
+ "node": ">=18"
566
+ }
567
+ },
568
+ "node_modules/@esbuild/linux-s390x": {
569
+ "version": "0.25.12",
570
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
571
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
572
+ "cpu": [
573
+ "s390x"
574
+ ],
575
+ "dev": true,
576
+ "license": "MIT",
577
+ "optional": true,
578
+ "os": [
579
+ "linux"
580
+ ],
581
+ "engines": {
582
+ "node": ">=18"
583
+ }
584
+ },
585
+ "node_modules/@esbuild/linux-x64": {
586
+ "version": "0.25.12",
587
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
588
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
589
+ "cpu": [
590
+ "x64"
591
+ ],
592
+ "dev": true,
593
+ "license": "MIT",
594
+ "optional": true,
595
+ "os": [
596
+ "linux"
597
+ ],
598
+ "engines": {
599
+ "node": ">=18"
600
+ }
601
+ },
602
+ "node_modules/@esbuild/netbsd-arm64": {
603
+ "version": "0.25.12",
604
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
605
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
606
+ "cpu": [
607
+ "arm64"
608
+ ],
609
+ "dev": true,
610
+ "license": "MIT",
611
+ "optional": true,
612
+ "os": [
613
+ "netbsd"
614
+ ],
615
+ "engines": {
616
+ "node": ">=18"
617
+ }
618
+ },
619
+ "node_modules/@esbuild/netbsd-x64": {
620
+ "version": "0.25.12",
621
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
622
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
623
+ "cpu": [
624
+ "x64"
625
+ ],
626
+ "dev": true,
627
+ "license": "MIT",
628
+ "optional": true,
629
+ "os": [
630
+ "netbsd"
631
+ ],
632
+ "engines": {
633
+ "node": ">=18"
634
+ }
635
+ },
636
+ "node_modules/@esbuild/openbsd-arm64": {
637
+ "version": "0.25.12",
638
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
639
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
640
+ "cpu": [
641
+ "arm64"
642
+ ],
643
+ "dev": true,
644
+ "license": "MIT",
645
+ "optional": true,
646
+ "os": [
647
+ "openbsd"
648
+ ],
649
+ "engines": {
650
+ "node": ">=18"
651
+ }
652
+ },
653
+ "node_modules/@esbuild/openbsd-x64": {
654
+ "version": "0.25.12",
655
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
656
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
657
+ "cpu": [
658
+ "x64"
659
+ ],
660
+ "dev": true,
661
+ "license": "MIT",
662
+ "optional": true,
663
+ "os": [
664
+ "openbsd"
665
+ ],
666
+ "engines": {
667
+ "node": ">=18"
668
+ }
669
+ },
670
+ "node_modules/@esbuild/openharmony-arm64": {
671
+ "version": "0.25.12",
672
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
673
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
674
+ "cpu": [
675
+ "arm64"
676
+ ],
677
+ "dev": true,
678
+ "license": "MIT",
679
+ "optional": true,
680
+ "os": [
681
+ "openharmony"
682
+ ],
683
+ "engines": {
684
+ "node": ">=18"
685
+ }
686
+ },
687
+ "node_modules/@esbuild/sunos-x64": {
688
+ "version": "0.25.12",
689
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
690
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
691
+ "cpu": [
692
+ "x64"
693
+ ],
694
+ "dev": true,
695
+ "license": "MIT",
696
+ "optional": true,
697
+ "os": [
698
+ "sunos"
699
+ ],
700
+ "engines": {
701
+ "node": ">=18"
702
+ }
703
+ },
704
+ "node_modules/@esbuild/win32-arm64": {
705
+ "version": "0.25.12",
706
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
707
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
708
+ "cpu": [
709
+ "arm64"
710
+ ],
711
+ "dev": true,
712
+ "license": "MIT",
713
+ "optional": true,
714
+ "os": [
715
+ "win32"
716
+ ],
717
+ "engines": {
718
+ "node": ">=18"
719
+ }
720
+ },
721
+ "node_modules/@esbuild/win32-ia32": {
722
+ "version": "0.25.12",
723
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
724
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
725
+ "cpu": [
726
+ "ia32"
727
+ ],
728
+ "dev": true,
729
+ "license": "MIT",
730
+ "optional": true,
731
+ "os": [
732
+ "win32"
733
+ ],
734
+ "engines": {
735
+ "node": ">=18"
736
+ }
737
+ },
738
+ "node_modules/@esbuild/win32-x64": {
739
+ "version": "0.25.12",
740
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
741
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
742
+ "cpu": [
743
+ "x64"
744
+ ],
745
+ "dev": true,
746
+ "license": "MIT",
747
+ "optional": true,
748
+ "os": [
749
+ "win32"
750
+ ],
751
+ "engines": {
752
+ "node": ">=18"
753
+ }
754
+ },
755
+ "node_modules/@jridgewell/gen-mapping": {
756
+ "version": "0.3.13",
757
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
758
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
759
+ "dev": true,
760
+ "license": "MIT",
761
+ "dependencies": {
762
+ "@jridgewell/sourcemap-codec": "^1.5.0",
763
+ "@jridgewell/trace-mapping": "^0.3.24"
764
+ }
765
+ },
766
+ "node_modules/@jridgewell/remapping": {
767
+ "version": "2.3.5",
768
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
769
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
770
+ "dev": true,
771
+ "license": "MIT",
772
+ "dependencies": {
773
+ "@jridgewell/gen-mapping": "^0.3.5",
774
+ "@jridgewell/trace-mapping": "^0.3.24"
775
+ }
776
+ },
777
+ "node_modules/@jridgewell/resolve-uri": {
778
+ "version": "3.1.2",
779
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
780
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
781
+ "dev": true,
782
+ "license": "MIT",
783
+ "engines": {
784
+ "node": ">=6.0.0"
785
+ }
786
+ },
787
+ "node_modules/@jridgewell/sourcemap-codec": {
788
+ "version": "1.5.5",
789
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
790
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
791
+ "dev": true,
792
+ "license": "MIT"
793
+ },
794
+ "node_modules/@jridgewell/trace-mapping": {
795
+ "version": "0.3.31",
796
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
797
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
798
+ "dev": true,
799
+ "license": "MIT",
800
+ "dependencies": {
801
+ "@jridgewell/resolve-uri": "^3.1.0",
802
+ "@jridgewell/sourcemap-codec": "^1.4.14"
803
+ }
804
+ },
805
+ "node_modules/@rolldown/pluginutils": {
806
+ "version": "1.0.0-beta.27",
807
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
808
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
809
+ "dev": true,
810
+ "license": "MIT"
811
+ },
812
+ "node_modules/@rollup/rollup-android-arm-eabi": {
813
+ "version": "4.62.0",
814
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz",
815
+ "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==",
816
+ "cpu": [
817
+ "arm"
818
+ ],
819
+ "dev": true,
820
+ "license": "MIT",
821
+ "optional": true,
822
+ "os": [
823
+ "android"
824
+ ]
825
+ },
826
+ "node_modules/@rollup/rollup-android-arm64": {
827
+ "version": "4.62.0",
828
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz",
829
+ "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==",
830
+ "cpu": [
831
+ "arm64"
832
+ ],
833
+ "dev": true,
834
+ "license": "MIT",
835
+ "optional": true,
836
+ "os": [
837
+ "android"
838
+ ]
839
+ },
840
+ "node_modules/@rollup/rollup-darwin-arm64": {
841
+ "version": "4.62.0",
842
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz",
843
+ "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==",
844
+ "cpu": [
845
+ "arm64"
846
+ ],
847
+ "dev": true,
848
+ "license": "MIT",
849
+ "optional": true,
850
+ "os": [
851
+ "darwin"
852
+ ]
853
+ },
854
+ "node_modules/@rollup/rollup-darwin-x64": {
855
+ "version": "4.62.0",
856
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz",
857
+ "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==",
858
+ "cpu": [
859
+ "x64"
860
+ ],
861
+ "dev": true,
862
+ "license": "MIT",
863
+ "optional": true,
864
+ "os": [
865
+ "darwin"
866
+ ]
867
+ },
868
+ "node_modules/@rollup/rollup-freebsd-arm64": {
869
+ "version": "4.62.0",
870
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz",
871
+ "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==",
872
+ "cpu": [
873
+ "arm64"
874
+ ],
875
+ "dev": true,
876
+ "license": "MIT",
877
+ "optional": true,
878
+ "os": [
879
+ "freebsd"
880
+ ]
881
+ },
882
+ "node_modules/@rollup/rollup-freebsd-x64": {
883
+ "version": "4.62.0",
884
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz",
885
+ "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==",
886
+ "cpu": [
887
+ "x64"
888
+ ],
889
+ "dev": true,
890
+ "license": "MIT",
891
+ "optional": true,
892
+ "os": [
893
+ "freebsd"
894
+ ]
895
+ },
896
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
897
+ "version": "4.62.0",
898
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz",
899
+ "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==",
900
+ "cpu": [
901
+ "arm"
902
+ ],
903
+ "dev": true,
904
+ "license": "MIT",
905
+ "optional": true,
906
+ "os": [
907
+ "linux"
908
+ ]
909
+ },
910
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
911
+ "version": "4.62.0",
912
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz",
913
+ "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==",
914
+ "cpu": [
915
+ "arm"
916
+ ],
917
+ "dev": true,
918
+ "license": "MIT",
919
+ "optional": true,
920
+ "os": [
921
+ "linux"
922
+ ]
923
+ },
924
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
925
+ "version": "4.62.0",
926
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz",
927
+ "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==",
928
+ "cpu": [
929
+ "arm64"
930
+ ],
931
+ "dev": true,
932
+ "license": "MIT",
933
+ "optional": true,
934
+ "os": [
935
+ "linux"
936
+ ]
937
+ },
938
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
939
+ "version": "4.62.0",
940
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz",
941
+ "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==",
942
+ "cpu": [
943
+ "arm64"
944
+ ],
945
+ "dev": true,
946
+ "license": "MIT",
947
+ "optional": true,
948
+ "os": [
949
+ "linux"
950
+ ]
951
+ },
952
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
953
+ "version": "4.62.0",
954
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz",
955
+ "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==",
956
+ "cpu": [
957
+ "loong64"
958
+ ],
959
+ "dev": true,
960
+ "license": "MIT",
961
+ "optional": true,
962
+ "os": [
963
+ "linux"
964
+ ]
965
+ },
966
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
967
+ "version": "4.62.0",
968
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz",
969
+ "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==",
970
+ "cpu": [
971
+ "loong64"
972
+ ],
973
+ "dev": true,
974
+ "license": "MIT",
975
+ "optional": true,
976
+ "os": [
977
+ "linux"
978
+ ]
979
+ },
980
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
981
+ "version": "4.62.0",
982
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz",
983
+ "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==",
984
+ "cpu": [
985
+ "ppc64"
986
+ ],
987
+ "dev": true,
988
+ "license": "MIT",
989
+ "optional": true,
990
+ "os": [
991
+ "linux"
992
+ ]
993
+ },
994
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
995
+ "version": "4.62.0",
996
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz",
997
+ "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==",
998
+ "cpu": [
999
+ "ppc64"
1000
+ ],
1001
+ "dev": true,
1002
+ "license": "MIT",
1003
+ "optional": true,
1004
+ "os": [
1005
+ "linux"
1006
+ ]
1007
+ },
1008
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
1009
+ "version": "4.62.0",
1010
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz",
1011
+ "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==",
1012
+ "cpu": [
1013
+ "riscv64"
1014
+ ],
1015
+ "dev": true,
1016
+ "license": "MIT",
1017
+ "optional": true,
1018
+ "os": [
1019
+ "linux"
1020
+ ]
1021
+ },
1022
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
1023
+ "version": "4.62.0",
1024
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz",
1025
+ "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==",
1026
+ "cpu": [
1027
+ "riscv64"
1028
+ ],
1029
+ "dev": true,
1030
+ "license": "MIT",
1031
+ "optional": true,
1032
+ "os": [
1033
+ "linux"
1034
+ ]
1035
+ },
1036
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
1037
+ "version": "4.62.0",
1038
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz",
1039
+ "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==",
1040
+ "cpu": [
1041
+ "s390x"
1042
+ ],
1043
+ "dev": true,
1044
+ "license": "MIT",
1045
+ "optional": true,
1046
+ "os": [
1047
+ "linux"
1048
+ ]
1049
+ },
1050
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
1051
+ "version": "4.62.0",
1052
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz",
1053
+ "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==",
1054
+ "cpu": [
1055
+ "x64"
1056
+ ],
1057
+ "dev": true,
1058
+ "license": "MIT",
1059
+ "optional": true,
1060
+ "os": [
1061
+ "linux"
1062
+ ]
1063
+ },
1064
+ "node_modules/@rollup/rollup-linux-x64-musl": {
1065
+ "version": "4.62.0",
1066
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz",
1067
+ "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==",
1068
+ "cpu": [
1069
+ "x64"
1070
+ ],
1071
+ "dev": true,
1072
+ "license": "MIT",
1073
+ "optional": true,
1074
+ "os": [
1075
+ "linux"
1076
+ ]
1077
+ },
1078
+ "node_modules/@rollup/rollup-openbsd-x64": {
1079
+ "version": "4.62.0",
1080
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz",
1081
+ "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==",
1082
+ "cpu": [
1083
+ "x64"
1084
+ ],
1085
+ "dev": true,
1086
+ "license": "MIT",
1087
+ "optional": true,
1088
+ "os": [
1089
+ "openbsd"
1090
+ ]
1091
+ },
1092
+ "node_modules/@rollup/rollup-openharmony-arm64": {
1093
+ "version": "4.62.0",
1094
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz",
1095
+ "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==",
1096
+ "cpu": [
1097
+ "arm64"
1098
+ ],
1099
+ "dev": true,
1100
+ "license": "MIT",
1101
+ "optional": true,
1102
+ "os": [
1103
+ "openharmony"
1104
+ ]
1105
+ },
1106
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1107
+ "version": "4.62.0",
1108
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz",
1109
+ "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==",
1110
+ "cpu": [
1111
+ "arm64"
1112
+ ],
1113
+ "dev": true,
1114
+ "license": "MIT",
1115
+ "optional": true,
1116
+ "os": [
1117
+ "win32"
1118
+ ]
1119
+ },
1120
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1121
+ "version": "4.62.0",
1122
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz",
1123
+ "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==",
1124
+ "cpu": [
1125
+ "ia32"
1126
+ ],
1127
+ "dev": true,
1128
+ "license": "MIT",
1129
+ "optional": true,
1130
+ "os": [
1131
+ "win32"
1132
+ ]
1133
+ },
1134
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1135
+ "version": "4.62.0",
1136
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz",
1137
+ "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==",
1138
+ "cpu": [
1139
+ "x64"
1140
+ ],
1141
+ "dev": true,
1142
+ "license": "MIT",
1143
+ "optional": true,
1144
+ "os": [
1145
+ "win32"
1146
+ ]
1147
+ },
1148
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1149
+ "version": "4.62.0",
1150
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz",
1151
+ "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==",
1152
+ "cpu": [
1153
+ "x64"
1154
+ ],
1155
+ "dev": true,
1156
+ "license": "MIT",
1157
+ "optional": true,
1158
+ "os": [
1159
+ "win32"
1160
+ ]
1161
+ },
1162
+ "node_modules/@tailwindcss/node": {
1163
+ "version": "4.3.1",
1164
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz",
1165
+ "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==",
1166
+ "dev": true,
1167
+ "license": "MIT",
1168
+ "dependencies": {
1169
+ "@jridgewell/remapping": "^2.3.5",
1170
+ "enhanced-resolve": "5.21.6",
1171
+ "jiti": "^2.7.0",
1172
+ "lightningcss": "1.32.0",
1173
+ "magic-string": "^0.30.21",
1174
+ "source-map-js": "^1.2.1",
1175
+ "tailwindcss": "4.3.1"
1176
+ }
1177
+ },
1178
+ "node_modules/@tailwindcss/oxide": {
1179
+ "version": "4.3.1",
1180
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz",
1181
+ "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==",
1182
+ "dev": true,
1183
+ "license": "MIT",
1184
+ "engines": {
1185
+ "node": ">= 20"
1186
+ },
1187
+ "optionalDependencies": {
1188
+ "@tailwindcss/oxide-android-arm64": "4.3.1",
1189
+ "@tailwindcss/oxide-darwin-arm64": "4.3.1",
1190
+ "@tailwindcss/oxide-darwin-x64": "4.3.1",
1191
+ "@tailwindcss/oxide-freebsd-x64": "4.3.1",
1192
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1",
1193
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1",
1194
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.1",
1195
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.1",
1196
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.1",
1197
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.1",
1198
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1",
1199
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.1"
1200
+ }
1201
+ },
1202
+ "node_modules/@tailwindcss/oxide-android-arm64": {
1203
+ "version": "4.3.1",
1204
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz",
1205
+ "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==",
1206
+ "cpu": [
1207
+ "arm64"
1208
+ ],
1209
+ "dev": true,
1210
+ "license": "MIT",
1211
+ "optional": true,
1212
+ "os": [
1213
+ "android"
1214
+ ],
1215
+ "engines": {
1216
+ "node": ">= 20"
1217
+ }
1218
+ },
1219
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
1220
+ "version": "4.3.1",
1221
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz",
1222
+ "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==",
1223
+ "cpu": [
1224
+ "arm64"
1225
+ ],
1226
+ "dev": true,
1227
+ "license": "MIT",
1228
+ "optional": true,
1229
+ "os": [
1230
+ "darwin"
1231
+ ],
1232
+ "engines": {
1233
+ "node": ">= 20"
1234
+ }
1235
+ },
1236
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
1237
+ "version": "4.3.1",
1238
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz",
1239
+ "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==",
1240
+ "cpu": [
1241
+ "x64"
1242
+ ],
1243
+ "dev": true,
1244
+ "license": "MIT",
1245
+ "optional": true,
1246
+ "os": [
1247
+ "darwin"
1248
+ ],
1249
+ "engines": {
1250
+ "node": ">= 20"
1251
+ }
1252
+ },
1253
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
1254
+ "version": "4.3.1",
1255
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz",
1256
+ "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==",
1257
+ "cpu": [
1258
+ "x64"
1259
+ ],
1260
+ "dev": true,
1261
+ "license": "MIT",
1262
+ "optional": true,
1263
+ "os": [
1264
+ "freebsd"
1265
+ ],
1266
+ "engines": {
1267
+ "node": ">= 20"
1268
+ }
1269
+ },
1270
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
1271
+ "version": "4.3.1",
1272
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz",
1273
+ "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==",
1274
+ "cpu": [
1275
+ "arm"
1276
+ ],
1277
+ "dev": true,
1278
+ "license": "MIT",
1279
+ "optional": true,
1280
+ "os": [
1281
+ "linux"
1282
+ ],
1283
+ "engines": {
1284
+ "node": ">= 20"
1285
+ }
1286
+ },
1287
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
1288
+ "version": "4.3.1",
1289
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz",
1290
+ "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==",
1291
+ "cpu": [
1292
+ "arm64"
1293
+ ],
1294
+ "dev": true,
1295
+ "license": "MIT",
1296
+ "optional": true,
1297
+ "os": [
1298
+ "linux"
1299
+ ],
1300
+ "engines": {
1301
+ "node": ">= 20"
1302
+ }
1303
+ },
1304
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
1305
+ "version": "4.3.1",
1306
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz",
1307
+ "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==",
1308
+ "cpu": [
1309
+ "arm64"
1310
+ ],
1311
+ "dev": true,
1312
+ "license": "MIT",
1313
+ "optional": true,
1314
+ "os": [
1315
+ "linux"
1316
+ ],
1317
+ "engines": {
1318
+ "node": ">= 20"
1319
+ }
1320
+ },
1321
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
1322
+ "version": "4.3.1",
1323
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz",
1324
+ "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==",
1325
+ "cpu": [
1326
+ "x64"
1327
+ ],
1328
+ "dev": true,
1329
+ "license": "MIT",
1330
+ "optional": true,
1331
+ "os": [
1332
+ "linux"
1333
+ ],
1334
+ "engines": {
1335
+ "node": ">= 20"
1336
+ }
1337
+ },
1338
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
1339
+ "version": "4.3.1",
1340
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz",
1341
+ "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==",
1342
+ "cpu": [
1343
+ "x64"
1344
+ ],
1345
+ "dev": true,
1346
+ "license": "MIT",
1347
+ "optional": true,
1348
+ "os": [
1349
+ "linux"
1350
+ ],
1351
+ "engines": {
1352
+ "node": ">= 20"
1353
+ }
1354
+ },
1355
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
1356
+ "version": "4.3.1",
1357
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz",
1358
+ "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==",
1359
+ "bundleDependencies": [
1360
+ "@napi-rs/wasm-runtime",
1361
+ "@emnapi/core",
1362
+ "@emnapi/runtime",
1363
+ "@tybys/wasm-util",
1364
+ "@emnapi/wasi-threads",
1365
+ "tslib"
1366
+ ],
1367
+ "cpu": [
1368
+ "wasm32"
1369
+ ],
1370
+ "dev": true,
1371
+ "license": "MIT",
1372
+ "optional": true,
1373
+ "dependencies": {
1374
+ "@emnapi/core": "^1.10.0",
1375
+ "@emnapi/runtime": "^1.10.0",
1376
+ "@emnapi/wasi-threads": "^1.2.1",
1377
+ "@napi-rs/wasm-runtime": "^1.1.4",
1378
+ "@tybys/wasm-util": "^0.10.2",
1379
+ "tslib": "^2.8.1"
1380
+ },
1381
+ "engines": {
1382
+ "node": ">=14.0.0"
1383
+ }
1384
+ },
1385
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
1386
+ "version": "4.3.1",
1387
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz",
1388
+ "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==",
1389
+ "cpu": [
1390
+ "arm64"
1391
+ ],
1392
+ "dev": true,
1393
+ "license": "MIT",
1394
+ "optional": true,
1395
+ "os": [
1396
+ "win32"
1397
+ ],
1398
+ "engines": {
1399
+ "node": ">= 20"
1400
+ }
1401
+ },
1402
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
1403
+ "version": "4.3.1",
1404
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz",
1405
+ "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==",
1406
+ "cpu": [
1407
+ "x64"
1408
+ ],
1409
+ "dev": true,
1410
+ "license": "MIT",
1411
+ "optional": true,
1412
+ "os": [
1413
+ "win32"
1414
+ ],
1415
+ "engines": {
1416
+ "node": ">= 20"
1417
+ }
1418
+ },
1419
+ "node_modules/@tailwindcss/vite": {
1420
+ "version": "4.3.1",
1421
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz",
1422
+ "integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==",
1423
+ "dev": true,
1424
+ "license": "MIT",
1425
+ "dependencies": {
1426
+ "@tailwindcss/node": "4.3.1",
1427
+ "@tailwindcss/oxide": "4.3.1",
1428
+ "tailwindcss": "4.3.1"
1429
+ },
1430
+ "peerDependencies": {
1431
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
1432
+ }
1433
+ },
1434
+ "node_modules/@types/babel__core": {
1435
+ "version": "7.20.5",
1436
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1437
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1438
+ "dev": true,
1439
+ "license": "MIT",
1440
+ "dependencies": {
1441
+ "@babel/parser": "^7.20.7",
1442
+ "@babel/types": "^7.20.7",
1443
+ "@types/babel__generator": "*",
1444
+ "@types/babel__template": "*",
1445
+ "@types/babel__traverse": "*"
1446
+ }
1447
+ },
1448
+ "node_modules/@types/babel__generator": {
1449
+ "version": "7.27.0",
1450
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1451
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1452
+ "dev": true,
1453
+ "license": "MIT",
1454
+ "dependencies": {
1455
+ "@babel/types": "^7.0.0"
1456
+ }
1457
+ },
1458
+ "node_modules/@types/babel__template": {
1459
+ "version": "7.4.4",
1460
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1461
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1462
+ "dev": true,
1463
+ "license": "MIT",
1464
+ "dependencies": {
1465
+ "@babel/parser": "^7.1.0",
1466
+ "@babel/types": "^7.0.0"
1467
+ }
1468
+ },
1469
+ "node_modules/@types/babel__traverse": {
1470
+ "version": "7.28.0",
1471
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1472
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1473
+ "dev": true,
1474
+ "license": "MIT",
1475
+ "dependencies": {
1476
+ "@babel/types": "^7.28.2"
1477
+ }
1478
+ },
1479
+ "node_modules/@types/d3-array": {
1480
+ "version": "3.2.2",
1481
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
1482
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
1483
+ "license": "MIT"
1484
+ },
1485
+ "node_modules/@types/d3-color": {
1486
+ "version": "3.1.3",
1487
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
1488
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
1489
+ "license": "MIT"
1490
+ },
1491
+ "node_modules/@types/d3-ease": {
1492
+ "version": "3.0.2",
1493
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
1494
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
1495
+ "license": "MIT"
1496
+ },
1497
+ "node_modules/@types/d3-interpolate": {
1498
+ "version": "3.0.4",
1499
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
1500
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
1501
+ "license": "MIT",
1502
+ "dependencies": {
1503
+ "@types/d3-color": "*"
1504
+ }
1505
+ },
1506
+ "node_modules/@types/d3-path": {
1507
+ "version": "3.1.1",
1508
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
1509
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
1510
+ "license": "MIT"
1511
+ },
1512
+ "node_modules/@types/d3-scale": {
1513
+ "version": "4.0.9",
1514
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
1515
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
1516
+ "license": "MIT",
1517
+ "dependencies": {
1518
+ "@types/d3-time": "*"
1519
+ }
1520
+ },
1521
+ "node_modules/@types/d3-shape": {
1522
+ "version": "3.1.8",
1523
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
1524
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
1525
+ "license": "MIT",
1526
+ "dependencies": {
1527
+ "@types/d3-path": "*"
1528
+ }
1529
+ },
1530
+ "node_modules/@types/d3-time": {
1531
+ "version": "3.0.4",
1532
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
1533
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
1534
+ "license": "MIT"
1535
+ },
1536
+ "node_modules/@types/d3-timer": {
1537
+ "version": "3.0.2",
1538
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
1539
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
1540
+ "license": "MIT"
1541
+ },
1542
+ "node_modules/@types/estree": {
1543
+ "version": "1.0.9",
1544
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
1545
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
1546
+ "dev": true,
1547
+ "license": "MIT"
1548
+ },
1549
+ "node_modules/@vitejs/plugin-react": {
1550
+ "version": "4.7.0",
1551
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
1552
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
1553
+ "dev": true,
1554
+ "license": "MIT",
1555
+ "dependencies": {
1556
+ "@babel/core": "^7.28.0",
1557
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1558
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1559
+ "@rolldown/pluginutils": "1.0.0-beta.27",
1560
+ "@types/babel__core": "^7.20.5",
1561
+ "react-refresh": "^0.17.0"
1562
+ },
1563
+ "engines": {
1564
+ "node": "^14.18.0 || >=16.0.0"
1565
+ },
1566
+ "peerDependencies": {
1567
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1568
+ }
1569
+ },
1570
+ "node_modules/baseline-browser-mapping": {
1571
+ "version": "2.10.37",
1572
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz",
1573
+ "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==",
1574
+ "dev": true,
1575
+ "license": "Apache-2.0",
1576
+ "bin": {
1577
+ "baseline-browser-mapping": "dist/cli.cjs"
1578
+ },
1579
+ "engines": {
1580
+ "node": ">=6.0.0"
1581
+ }
1582
+ },
1583
+ "node_modules/browserslist": {
1584
+ "version": "4.28.2",
1585
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
1586
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
1587
+ "dev": true,
1588
+ "funding": [
1589
+ {
1590
+ "type": "opencollective",
1591
+ "url": "https://opencollective.com/browserslist"
1592
+ },
1593
+ {
1594
+ "type": "tidelift",
1595
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1596
+ },
1597
+ {
1598
+ "type": "github",
1599
+ "url": "https://github.com/sponsors/ai"
1600
+ }
1601
+ ],
1602
+ "license": "MIT",
1603
+ "dependencies": {
1604
+ "baseline-browser-mapping": "^2.10.12",
1605
+ "caniuse-lite": "^1.0.30001782",
1606
+ "electron-to-chromium": "^1.5.328",
1607
+ "node-releases": "^2.0.36",
1608
+ "update-browserslist-db": "^1.2.3"
1609
+ },
1610
+ "bin": {
1611
+ "browserslist": "cli.js"
1612
+ },
1613
+ "engines": {
1614
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1615
+ }
1616
+ },
1617
+ "node_modules/caniuse-lite": {
1618
+ "version": "1.0.30001799",
1619
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
1620
+ "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
1621
+ "dev": true,
1622
+ "funding": [
1623
+ {
1624
+ "type": "opencollective",
1625
+ "url": "https://opencollective.com/browserslist"
1626
+ },
1627
+ {
1628
+ "type": "tidelift",
1629
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1630
+ },
1631
+ {
1632
+ "type": "github",
1633
+ "url": "https://github.com/sponsors/ai"
1634
+ }
1635
+ ],
1636
+ "license": "CC-BY-4.0"
1637
+ },
1638
+ "node_modules/clsx": {
1639
+ "version": "2.1.1",
1640
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
1641
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
1642
+ "license": "MIT",
1643
+ "engines": {
1644
+ "node": ">=6"
1645
+ }
1646
+ },
1647
+ "node_modules/convert-source-map": {
1648
+ "version": "2.0.0",
1649
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1650
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1651
+ "dev": true,
1652
+ "license": "MIT"
1653
+ },
1654
+ "node_modules/csstype": {
1655
+ "version": "3.2.3",
1656
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1657
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1658
+ "license": "MIT"
1659
+ },
1660
+ "node_modules/d3-array": {
1661
+ "version": "3.2.4",
1662
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
1663
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
1664
+ "license": "ISC",
1665
+ "dependencies": {
1666
+ "internmap": "1 - 2"
1667
+ },
1668
+ "engines": {
1669
+ "node": ">=12"
1670
+ }
1671
+ },
1672
+ "node_modules/d3-color": {
1673
+ "version": "3.1.0",
1674
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
1675
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
1676
+ "license": "ISC",
1677
+ "engines": {
1678
+ "node": ">=12"
1679
+ }
1680
+ },
1681
+ "node_modules/d3-ease": {
1682
+ "version": "3.0.1",
1683
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
1684
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
1685
+ "license": "BSD-3-Clause",
1686
+ "engines": {
1687
+ "node": ">=12"
1688
+ }
1689
+ },
1690
+ "node_modules/d3-format": {
1691
+ "version": "3.1.2",
1692
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
1693
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
1694
+ "license": "ISC",
1695
+ "engines": {
1696
+ "node": ">=12"
1697
+ }
1698
+ },
1699
+ "node_modules/d3-interpolate": {
1700
+ "version": "3.0.1",
1701
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
1702
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
1703
+ "license": "ISC",
1704
+ "dependencies": {
1705
+ "d3-color": "1 - 3"
1706
+ },
1707
+ "engines": {
1708
+ "node": ">=12"
1709
+ }
1710
+ },
1711
+ "node_modules/d3-path": {
1712
+ "version": "3.1.0",
1713
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
1714
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
1715
+ "license": "ISC",
1716
+ "engines": {
1717
+ "node": ">=12"
1718
+ }
1719
+ },
1720
+ "node_modules/d3-scale": {
1721
+ "version": "4.0.2",
1722
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
1723
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
1724
+ "license": "ISC",
1725
+ "dependencies": {
1726
+ "d3-array": "2.10.0 - 3",
1727
+ "d3-format": "1 - 3",
1728
+ "d3-interpolate": "1.2.0 - 3",
1729
+ "d3-time": "2.1.1 - 3",
1730
+ "d3-time-format": "2 - 4"
1731
+ },
1732
+ "engines": {
1733
+ "node": ">=12"
1734
+ }
1735
+ },
1736
+ "node_modules/d3-shape": {
1737
+ "version": "3.2.0",
1738
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
1739
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
1740
+ "license": "ISC",
1741
+ "dependencies": {
1742
+ "d3-path": "^3.1.0"
1743
+ },
1744
+ "engines": {
1745
+ "node": ">=12"
1746
+ }
1747
+ },
1748
+ "node_modules/d3-time": {
1749
+ "version": "3.1.0",
1750
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
1751
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
1752
+ "license": "ISC",
1753
+ "dependencies": {
1754
+ "d3-array": "2 - 3"
1755
+ },
1756
+ "engines": {
1757
+ "node": ">=12"
1758
+ }
1759
+ },
1760
+ "node_modules/d3-time-format": {
1761
+ "version": "4.1.0",
1762
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
1763
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
1764
+ "license": "ISC",
1765
+ "dependencies": {
1766
+ "d3-time": "1 - 3"
1767
+ },
1768
+ "engines": {
1769
+ "node": ">=12"
1770
+ }
1771
+ },
1772
+ "node_modules/d3-timer": {
1773
+ "version": "3.0.1",
1774
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
1775
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
1776
+ "license": "ISC",
1777
+ "engines": {
1778
+ "node": ">=12"
1779
+ }
1780
+ },
1781
+ "node_modules/debug": {
1782
+ "version": "4.4.3",
1783
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1784
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1785
+ "dev": true,
1786
+ "license": "MIT",
1787
+ "dependencies": {
1788
+ "ms": "^2.1.3"
1789
+ },
1790
+ "engines": {
1791
+ "node": ">=6.0"
1792
+ },
1793
+ "peerDependenciesMeta": {
1794
+ "supports-color": {
1795
+ "optional": true
1796
+ }
1797
+ }
1798
+ },
1799
+ "node_modules/decimal.js-light": {
1800
+ "version": "2.5.1",
1801
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
1802
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
1803
+ "license": "MIT"
1804
+ },
1805
+ "node_modules/detect-libc": {
1806
+ "version": "2.1.2",
1807
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
1808
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
1809
+ "dev": true,
1810
+ "license": "Apache-2.0",
1811
+ "engines": {
1812
+ "node": ">=8"
1813
+ }
1814
+ },
1815
+ "node_modules/dom-helpers": {
1816
+ "version": "5.2.1",
1817
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
1818
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
1819
+ "license": "MIT",
1820
+ "dependencies": {
1821
+ "@babel/runtime": "^7.8.7",
1822
+ "csstype": "^3.0.2"
1823
+ }
1824
+ },
1825
+ "node_modules/electron-to-chromium": {
1826
+ "version": "1.5.375",
1827
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz",
1828
+ "integrity": "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==",
1829
+ "dev": true,
1830
+ "license": "ISC"
1831
+ },
1832
+ "node_modules/enhanced-resolve": {
1833
+ "version": "5.21.6",
1834
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
1835
+ "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
1836
+ "dev": true,
1837
+ "license": "MIT",
1838
+ "dependencies": {
1839
+ "graceful-fs": "^4.2.4",
1840
+ "tapable": "^2.3.3"
1841
+ },
1842
+ "engines": {
1843
+ "node": ">=10.13.0"
1844
+ }
1845
+ },
1846
+ "node_modules/esbuild": {
1847
+ "version": "0.25.12",
1848
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
1849
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
1850
+ "dev": true,
1851
+ "hasInstallScript": true,
1852
+ "license": "MIT",
1853
+ "bin": {
1854
+ "esbuild": "bin/esbuild"
1855
+ },
1856
+ "engines": {
1857
+ "node": ">=18"
1858
+ },
1859
+ "optionalDependencies": {
1860
+ "@esbuild/aix-ppc64": "0.25.12",
1861
+ "@esbuild/android-arm": "0.25.12",
1862
+ "@esbuild/android-arm64": "0.25.12",
1863
+ "@esbuild/android-x64": "0.25.12",
1864
+ "@esbuild/darwin-arm64": "0.25.12",
1865
+ "@esbuild/darwin-x64": "0.25.12",
1866
+ "@esbuild/freebsd-arm64": "0.25.12",
1867
+ "@esbuild/freebsd-x64": "0.25.12",
1868
+ "@esbuild/linux-arm": "0.25.12",
1869
+ "@esbuild/linux-arm64": "0.25.12",
1870
+ "@esbuild/linux-ia32": "0.25.12",
1871
+ "@esbuild/linux-loong64": "0.25.12",
1872
+ "@esbuild/linux-mips64el": "0.25.12",
1873
+ "@esbuild/linux-ppc64": "0.25.12",
1874
+ "@esbuild/linux-riscv64": "0.25.12",
1875
+ "@esbuild/linux-s390x": "0.25.12",
1876
+ "@esbuild/linux-x64": "0.25.12",
1877
+ "@esbuild/netbsd-arm64": "0.25.12",
1878
+ "@esbuild/netbsd-x64": "0.25.12",
1879
+ "@esbuild/openbsd-arm64": "0.25.12",
1880
+ "@esbuild/openbsd-x64": "0.25.12",
1881
+ "@esbuild/openharmony-arm64": "0.25.12",
1882
+ "@esbuild/sunos-x64": "0.25.12",
1883
+ "@esbuild/win32-arm64": "0.25.12",
1884
+ "@esbuild/win32-ia32": "0.25.12",
1885
+ "@esbuild/win32-x64": "0.25.12"
1886
+ }
1887
+ },
1888
+ "node_modules/escalade": {
1889
+ "version": "3.2.0",
1890
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1891
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1892
+ "dev": true,
1893
+ "license": "MIT",
1894
+ "engines": {
1895
+ "node": ">=6"
1896
+ }
1897
+ },
1898
+ "node_modules/eventemitter3": {
1899
+ "version": "4.0.7",
1900
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
1901
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
1902
+ "license": "MIT"
1903
+ },
1904
+ "node_modules/fast-equals": {
1905
+ "version": "5.4.0",
1906
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
1907
+ "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
1908
+ "license": "MIT",
1909
+ "engines": {
1910
+ "node": ">=6.0.0"
1911
+ }
1912
+ },
1913
+ "node_modules/fdir": {
1914
+ "version": "6.5.0",
1915
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1916
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1917
+ "dev": true,
1918
+ "license": "MIT",
1919
+ "engines": {
1920
+ "node": ">=12.0.0"
1921
+ },
1922
+ "peerDependencies": {
1923
+ "picomatch": "^3 || ^4"
1924
+ },
1925
+ "peerDependenciesMeta": {
1926
+ "picomatch": {
1927
+ "optional": true
1928
+ }
1929
+ }
1930
+ },
1931
+ "node_modules/fsevents": {
1932
+ "version": "2.3.3",
1933
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1934
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1935
+ "dev": true,
1936
+ "hasInstallScript": true,
1937
+ "license": "MIT",
1938
+ "optional": true,
1939
+ "os": [
1940
+ "darwin"
1941
+ ],
1942
+ "engines": {
1943
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1944
+ }
1945
+ },
1946
+ "node_modules/gensync": {
1947
+ "version": "1.0.0-beta.2",
1948
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1949
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1950
+ "dev": true,
1951
+ "license": "MIT",
1952
+ "engines": {
1953
+ "node": ">=6.9.0"
1954
+ }
1955
+ },
1956
+ "node_modules/graceful-fs": {
1957
+ "version": "4.2.11",
1958
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
1959
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
1960
+ "dev": true,
1961
+ "license": "ISC"
1962
+ },
1963
+ "node_modules/internmap": {
1964
+ "version": "2.0.3",
1965
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
1966
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
1967
+ "license": "ISC",
1968
+ "engines": {
1969
+ "node": ">=12"
1970
+ }
1971
+ },
1972
+ "node_modules/jiti": {
1973
+ "version": "2.7.0",
1974
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
1975
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
1976
+ "dev": true,
1977
+ "license": "MIT",
1978
+ "bin": {
1979
+ "jiti": "lib/jiti-cli.mjs"
1980
+ }
1981
+ },
1982
+ "node_modules/js-tokens": {
1983
+ "version": "4.0.0",
1984
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1985
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1986
+ "license": "MIT"
1987
+ },
1988
+ "node_modules/jsesc": {
1989
+ "version": "3.1.0",
1990
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1991
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1992
+ "dev": true,
1993
+ "license": "MIT",
1994
+ "bin": {
1995
+ "jsesc": "bin/jsesc"
1996
+ },
1997
+ "engines": {
1998
+ "node": ">=6"
1999
+ }
2000
+ },
2001
+ "node_modules/json5": {
2002
+ "version": "2.2.3",
2003
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
2004
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
2005
+ "dev": true,
2006
+ "license": "MIT",
2007
+ "bin": {
2008
+ "json5": "lib/cli.js"
2009
+ },
2010
+ "engines": {
2011
+ "node": ">=6"
2012
+ }
2013
+ },
2014
+ "node_modules/lightningcss": {
2015
+ "version": "1.32.0",
2016
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
2017
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
2018
+ "dev": true,
2019
+ "license": "MPL-2.0",
2020
+ "dependencies": {
2021
+ "detect-libc": "^2.0.3"
2022
+ },
2023
+ "engines": {
2024
+ "node": ">= 12.0.0"
2025
+ },
2026
+ "funding": {
2027
+ "type": "opencollective",
2028
+ "url": "https://opencollective.com/parcel"
2029
+ },
2030
+ "optionalDependencies": {
2031
+ "lightningcss-android-arm64": "1.32.0",
2032
+ "lightningcss-darwin-arm64": "1.32.0",
2033
+ "lightningcss-darwin-x64": "1.32.0",
2034
+ "lightningcss-freebsd-x64": "1.32.0",
2035
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
2036
+ "lightningcss-linux-arm64-gnu": "1.32.0",
2037
+ "lightningcss-linux-arm64-musl": "1.32.0",
2038
+ "lightningcss-linux-x64-gnu": "1.32.0",
2039
+ "lightningcss-linux-x64-musl": "1.32.0",
2040
+ "lightningcss-win32-arm64-msvc": "1.32.0",
2041
+ "lightningcss-win32-x64-msvc": "1.32.0"
2042
+ }
2043
+ },
2044
+ "node_modules/lightningcss-android-arm64": {
2045
+ "version": "1.32.0",
2046
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
2047
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
2048
+ "cpu": [
2049
+ "arm64"
2050
+ ],
2051
+ "dev": true,
2052
+ "license": "MPL-2.0",
2053
+ "optional": true,
2054
+ "os": [
2055
+ "android"
2056
+ ],
2057
+ "engines": {
2058
+ "node": ">= 12.0.0"
2059
+ },
2060
+ "funding": {
2061
+ "type": "opencollective",
2062
+ "url": "https://opencollective.com/parcel"
2063
+ }
2064
+ },
2065
+ "node_modules/lightningcss-darwin-arm64": {
2066
+ "version": "1.32.0",
2067
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
2068
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
2069
+ "cpu": [
2070
+ "arm64"
2071
+ ],
2072
+ "dev": true,
2073
+ "license": "MPL-2.0",
2074
+ "optional": true,
2075
+ "os": [
2076
+ "darwin"
2077
+ ],
2078
+ "engines": {
2079
+ "node": ">= 12.0.0"
2080
+ },
2081
+ "funding": {
2082
+ "type": "opencollective",
2083
+ "url": "https://opencollective.com/parcel"
2084
+ }
2085
+ },
2086
+ "node_modules/lightningcss-darwin-x64": {
2087
+ "version": "1.32.0",
2088
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
2089
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
2090
+ "cpu": [
2091
+ "x64"
2092
+ ],
2093
+ "dev": true,
2094
+ "license": "MPL-2.0",
2095
+ "optional": true,
2096
+ "os": [
2097
+ "darwin"
2098
+ ],
2099
+ "engines": {
2100
+ "node": ">= 12.0.0"
2101
+ },
2102
+ "funding": {
2103
+ "type": "opencollective",
2104
+ "url": "https://opencollective.com/parcel"
2105
+ }
2106
+ },
2107
+ "node_modules/lightningcss-freebsd-x64": {
2108
+ "version": "1.32.0",
2109
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
2110
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
2111
+ "cpu": [
2112
+ "x64"
2113
+ ],
2114
+ "dev": true,
2115
+ "license": "MPL-2.0",
2116
+ "optional": true,
2117
+ "os": [
2118
+ "freebsd"
2119
+ ],
2120
+ "engines": {
2121
+ "node": ">= 12.0.0"
2122
+ },
2123
+ "funding": {
2124
+ "type": "opencollective",
2125
+ "url": "https://opencollective.com/parcel"
2126
+ }
2127
+ },
2128
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
2129
+ "version": "1.32.0",
2130
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
2131
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
2132
+ "cpu": [
2133
+ "arm"
2134
+ ],
2135
+ "dev": true,
2136
+ "license": "MPL-2.0",
2137
+ "optional": true,
2138
+ "os": [
2139
+ "linux"
2140
+ ],
2141
+ "engines": {
2142
+ "node": ">= 12.0.0"
2143
+ },
2144
+ "funding": {
2145
+ "type": "opencollective",
2146
+ "url": "https://opencollective.com/parcel"
2147
+ }
2148
+ },
2149
+ "node_modules/lightningcss-linux-arm64-gnu": {
2150
+ "version": "1.32.0",
2151
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
2152
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
2153
+ "cpu": [
2154
+ "arm64"
2155
+ ],
2156
+ "dev": true,
2157
+ "license": "MPL-2.0",
2158
+ "optional": true,
2159
+ "os": [
2160
+ "linux"
2161
+ ],
2162
+ "engines": {
2163
+ "node": ">= 12.0.0"
2164
+ },
2165
+ "funding": {
2166
+ "type": "opencollective",
2167
+ "url": "https://opencollective.com/parcel"
2168
+ }
2169
+ },
2170
+ "node_modules/lightningcss-linux-arm64-musl": {
2171
+ "version": "1.32.0",
2172
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
2173
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
2174
+ "cpu": [
2175
+ "arm64"
2176
+ ],
2177
+ "dev": true,
2178
+ "license": "MPL-2.0",
2179
+ "optional": true,
2180
+ "os": [
2181
+ "linux"
2182
+ ],
2183
+ "engines": {
2184
+ "node": ">= 12.0.0"
2185
+ },
2186
+ "funding": {
2187
+ "type": "opencollective",
2188
+ "url": "https://opencollective.com/parcel"
2189
+ }
2190
+ },
2191
+ "node_modules/lightningcss-linux-x64-gnu": {
2192
+ "version": "1.32.0",
2193
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
2194
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
2195
+ "cpu": [
2196
+ "x64"
2197
+ ],
2198
+ "dev": true,
2199
+ "license": "MPL-2.0",
2200
+ "optional": true,
2201
+ "os": [
2202
+ "linux"
2203
+ ],
2204
+ "engines": {
2205
+ "node": ">= 12.0.0"
2206
+ },
2207
+ "funding": {
2208
+ "type": "opencollective",
2209
+ "url": "https://opencollective.com/parcel"
2210
+ }
2211
+ },
2212
+ "node_modules/lightningcss-linux-x64-musl": {
2213
+ "version": "1.32.0",
2214
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
2215
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
2216
+ "cpu": [
2217
+ "x64"
2218
+ ],
2219
+ "dev": true,
2220
+ "license": "MPL-2.0",
2221
+ "optional": true,
2222
+ "os": [
2223
+ "linux"
2224
+ ],
2225
+ "engines": {
2226
+ "node": ">= 12.0.0"
2227
+ },
2228
+ "funding": {
2229
+ "type": "opencollective",
2230
+ "url": "https://opencollective.com/parcel"
2231
+ }
2232
+ },
2233
+ "node_modules/lightningcss-win32-arm64-msvc": {
2234
+ "version": "1.32.0",
2235
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
2236
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
2237
+ "cpu": [
2238
+ "arm64"
2239
+ ],
2240
+ "dev": true,
2241
+ "license": "MPL-2.0",
2242
+ "optional": true,
2243
+ "os": [
2244
+ "win32"
2245
+ ],
2246
+ "engines": {
2247
+ "node": ">= 12.0.0"
2248
+ },
2249
+ "funding": {
2250
+ "type": "opencollective",
2251
+ "url": "https://opencollective.com/parcel"
2252
+ }
2253
+ },
2254
+ "node_modules/lightningcss-win32-x64-msvc": {
2255
+ "version": "1.32.0",
2256
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
2257
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
2258
+ "cpu": [
2259
+ "x64"
2260
+ ],
2261
+ "dev": true,
2262
+ "license": "MPL-2.0",
2263
+ "optional": true,
2264
+ "os": [
2265
+ "win32"
2266
+ ],
2267
+ "engines": {
2268
+ "node": ">= 12.0.0"
2269
+ },
2270
+ "funding": {
2271
+ "type": "opencollective",
2272
+ "url": "https://opencollective.com/parcel"
2273
+ }
2274
+ },
2275
+ "node_modules/lodash": {
2276
+ "version": "4.18.1",
2277
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
2278
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
2279
+ "license": "MIT"
2280
+ },
2281
+ "node_modules/loose-envify": {
2282
+ "version": "1.4.0",
2283
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
2284
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
2285
+ "license": "MIT",
2286
+ "dependencies": {
2287
+ "js-tokens": "^3.0.0 || ^4.0.0"
2288
+ },
2289
+ "bin": {
2290
+ "loose-envify": "cli.js"
2291
+ }
2292
+ },
2293
+ "node_modules/lru-cache": {
2294
+ "version": "5.1.1",
2295
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
2296
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
2297
+ "dev": true,
2298
+ "license": "ISC",
2299
+ "dependencies": {
2300
+ "yallist": "^3.0.2"
2301
+ }
2302
+ },
2303
+ "node_modules/magic-string": {
2304
+ "version": "0.30.21",
2305
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
2306
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
2307
+ "dev": true,
2308
+ "license": "MIT",
2309
+ "dependencies": {
2310
+ "@jridgewell/sourcemap-codec": "^1.5.5"
2311
+ }
2312
+ },
2313
+ "node_modules/ms": {
2314
+ "version": "2.1.3",
2315
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
2316
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
2317
+ "dev": true,
2318
+ "license": "MIT"
2319
+ },
2320
+ "node_modules/nanoid": {
2321
+ "version": "3.3.12",
2322
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
2323
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
2324
+ "dev": true,
2325
+ "funding": [
2326
+ {
2327
+ "type": "github",
2328
+ "url": "https://github.com/sponsors/ai"
2329
+ }
2330
+ ],
2331
+ "license": "MIT",
2332
+ "bin": {
2333
+ "nanoid": "bin/nanoid.cjs"
2334
+ },
2335
+ "engines": {
2336
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
2337
+ }
2338
+ },
2339
+ "node_modules/node-releases": {
2340
+ "version": "2.0.47",
2341
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
2342
+ "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==",
2343
+ "dev": true,
2344
+ "license": "MIT",
2345
+ "engines": {
2346
+ "node": ">=18"
2347
+ }
2348
+ },
2349
+ "node_modules/object-assign": {
2350
+ "version": "4.1.1",
2351
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
2352
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
2353
+ "license": "MIT",
2354
+ "engines": {
2355
+ "node": ">=0.10.0"
2356
+ }
2357
+ },
2358
+ "node_modules/picocolors": {
2359
+ "version": "1.1.1",
2360
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
2361
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
2362
+ "dev": true,
2363
+ "license": "ISC"
2364
+ },
2365
+ "node_modules/picomatch": {
2366
+ "version": "4.0.4",
2367
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
2368
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
2369
+ "dev": true,
2370
+ "license": "MIT",
2371
+ "engines": {
2372
+ "node": ">=12"
2373
+ },
2374
+ "funding": {
2375
+ "url": "https://github.com/sponsors/jonschlinkert"
2376
+ }
2377
+ },
2378
+ "node_modules/postcss": {
2379
+ "version": "8.5.15",
2380
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
2381
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
2382
+ "dev": true,
2383
+ "funding": [
2384
+ {
2385
+ "type": "opencollective",
2386
+ "url": "https://opencollective.com/postcss/"
2387
+ },
2388
+ {
2389
+ "type": "tidelift",
2390
+ "url": "https://tidelift.com/funding/github/npm/postcss"
2391
+ },
2392
+ {
2393
+ "type": "github",
2394
+ "url": "https://github.com/sponsors/ai"
2395
+ }
2396
+ ],
2397
+ "license": "MIT",
2398
+ "dependencies": {
2399
+ "nanoid": "^3.3.12",
2400
+ "picocolors": "^1.1.1",
2401
+ "source-map-js": "^1.2.1"
2402
+ },
2403
+ "engines": {
2404
+ "node": "^10 || ^12 || >=14"
2405
+ }
2406
+ },
2407
+ "node_modules/prop-types": {
2408
+ "version": "15.8.1",
2409
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
2410
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
2411
+ "license": "MIT",
2412
+ "dependencies": {
2413
+ "loose-envify": "^1.4.0",
2414
+ "object-assign": "^4.1.1",
2415
+ "react-is": "^16.13.1"
2416
+ }
2417
+ },
2418
+ "node_modules/prop-types/node_modules/react-is": {
2419
+ "version": "16.13.1",
2420
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
2421
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
2422
+ "license": "MIT"
2423
+ },
2424
+ "node_modules/react": {
2425
+ "version": "19.2.7",
2426
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
2427
+ "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
2428
+ "license": "MIT",
2429
+ "engines": {
2430
+ "node": ">=0.10.0"
2431
+ }
2432
+ },
2433
+ "node_modules/react-dom": {
2434
+ "version": "19.2.7",
2435
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
2436
+ "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
2437
+ "license": "MIT",
2438
+ "dependencies": {
2439
+ "scheduler": "^0.27.0"
2440
+ },
2441
+ "peerDependencies": {
2442
+ "react": "^19.2.7"
2443
+ }
2444
+ },
2445
+ "node_modules/react-is": {
2446
+ "version": "18.3.1",
2447
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
2448
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
2449
+ "license": "MIT"
2450
+ },
2451
+ "node_modules/react-refresh": {
2452
+ "version": "0.17.0",
2453
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
2454
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
2455
+ "dev": true,
2456
+ "license": "MIT",
2457
+ "engines": {
2458
+ "node": ">=0.10.0"
2459
+ }
2460
+ },
2461
+ "node_modules/react-smooth": {
2462
+ "version": "4.0.4",
2463
+ "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
2464
+ "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
2465
+ "license": "MIT",
2466
+ "dependencies": {
2467
+ "fast-equals": "^5.0.1",
2468
+ "prop-types": "^15.8.1",
2469
+ "react-transition-group": "^4.4.5"
2470
+ },
2471
+ "peerDependencies": {
2472
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
2473
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2474
+ }
2475
+ },
2476
+ "node_modules/react-transition-group": {
2477
+ "version": "4.4.5",
2478
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
2479
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
2480
+ "license": "BSD-3-Clause",
2481
+ "dependencies": {
2482
+ "@babel/runtime": "^7.5.5",
2483
+ "dom-helpers": "^5.0.1",
2484
+ "loose-envify": "^1.4.0",
2485
+ "prop-types": "^15.6.2"
2486
+ },
2487
+ "peerDependencies": {
2488
+ "react": ">=16.6.0",
2489
+ "react-dom": ">=16.6.0"
2490
+ }
2491
+ },
2492
+ "node_modules/recharts": {
2493
+ "version": "2.15.4",
2494
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
2495
+ "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==",
2496
+ "deprecated": "1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide",
2497
+ "license": "MIT",
2498
+ "dependencies": {
2499
+ "clsx": "^2.0.0",
2500
+ "eventemitter3": "^4.0.1",
2501
+ "lodash": "^4.17.21",
2502
+ "react-is": "^18.3.1",
2503
+ "react-smooth": "^4.0.4",
2504
+ "recharts-scale": "^0.4.4",
2505
+ "tiny-invariant": "^1.3.1",
2506
+ "victory-vendor": "^36.6.8"
2507
+ },
2508
+ "engines": {
2509
+ "node": ">=14"
2510
+ },
2511
+ "peerDependencies": {
2512
+ "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
2513
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2514
+ }
2515
+ },
2516
+ "node_modules/recharts-scale": {
2517
+ "version": "0.4.5",
2518
+ "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
2519
+ "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
2520
+ "license": "MIT",
2521
+ "dependencies": {
2522
+ "decimal.js-light": "^2.4.1"
2523
+ }
2524
+ },
2525
+ "node_modules/rollup": {
2526
+ "version": "4.62.0",
2527
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz",
2528
+ "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==",
2529
+ "dev": true,
2530
+ "license": "MIT",
2531
+ "dependencies": {
2532
+ "@types/estree": "1.0.9"
2533
+ },
2534
+ "bin": {
2535
+ "rollup": "dist/bin/rollup"
2536
+ },
2537
+ "engines": {
2538
+ "node": ">=18.0.0",
2539
+ "npm": ">=8.0.0"
2540
+ },
2541
+ "optionalDependencies": {
2542
+ "@rollup/rollup-android-arm-eabi": "4.62.0",
2543
+ "@rollup/rollup-android-arm64": "4.62.0",
2544
+ "@rollup/rollup-darwin-arm64": "4.62.0",
2545
+ "@rollup/rollup-darwin-x64": "4.62.0",
2546
+ "@rollup/rollup-freebsd-arm64": "4.62.0",
2547
+ "@rollup/rollup-freebsd-x64": "4.62.0",
2548
+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.0",
2549
+ "@rollup/rollup-linux-arm-musleabihf": "4.62.0",
2550
+ "@rollup/rollup-linux-arm64-gnu": "4.62.0",
2551
+ "@rollup/rollup-linux-arm64-musl": "4.62.0",
2552
+ "@rollup/rollup-linux-loong64-gnu": "4.62.0",
2553
+ "@rollup/rollup-linux-loong64-musl": "4.62.0",
2554
+ "@rollup/rollup-linux-ppc64-gnu": "4.62.0",
2555
+ "@rollup/rollup-linux-ppc64-musl": "4.62.0",
2556
+ "@rollup/rollup-linux-riscv64-gnu": "4.62.0",
2557
+ "@rollup/rollup-linux-riscv64-musl": "4.62.0",
2558
+ "@rollup/rollup-linux-s390x-gnu": "4.62.0",
2559
+ "@rollup/rollup-linux-x64-gnu": "4.62.0",
2560
+ "@rollup/rollup-linux-x64-musl": "4.62.0",
2561
+ "@rollup/rollup-openbsd-x64": "4.62.0",
2562
+ "@rollup/rollup-openharmony-arm64": "4.62.0",
2563
+ "@rollup/rollup-win32-arm64-msvc": "4.62.0",
2564
+ "@rollup/rollup-win32-ia32-msvc": "4.62.0",
2565
+ "@rollup/rollup-win32-x64-gnu": "4.62.0",
2566
+ "@rollup/rollup-win32-x64-msvc": "4.62.0",
2567
+ "fsevents": "~2.3.2"
2568
+ }
2569
+ },
2570
+ "node_modules/scheduler": {
2571
+ "version": "0.27.0",
2572
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
2573
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
2574
+ "license": "MIT"
2575
+ },
2576
+ "node_modules/semver": {
2577
+ "version": "6.3.1",
2578
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
2579
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
2580
+ "dev": true,
2581
+ "license": "ISC",
2582
+ "bin": {
2583
+ "semver": "bin/semver.js"
2584
+ }
2585
+ },
2586
+ "node_modules/source-map-js": {
2587
+ "version": "1.2.1",
2588
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
2589
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
2590
+ "dev": true,
2591
+ "license": "BSD-3-Clause",
2592
+ "engines": {
2593
+ "node": ">=0.10.0"
2594
+ }
2595
+ },
2596
+ "node_modules/tailwindcss": {
2597
+ "version": "4.3.1",
2598
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz",
2599
+ "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==",
2600
+ "dev": true,
2601
+ "license": "MIT"
2602
+ },
2603
+ "node_modules/tapable": {
2604
+ "version": "2.3.3",
2605
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
2606
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
2607
+ "dev": true,
2608
+ "license": "MIT",
2609
+ "engines": {
2610
+ "node": ">=6"
2611
+ },
2612
+ "funding": {
2613
+ "type": "opencollective",
2614
+ "url": "https://opencollective.com/webpack"
2615
+ }
2616
+ },
2617
+ "node_modules/tiny-invariant": {
2618
+ "version": "1.3.3",
2619
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
2620
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
2621
+ "license": "MIT"
2622
+ },
2623
+ "node_modules/tinyglobby": {
2624
+ "version": "0.2.17",
2625
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
2626
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
2627
+ "dev": true,
2628
+ "license": "MIT",
2629
+ "dependencies": {
2630
+ "fdir": "^6.5.0",
2631
+ "picomatch": "^4.0.4"
2632
+ },
2633
+ "engines": {
2634
+ "node": ">=12.0.0"
2635
+ },
2636
+ "funding": {
2637
+ "url": "https://github.com/sponsors/SuperchupuDev"
2638
+ }
2639
+ },
2640
+ "node_modules/update-browserslist-db": {
2641
+ "version": "1.2.3",
2642
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
2643
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
2644
+ "dev": true,
2645
+ "funding": [
2646
+ {
2647
+ "type": "opencollective",
2648
+ "url": "https://opencollective.com/browserslist"
2649
+ },
2650
+ {
2651
+ "type": "tidelift",
2652
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
2653
+ },
2654
+ {
2655
+ "type": "github",
2656
+ "url": "https://github.com/sponsors/ai"
2657
+ }
2658
+ ],
2659
+ "license": "MIT",
2660
+ "dependencies": {
2661
+ "escalade": "^3.2.0",
2662
+ "picocolors": "^1.1.1"
2663
+ },
2664
+ "bin": {
2665
+ "update-browserslist-db": "cli.js"
2666
+ },
2667
+ "peerDependencies": {
2668
+ "browserslist": ">= 4.21.0"
2669
+ }
2670
+ },
2671
+ "node_modules/victory-vendor": {
2672
+ "version": "36.9.2",
2673
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
2674
+ "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
2675
+ "license": "MIT AND ISC",
2676
+ "dependencies": {
2677
+ "@types/d3-array": "^3.0.3",
2678
+ "@types/d3-ease": "^3.0.0",
2679
+ "@types/d3-interpolate": "^3.0.1",
2680
+ "@types/d3-scale": "^4.0.2",
2681
+ "@types/d3-shape": "^3.1.0",
2682
+ "@types/d3-time": "^3.0.0",
2683
+ "@types/d3-timer": "^3.0.0",
2684
+ "d3-array": "^3.1.6",
2685
+ "d3-ease": "^3.0.1",
2686
+ "d3-interpolate": "^3.0.1",
2687
+ "d3-scale": "^4.0.2",
2688
+ "d3-shape": "^3.1.0",
2689
+ "d3-time": "^3.0.0",
2690
+ "d3-timer": "^3.0.1"
2691
+ }
2692
+ },
2693
+ "node_modules/vite": {
2694
+ "version": "6.4.3",
2695
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
2696
+ "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
2697
+ "dev": true,
2698
+ "license": "MIT",
2699
+ "dependencies": {
2700
+ "esbuild": "^0.25.0",
2701
+ "fdir": "^6.4.4",
2702
+ "picomatch": "^4.0.2",
2703
+ "postcss": "^8.5.3",
2704
+ "rollup": "^4.34.9",
2705
+ "tinyglobby": "^0.2.13"
2706
+ },
2707
+ "bin": {
2708
+ "vite": "bin/vite.js"
2709
+ },
2710
+ "engines": {
2711
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
2712
+ },
2713
+ "funding": {
2714
+ "url": "https://github.com/vitejs/vite?sponsor=1"
2715
+ },
2716
+ "optionalDependencies": {
2717
+ "fsevents": "~2.3.3"
2718
+ },
2719
+ "peerDependencies": {
2720
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
2721
+ "jiti": ">=1.21.0",
2722
+ "less": "*",
2723
+ "lightningcss": "^1.21.0",
2724
+ "sass": "*",
2725
+ "sass-embedded": "*",
2726
+ "stylus": "*",
2727
+ "sugarss": "*",
2728
+ "terser": "^5.16.0",
2729
+ "tsx": "^4.8.1",
2730
+ "yaml": "^2.4.2"
2731
+ },
2732
+ "peerDependenciesMeta": {
2733
+ "@types/node": {
2734
+ "optional": true
2735
+ },
2736
+ "jiti": {
2737
+ "optional": true
2738
+ },
2739
+ "less": {
2740
+ "optional": true
2741
+ },
2742
+ "lightningcss": {
2743
+ "optional": true
2744
+ },
2745
+ "sass": {
2746
+ "optional": true
2747
+ },
2748
+ "sass-embedded": {
2749
+ "optional": true
2750
+ },
2751
+ "stylus": {
2752
+ "optional": true
2753
+ },
2754
+ "sugarss": {
2755
+ "optional": true
2756
+ },
2757
+ "terser": {
2758
+ "optional": true
2759
+ },
2760
+ "tsx": {
2761
+ "optional": true
2762
+ },
2763
+ "yaml": {
2764
+ "optional": true
2765
+ }
2766
+ }
2767
+ },
2768
+ "node_modules/yallist": {
2769
+ "version": "3.1.1",
2770
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
2771
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
2772
+ "dev": true,
2773
+ "license": "ISC"
2774
+ }
2775
+ }
2776
+ }
eval-dashboard/package.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "prism-eval-dashboard",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "dependencies": {
11
+ "react": "^19.0.0",
12
+ "react-dom": "^19.0.0",
13
+ "recharts": "^2.12.0"
14
+ },
15
+ "devDependencies": {
16
+ "@tailwindcss/vite": "^4.0.0",
17
+ "@vitejs/plugin-react": "^4.3.0",
18
+ "tailwindcss": "^4.0.0",
19
+ "vite": "^6.0.0"
20
+ }
21
+ }
eval-dashboard/public/data/index.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "version": "v2.0",
4
+ "tag": "baseline — mandatory web",
5
+ "date": "2026-06-17",
6
+ "file": "v2.0_20260617.json"
7
+ }
8
+ ]
eval-dashboard/public/data/runs/v2.0_20260617.json ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "v2.0",
3
+ "tag": "baseline - mandatory\n web",
4
+ "computed_at": "2026-06-17T21:18:00",
5
+ "sample_count": 20,
6
+ "config": {
7
+ "hyde_enabled": false,
8
+ "retrieve_k": 10,
9
+ "rerank_k": 5,
10
+ "llm": "llama-3.3-70b-versatile",
11
+ "judge_model": "llama-3.1-8b-instant",
12
+ "workspace": "default"
13
+ },
14
+ "metrics": {
15
+ "answer_correctness": 0.75,
16
+ "answer_relevancy": 0.8407,
17
+ "context_recall": 0.7013,
18
+ "precision_at_5": 0.89,
19
+ "latency_p50_ms": 791,
20
+ "latency_p95_ms": 1230,
21
+ "latency_p99_ms": 2578
22
+ },
23
+ "per_query": [
24
+ {
25
+ "query": "What was the total UPI transaction volume in FY2024?",
26
+ "answer": "131 billion transactions. \n[Doc 1] supports this answer.",
27
+ "ground_truth": "Total UPI transaction volume crossed 131 billion transactions in FY2024. Monthly volume reached 14.04 billion transactions by March 2024, growing 56% year-on-year in volume and 43% in value compared to FY2023. Total annual value was approximately Rs 200 lakh crore.",
28
+ "answer_correctness": 0.75,
29
+ "correctness_reason": "missing key details such as monthly volume, year-on-year growth, and total annual value",
30
+ "answer_relevancy": 0.3767,
31
+ "context_recall": 0.75,
32
+ "precision_at_5": 1.0,
33
+ "latency_ms": 2916,
34
+ "retrieved_sources": [
35
+ "npci_upi_report_2024.txt",
36
+ "npci_upi_report_2024.txt",
37
+ "rbi_annual_report_2024.txt",
38
+ "npci_upi_report_2024.txt",
39
+ "npci_upi_report_2024.txt"
40
+ ]
41
+ },
42
+ {
43
+ "query": "What was India's GDP growth rate in FY2024?",
44
+ "answer": "India's real GDP growth for FY2024 is estimated at 7.6 per cent. \n[Doc 1]",
45
+ "ground_truth": "India's real GDP grew at 7.6% in FY2024, driven by strong domestic demand and investment activity. This was above the global average and made India one of the fastest-growing major economies.",
46
+ "answer_correctness": 0.75,
47
+ "correctness_reason": "most key facts correct, minor omissions or imprecision",
48
+ "answer_relevancy": 0.8604,
49
+ "context_recall": 0.5,
50
+ "precision_at_5": 1.0,
51
+ "latency_ms": 1142,
52
+ "retrieved_sources": [
53
+ "rbi_annual_report_2024.txt",
54
+ "bajaj_finance_q3_2024_transcript.txt",
55
+ "bajaj_finance_q3_2024_transcript.txt",
56
+ "rbi_annual_report_2024.txt",
57
+ "rbi_annual_report_2024.txt"
58
+ ]
59
+ },
60
+ {
61
+ "query": "What were Bajaj Finance's Assets Under Management in Q3 FY2024?",
62
+ "answer": "Bajaj Finance's Assets Under Management (AUM) in Q3 FY2024 were Rs 3,10,672 crore. \n[Doc 3] supports this answer.",
63
+ "ground_truth": "Bajaj Finance's consolidated Assets Under Management (AUM) stood at Rs 3,10,672 crore as of Q3 FY2024, growing 35% year-on-year.",
64
+ "answer_correctness": 0.75,
65
+ "correctness_reason": "most key facts correct, minor omissions or imprecision (lack of year-on-year growth percentage in the generated answer)",
66
+ "answer_relevancy": 0.9651,
67
+ "context_recall": 1.0,
68
+ "precision_at_5": 0.8,
69
+ "latency_ms": 870,
70
+ "retrieved_sources": [
71
+ "bajaj_finance_q3_2024_transcript.txt",
72
+ "bajaj_finance_q3_2024_transcript.txt",
73
+ "bajaj_finance_q3_2024_transcript.txt",
74
+ "rbi_annual_report_2024.txt",
75
+ "bajaj_finance_q3_2024_transcript.txt"
76
+ ]
77
+ },
78
+ {
79
+ "query": "What is RBI's stance on digital lending regulations?",
80
+ "answer": "The RBI has enhanced guidelines on digital lending, requiring all digital loans to be disbursed and repaid through borrower bank accounts, and Lending Service Providers (LSPs) and Digital Lending Apps (DLAs) must comply with disclosure requirements. \n[Doc 3]",
81
+ "ground_truth": "RBI issued enhanced guidelines on digital lending requiring all digital loans to be disbursed and repaid through borrower bank accounts. Lending Service Providers (LSPs) and Digital Lending Apps (DLAs) must comply with disclosure requirements. The framework applies to NBFCs and banks offering digital lending products.",
82
+ "answer_correctness": 0.75,
83
+ "correctness_reason": "minor omission of 'framework applies to NBFCs and banks offering digital lending products'",
84
+ "answer_relevancy": 0.8134,
85
+ "context_recall": 1.0,
86
+ "precision_at_5": 0.8,
87
+ "latency_ms": 883,
88
+ "retrieved_sources": [
89
+ "bajaj_finance_q3_2024_transcript.txt",
90
+ "rbi_annual_report_2024.txt",
91
+ "rbi_annual_report_2024.txt",
92
+ "rbi_annual_report_2024.txt",
93
+ "rbi_annual_report_2024.txt"
94
+ ]
95
+ },
96
+ {
97
+ "query": "How many banks were live on UPI by March 2024?",
98
+ "answer": "580 banks were live on UPI by March 2024. \n[Doc 1] supports this answer.",
99
+ "ground_truth": "580 banks were live on UPI as of March 2024, up from 437 in March 2023. PSU banks accounted for 35% of UPI originating transactions, private banks 42%, and Small Finance Banks and Payment Banks 23%.",
100
+ "answer_correctness": 0.75,
101
+ "correctness_reason": "most key facts correct, minor omissions or imprecision",
102
+ "answer_relevancy": 1.0,
103
+ "context_recall": 0.2857,
104
+ "precision_at_5": 1.0,
105
+ "latency_ms": 774,
106
+ "retrieved_sources": [
107
+ "npci_upi_report_2024.txt",
108
+ "npci_upi_report_2024.txt",
109
+ "npci_upi_report_2024.txt",
110
+ "npci_upi_report_2024.txt",
111
+ "npci_upi_report_2024.txt"
112
+ ]
113
+ },
114
+ {
115
+ "query": "What was the UPI fraud rate in FY2024?",
116
+ "answer": "The UPI fraud rate remained low at 0.0006% of total transactions by volume in FY2024. \n[Doc 1]",
117
+ "ground_truth": "The UPI fraud rate was 0.0006% of total transactions in FY2024. There were 3.07 lakh fraud cases reported, representing a very small fraction given the 131 billion total transactions processed during the year.",
118
+ "answer_correctness": 0.75,
119
+ "correctness_reason": "minor omission of specific numbers and reference",
120
+ "answer_relevancy": 1.0,
121
+ "context_recall": 1.0,
122
+ "precision_at_5": 0.8,
123
+ "latency_ms": 727,
124
+ "retrieved_sources": [
125
+ "npci_upi_report_2024.txt",
126
+ "npci_upi_report_2024.txt",
127
+ "rbi_annual_report_2024.txt",
128
+ "npci_upi_report_2024.txt",
129
+ "npci_upi_report_2024.txt"
130
+ ]
131
+ },
132
+ {
133
+ "query": "What was the RBI repo rate during FY2024?",
134
+ "answer": "The RBI policy repo rate was 6.50 per cent during FY2024. \n[Doc 5]",
135
+ "ground_truth": "The RBI kept the repo rate unchanged at 6.50% throughout FY2024. The Monetary Policy Committee (MPC) maintained this rate to balance inflation control with growth support, keeping the policy stance as withdrawal of accommodation.",
136
+ "answer_correctness": 0.75,
137
+ "correctness_reason": "most key facts correct, minor omissions or imprecision",
138
+ "answer_relevancy": 0.9656,
139
+ "context_recall": null,
140
+ "precision_at_5": 1.0,
141
+ "latency_ms": 631,
142
+ "retrieved_sources": [
143
+ "rbi_annual_report_2024.txt",
144
+ "rbi_annual_report_2024.txt",
145
+ "rbi_annual_report_2024.txt",
146
+ "rbi_annual_report_2024.txt",
147
+ "rbi_annual_report_2024.txt"
148
+ ]
149
+ },
150
+ {
151
+ "query": "What was Bajaj Finance's profit after tax in Q3 FY2024?",
152
+ "answer": "Bajaj Finance's profit after tax in Q3 FY2024 was Rs 3,639 crore. \n[Doc 3] supports this answer.",
153
+ "ground_truth": "Bajaj Finance's profit after tax (PAT) in Q3 FY2024 was Rs 3,639 crore, growing 22% year-on-year. Earnings per share increased correspondingly.",
154
+ "answer_correctness": 0.75,
155
+ "correctness_reason": "omitted year-on-year growth and earnings per share information",
156
+ "answer_relevancy": 1.0,
157
+ "context_recall": 1.0,
158
+ "precision_at_5": 1.0,
159
+ "latency_ms": 684,
160
+ "retrieved_sources": [
161
+ "bajaj_finance_q3_2024_transcript.txt",
162
+ "bajaj_finance_q3_2024_transcript.txt",
163
+ "bajaj_finance_q3_2024_transcript.txt",
164
+ "bajaj_finance_q3_2024_transcript.txt",
165
+ "bajaj_finance_q3_2024_transcript.txt"
166
+ ]
167
+ },
168
+ {
169
+ "query": "What is UPI Lite and how many users does it have?",
170
+ "answer": "UPI Lite is a product innovation that enables small-value transactions (up to Rs 500) with near-zero decline rates. As of March 2024, UPI Lite had 5.2 crore enabled users. \n[Doc 1]",
171
+ "ground_truth": "UPI Lite is a feature enabling small-value transactions up to Rs 500 with near-zero decline rates by using an on-device wallet. As of March 2024, UPI Lite had 5.2 crore enabled users processing 18 crore transactions monthly. UPI Lite X, the offline variant, was piloted in 12 cities.",
172
+ "answer_correctness": 0.75,
173
+ "correctness_reason": "omitted information about transactions and UPI Lite X",
174
+ "answer_relevancy": 0.7858,
175
+ "context_recall": 1.0,
176
+ "precision_at_5": 0.8,
177
+ "latency_ms": 787,
178
+ "retrieved_sources": [
179
+ "npci_upi_report_2024.txt",
180
+ "npci_upi_report_2024.txt",
181
+ "rbi_annual_report_2024.txt",
182
+ "npci_upi_report_2024.txt",
183
+ "rbi_annual_report_2024.txt"
184
+ ]
185
+ },
186
+ {
187
+ "query": "What was India's CPI inflation rate in FY2024?",
188
+ "answer": "India's CPI inflation rate averaged 5.4 per cent in FY2024. \n[Doc 1]",
189
+ "ground_truth": "India's CPI inflation averaged 5.4% in FY2024, declining from 6.7% in FY2023. Core inflation (excluding food and fuel) moderated to 4.2%. Food inflation remained elevated, while fuel prices provided some relief.",
190
+ "answer_correctness": 0.75,
191
+ "correctness_reason": "most key facts correct, minor omissions or imprecision",
192
+ "answer_relevancy": 0.9321,
193
+ "context_recall": 0.6667,
194
+ "precision_at_5": 0.8,
195
+ "latency_ms": 743,
196
+ "retrieved_sources": [
197
+ "rbi_annual_report_2024.txt",
198
+ "bajaj_finance_q3_2024_transcript.txt",
199
+ "rbi_annual_report_2024.txt",
200
+ "rbi_annual_report_2024.txt",
201
+ "rbi_annual_report_2024.txt"
202
+ ]
203
+ },
204
+ {
205
+ "query": "What is Bajaj Finance's capital adequacy ratio?",
206
+ "answer": "Bajaj Finance's capital adequacy ratio (CRAR) stood at 23.8%. \n[Doc 1] supports this answer.",
207
+ "ground_truth": "Bajaj Finance's Capital to Risk-weighted Assets Ratio (CRAR) stood at 23.8% as of Q3 FY2024, with Tier-I capital ratio at 22.1%. Both are well above the regulatory minimum, reflecting a strong capital position.",
208
+ "answer_correctness": 0.75,
209
+ "correctness_reason": "missing key fact (Tier-I capital ratio) and reference to supporting document",
210
+ "answer_relevancy": 0.9008,
211
+ "context_recall": null,
212
+ "precision_at_5": 1.0,
213
+ "latency_ms": 842,
214
+ "retrieved_sources": [
215
+ "bajaj_finance_q3_2024_transcript.txt",
216
+ "bajaj_finance_q3_2024_transcript.txt",
217
+ "bajaj_finance_q3_2024_transcript.txt",
218
+ "bajaj_finance_q3_2024_transcript.txt",
219
+ "rbi_annual_report_2024.txt"
220
+ ]
221
+ },
222
+ {
223
+ "query": "Which countries accept UPI payments internationally?",
224
+ "answer": "According to Doc 1, UPI acceptance was enabled in 7 countries: Singapore, UAE, France, Sri Lanka, Mauritius, Nepal, and Bhutan.",
225
+ "ground_truth": "UPI is accepted internationally in Singapore, UAE, France, Sri Lanka, and several other countries for cross-border payments. NPCI International is driving further expansion to reach 20 countries by 2028.",
226
+ "answer_correctness": 0.75,
227
+ "correctness_reason": "most key facts correct, minor omissions or imprecision",
228
+ "answer_relevancy": 0.7839,
229
+ "context_recall": 0.6,
230
+ "precision_at_5": 0.6,
231
+ "latency_ms": 796,
232
+ "retrieved_sources": [
233
+ "npci_upi_report_2024.txt",
234
+ "npci_upi_report_2024.txt",
235
+ "npci_upi_report_2024.txt",
236
+ "rbi_annual_report_2024.txt",
237
+ "rbi_annual_report_2024.txt"
238
+ ]
239
+ },
240
+ {
241
+ "query": "What was India's foreign exchange reserves in March 2024?",
242
+ "answer": "India's foreign exchange reserves stood at USD 645.6 billion as of March 2024. \n[Doc 1]",
243
+ "ground_truth": "India's foreign exchange reserves stood at USD 645.6 billion as of March 2024, providing approximately 11 months of import cover and reflecting strong external sector resilience.",
244
+ "answer_correctness": 0.75,
245
+ "correctness_reason": "missing import cover and external sector resilience details",
246
+ "answer_relevancy": 0.9549,
247
+ "context_recall": 1.0,
248
+ "precision_at_5": 1.0,
249
+ "latency_ms": 750,
250
+ "retrieved_sources": [
251
+ "rbi_annual_report_2024.txt",
252
+ "rbi_annual_report_2024.txt",
253
+ "npci_upi_report_2024.txt",
254
+ "rbi_annual_report_2024.txt",
255
+ "rbi_annual_report_2024.txt"
256
+ ]
257
+ },
258
+ {
259
+ "query": "What is Bajaj Finance's Gross NPA ratio?",
260
+ "answer": "The Gross NPA ratio for Bajaj Finance is not explicitly stated in the provided documents as a single value for the company. However, Doc 1 mentions the banking sector's GNPA ratio declined to 3.2 per cent, and Doc 3 states Gross NPA stood at 0.95% as of December 2023. Additionally, Doc 4 provides guidance that GNPA is expected to be below 1.1% for FY2024. \n\nDocuments: Doc 1, Doc 3, Doc 4.",
261
+ "ground_truth": "Bajaj Finance's Gross NPA (GNPA) ratio was 0.95% and Net NPA ratio was 0.36% as of Q3 FY2024. Provision coverage remained strong, reflecting disciplined underwriting and collections.",
262
+ "answer_correctness": 0.75,
263
+ "correctness_reason": "Most key facts correct, minor omissions or imprecision",
264
+ "answer_relevancy": 0.0,
265
+ "context_recall": null,
266
+ "precision_at_5": 1.0,
267
+ "latency_ms": 998,
268
+ "retrieved_sources": [
269
+ "rbi_annual_report_2024.txt",
270
+ "bajaj_finance_q3_2024_transcript.txt",
271
+ "bajaj_finance_q3_2024_transcript.txt",
272
+ "bajaj_finance_q3_2024_transcript.txt",
273
+ "bajaj_finance_q3_2024_transcript.txt"
274
+ ]
275
+ },
276
+ {
277
+ "query": "What is the market share of PhonePe on UPI?",
278
+ "answer": "The market share of PhonePe on UPI is 47%. \nThis information is supported by Doc 1.",
279
+ "ground_truth": "PhonePe held 47% market share on UPI by transaction volume, followed by Google Pay at 34%. The top two players together control over 80% of UPI transactions, though NPCI's market cap guidelines aim to limit concentration.",
280
+ "answer_correctness": 0.75,
281
+ "correctness_reason": "missing market share of Google Pay and concentration of top two players",
282
+ "answer_relevancy": 1.0,
283
+ "context_recall": 0.2857,
284
+ "precision_at_5": 1.0,
285
+ "latency_ms": 1003,
286
+ "retrieved_sources": [
287
+ "npci_upi_report_2024.txt",
288
+ "npci_upi_report_2024.txt",
289
+ "npci_upi_report_2024.txt",
290
+ "npci_upi_report_2024.txt",
291
+ "npci_upi_report_2024.txt"
292
+ ]
293
+ },
294
+ {
295
+ "query": "What is the banking sector's GNPA ratio as reported by RBI?",
296
+ "answer": "The banking sector's GNPA ratio is 3.2 per cent. \n[Doc 1] supports this answer.",
297
+ "ground_truth": "The banking sector's Gross NPA (GNPA) ratio declined to 3.2% as of March 2024, the lowest in over a decade. Net NPA ratio stood at 0.8%. The improvement reflects better asset quality, recoveries, and write-offs.",
298
+ "answer_correctness": 0.75,
299
+ "correctness_reason": "most key facts correct, minor omissions or imprecision",
300
+ "answer_relevancy": 0.8867,
301
+ "context_recall": 0.6667,
302
+ "precision_at_5": 0.8,
303
+ "latency_ms": 786,
304
+ "retrieved_sources": [
305
+ "rbi_annual_report_2024.txt",
306
+ "rbi_annual_report_2024.txt",
307
+ "rbi_annual_report_2024.txt",
308
+ "bajaj_finance_q3_2024_transcript.txt",
309
+ "bajaj_finance_q3_2024_transcript.txt"
310
+ ]
311
+ },
312
+ {
313
+ "query": "How much did Bajaj Finance spend on technology?",
314
+ "answer": "Bajaj Finance spent Rs 1,180 crore on technology for 9M FY2024. [Doc 4]",
315
+ "ground_truth": "Bajaj Finance spent Rs 1,180 crore on technology in Q3 FY2024, representing approximately 12% of operating expenses. Technology investments focus on digital origination, customer experience, and risk management platforms.",
316
+ "answer_correctness": 0.75,
317
+ "correctness_reason": "most key facts correct, minor omissions or imprecision",
318
+ "answer_relevancy": 0.9343,
319
+ "context_recall": 0.5,
320
+ "precision_at_5": 1.0,
321
+ "latency_ms": 834,
322
+ "retrieved_sources": [
323
+ "bajaj_finance_q3_2024_transcript.txt",
324
+ "bajaj_finance_q3_2024_transcript.txt",
325
+ "bajaj_finance_q3_2024_transcript.txt",
326
+ "bajaj_finance_q3_2024_transcript.txt",
327
+ "bajaj_finance_q3_2024_transcript.txt"
328
+ ]
329
+ },
330
+ {
331
+ "query": "What are the BBPS transaction statistics for FY2024?",
332
+ "answer": "BBPS processed 1,243 crore transactions valued at Rs 15.4 lakh crore during FY2024, with a 42% growth over FY2023. \n[Doc 1]",
333
+ "ground_truth": "Bharat Bill Payment System (BBPS) processed 1,243 crore transactions worth Rs 15.4 lakh crore in FY2024, growing 42% year-on-year. BBPS handles utility bill payments, insurance premiums, EMI collections, and other recurring payments.",
334
+ "answer_correctness": 0.75,
335
+ "correctness_reason": "minor inaccuracy in year-on-year comparison",
336
+ "answer_relevancy": 0.8679,
337
+ "context_recall": 0.6667,
338
+ "precision_at_5": 0.8,
339
+ "latency_ms": 688,
340
+ "retrieved_sources": [
341
+ "npci_upi_report_2024.txt",
342
+ "npci_upi_report_2024.txt",
343
+ "npci_upi_report_2024.txt",
344
+ "rbi_annual_report_2024.txt",
345
+ "npci_upi_report_2024.txt"
346
+ ]
347
+ },
348
+ {
349
+ "query": "What is the Digital Rupee pilot status?",
350
+ "answer": "The Digital Rupee (e-Rupee) pilot programmes continued with both retail (e\u20b9-R) and wholesale (e\u20b9-W) variants, and the retail CBDC pilot was expanded to cover 50 cities with participation from 13 banks, having approximately 10 lakh retail e\u20b9 wallets active as of March 2024. \n[Doc 1]",
351
+ "ground_truth": "RBI's Digital Rupee (e-Rupee / CBDC) retail pilot was expanded to 50 cities with over 10 lakh users as of FY2024. The pilot covers retail use cases for person-to-person and person-to-merchant transactions using a digital wallet issued by participating banks.",
352
+ "answer_correctness": 0.75,
353
+ "correctness_reason": "minor difference in wording and slight discrepancy in the number of banks",
354
+ "answer_relevancy": 0.9069,
355
+ "context_recall": 0.3333,
356
+ "precision_at_5": 0.6,
357
+ "latency_ms": 861,
358
+ "retrieved_sources": [
359
+ "rbi_annual_report_2024.txt",
360
+ "rbi_annual_report_2024.txt",
361
+ "rbi_annual_report_2024.txt",
362
+ "npci_upi_report_2024.txt",
363
+ "npci_upi_report_2024.txt"
364
+ ]
365
+ },
366
+ {
367
+ "query": "What is Bajaj Housing Finance's AUM and IPO plans?",
368
+ "answer": "Bajaj Housing Finance's AUM is Rs 72,400 crore, and its IPO is targeted for H1 FY2025. \n[Doc 1, Doc 2]",
369
+ "ground_truth": "Bajaj Housing Finance's AUM stood at Rs 72,400 crore as of Q3 FY2024. The company filed its Draft Red Herring Prospectus (DRHP) for an IPO and listing on stock exchanges, subject to regulatory approvals.",
370
+ "answer_correctness": 0.75,
371
+ "correctness_reason": "most key facts correct, minor omissions or imprecision",
372
+ "answer_relevancy": 0.8804,
373
+ "context_recall": 0.6667,
374
+ "precision_at_5": 1.0,
375
+ "latency_ms": 779,
376
+ "retrieved_sources": [
377
+ "bajaj_finance_q3_2024_transcript.txt",
378
+ "bajaj_finance_q3_2024_transcript.txt",
379
+ "bajaj_finance_q3_2024_transcript.txt",
380
+ "bajaj_finance_q3_2024_transcript.txt",
381
+ "bajaj_finance_q3_2024_transcript.txt"
382
+ ]
383
+ }
384
+ ]
385
+ }
eval-dashboard/src/App.jsx ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react'
2
+ import MetricCard from './components/MetricCard'
3
+ import EvolutionChart from './components/EvolutionChart'
4
+ import RunTable from './components/RunTable'
5
+ import LatencyStats from './components/LatencyStats'
6
+
7
+ const METRIC_DEFS = [
8
+ {
9
+ key: 'answer_correctness',
10
+ label: 'Answer Correctness',
11
+ description: 'LLM judge (8B) comparing generated answer to ground truth. 0–1 normalized.',
12
+ },
13
+ {
14
+ key: 'answer_relevancy',
15
+ label: 'Answer Relevancy',
16
+ description: 'RAGAS: does the answer address the question? Penalizes off-topic responses.',
17
+ },
18
+ {
19
+ key: 'context_recall',
20
+ label: 'Context Recall',
21
+ description: 'RAGAS: did retrieval surface all chunks needed to answer? Higher = less missing context.',
22
+ },
23
+ {
24
+ key: 'precision_at_5',
25
+ label: 'Precision@5',
26
+ description: 'Retrieval: what fraction of top-5 chunks are relevant? Based on source + keyword match.',
27
+ },
28
+ ]
29
+
30
+ function EmptyState() {
31
+ return (
32
+ <div className="flex flex-col items-center justify-center py-24 text-center">
33
+ <div className="w-16 h-16 rounded-2xl bg-indigo-50 flex items-center justify-center mb-4">
34
+ <svg className="w-8 h-8 text-indigo-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
35
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5"
36
+ d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
37
+ </svg>
38
+ </div>
39
+ <h2 className="text-lg font-semibold text-gray-700 mb-1">No eval runs yet</h2>
40
+ <p className="text-sm text-gray-400 max-w-sm">
41
+ Run the eval script to generate the first benchmark:
42
+ </p>
43
+ <pre className="mt-3 text-xs bg-gray-100 text-gray-600 px-4 py-3 rounded-xl font-mono">
44
+ python scripts/run_eval_versioned.py --version v2.0 --tag "baseline" --n 50
45
+ </pre>
46
+ </div>
47
+ )
48
+ }
49
+
50
+ export default function App() {
51
+ const [indexData, setIndexData] = useState([])
52
+ const [runs, setRuns] = useState([])
53
+ const [selectedVersion, setSelectedVersion] = useState(null)
54
+ const [loading, setLoading] = useState(true)
55
+ const [error, setError] = useState(null)
56
+
57
+ useEffect(() => {
58
+ fetch('/data/index.json')
59
+ .then(r => r.json())
60
+ .then(async (idx) => {
61
+ if (!idx.length) { setLoading(false); return }
62
+ const loaded = await Promise.all(
63
+ idx.map(entry => fetch(`/data/runs/${entry.file}`).then(r => r.json()))
64
+ )
65
+ setIndexData(idx)
66
+ setRuns(loaded)
67
+ setSelectedVersion(idx[idx.length - 1].version)
68
+ setLoading(false)
69
+ })
70
+ .catch(e => { setError(e.message); setLoading(false) })
71
+ }, [])
72
+
73
+ const currentIdx = indexData.findIndex(e => e.version === selectedVersion)
74
+ const currentRun = runs[currentIdx] ?? null
75
+ const prevRun = currentIdx > 0 ? runs[currentIdx - 1] : null
76
+
77
+ return (
78
+ <div className="min-h-screen bg-gray-50">
79
+ {/* Header */}
80
+ <header className="bg-white border-b border-gray-100 px-6 py-4">
81
+ <div className="max-w-6xl mx-auto flex items-center justify-between">
82
+ <div className="flex items-center gap-3">
83
+ <div className="w-8 h-8 rounded-lg bg-indigo-600 flex items-center justify-center">
84
+ <svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
85
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
86
+ d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
87
+ </svg>
88
+ </div>
89
+ <div>
90
+ <h1 className="text-base font-semibold text-gray-900">Prism Eval Dashboard</h1>
91
+ <p className="text-xs text-gray-400">Developer benchmark — not shown to users</p>
92
+ </div>
93
+ </div>
94
+
95
+ {indexData.length > 0 && (
96
+ <div className="flex items-center gap-3">
97
+ <span className="text-xs text-gray-400">{indexData.length} run{indexData.length !== 1 ? 's' : ''}</span>
98
+ <select
99
+ value={selectedVersion ?? ''}
100
+ onChange={e => setSelectedVersion(e.target.value)}
101
+ className="text-sm border border-gray-200 rounded-lg px-3 py-1.5 bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500"
102
+ >
103
+ {indexData.map(e => (
104
+ <option key={e.version} value={e.version}>
105
+ {e.version} — {e.tag}
106
+ </option>
107
+ ))}
108
+ </select>
109
+ </div>
110
+ )}
111
+ </div>
112
+ </header>
113
+
114
+ <main className="max-w-6xl mx-auto px-6 py-8 flex flex-col gap-6">
115
+ {loading && (
116
+ <div className="flex items-center justify-center py-24 text-gray-400 text-sm">Loading...</div>
117
+ )}
118
+ {error && (
119
+ <div className="bg-red-50 border border-red-200 text-red-700 text-sm px-4 py-3 rounded-xl">
120
+ Failed to load eval data: {error}
121
+ </div>
122
+ )}
123
+
124
+ {!loading && !error && !runs.length && <EmptyState />}
125
+
126
+ {currentRun && (
127
+ <>
128
+ {/* Run meta */}
129
+ <div className="flex items-center gap-4 text-xs text-gray-400">
130
+ <span className="bg-indigo-50 text-indigo-700 font-medium px-2.5 py-1 rounded-full">
131
+ {currentRun.version}
132
+ </span>
133
+ <span>{currentRun.tag}</span>
134
+ <span>·</span>
135
+ <span>{currentRun.sample_count} samples</span>
136
+ <span>·</span>
137
+ <span>{new Date(currentRun.computed_at).toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' })}</span>
138
+ {currentRun.config && (
139
+ <>
140
+ <span>·</span>
141
+ <span>HyDE: {currentRun.config.hyde_enabled ? 'on' : 'off'}</span>
142
+ <span>·</span>
143
+ <span>retrieve_k={currentRun.config.retrieve_k}</span>
144
+ </>
145
+ )}
146
+ </div>
147
+
148
+ {/* Metric cards */}
149
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
150
+ {METRIC_DEFS.map(m => (
151
+ <MetricCard
152
+ key={m.key}
153
+ label={m.label}
154
+ description={m.description}
155
+ value={currentRun.metrics?.[m.key]}
156
+ prevValue={prevRun?.metrics?.[m.key]}
157
+ />
158
+ ))}
159
+ </div>
160
+
161
+ {/* Evolution chart + latency */}
162
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
163
+ <div className="lg:col-span-2">
164
+ <EvolutionChart runs={runs} index={indexData} />
165
+ </div>
166
+ <LatencyStats metrics={currentRun.metrics} />
167
+ </div>
168
+
169
+ {/* Per-query table */}
170
+ <RunTable perQuery={currentRun.per_query} />
171
+ </>
172
+ )}
173
+ </main>
174
+ </div>
175
+ )
176
+ }
eval-dashboard/src/components/EvolutionChart.jsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer
3
+ } from 'recharts'
4
+
5
+ const METRICS = [
6
+ { key: 'answer_correctness', label: 'Answer Correctness', color: '#6366f1' },
7
+ { key: 'answer_relevancy', label: 'Answer Relevancy', color: '#10b981' },
8
+ { key: 'context_recall', label: 'Context Recall', color: '#f59e0b' },
9
+ { key: 'precision_at_5', label: 'Precision@5', color: '#ef4444' },
10
+ ]
11
+
12
+ export default function EvolutionChart({ runs, index }) {
13
+ if (!runs.length) return null
14
+
15
+ const data = runs.map((run, i) => ({
16
+ version: index[i]?.version ?? `run${i + 1}`,
17
+ ...Object.fromEntries(
18
+ METRICS.map(m => [m.key, run.metrics?.[m.key] != null
19
+ ? parseFloat((run.metrics[m.key] * 100).toFixed(1))
20
+ : null
21
+ ])
22
+ ),
23
+ }))
24
+
25
+ const fmt = (v) => `${v}%`
26
+
27
+ return (
28
+ <div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6">
29
+ <h2 className="text-sm font-semibold text-gray-700 mb-4">Metric Evolution Across Versions</h2>
30
+ <ResponsiveContainer width="100%" height={280}>
31
+ <LineChart data={data} margin={{ top: 4, right: 16, left: 0, bottom: 4 }}>
32
+ <CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" />
33
+ <XAxis dataKey="version" tick={{ fontSize: 12 }} />
34
+ <YAxis domain={[0, 100]} tickFormatter={fmt} tick={{ fontSize: 12 }} width={40} />
35
+ <Tooltip formatter={(v) => `${v}%`} />
36
+ <Legend wrapperStyle={{ fontSize: 12 }} />
37
+ {METRICS.map(m => (
38
+ <Line
39
+ key={m.key}
40
+ type="monotone"
41
+ dataKey={m.key}
42
+ name={m.label}
43
+ stroke={m.color}
44
+ strokeWidth={2}
45
+ dot={{ r: 4 }}
46
+ connectNulls
47
+ />
48
+ ))}
49
+ </LineChart>
50
+ </ResponsiveContainer>
51
+ </div>
52
+ )
53
+ }
eval-dashboard/src/components/LatencyStats.jsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function StatBar({ label, value, max }) {
2
+ const pct = max > 0 ? Math.min((value / max) * 100, 100) : 0
3
+ const color = value < 3000 ? 'bg-emerald-500' : value < 6000 ? 'bg-yellow-500' : 'bg-red-500'
4
+ return (
5
+ <div className="flex flex-col gap-1">
6
+ <div className="flex justify-between text-xs text-gray-500">
7
+ <span>{label}</span>
8
+ <span className="font-mono font-medium text-gray-700">{value ? `${value.toLocaleString()}ms` : '—'}</span>
9
+ </div>
10
+ <div className="h-2 bg-gray-100 rounded-full overflow-hidden">
11
+ <div className={`h-full rounded-full ${color}`} style={{ width: `${pct}%` }} />
12
+ </div>
13
+ </div>
14
+ )
15
+ }
16
+
17
+ export default function LatencyStats({ metrics }) {
18
+ const p50 = metrics?.latency_p50_ms
19
+ const p95 = metrics?.latency_p95_ms
20
+ const p99 = metrics?.latency_p99_ms
21
+ const max = Math.max(p99 ?? 0, p95 ?? 0, p50 ?? 0, 10000)
22
+
23
+ return (
24
+ <div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6">
25
+ <h2 className="text-sm font-semibold text-gray-700 mb-4">Response Latency</h2>
26
+ <div className="flex flex-col gap-4">
27
+ <StatBar label="p50 (median)" value={p50} max={max} />
28
+ <StatBar label="p95" value={p95} max={max} />
29
+ {p99 != null && <StatBar label="p99" value={p99} max={max} />}
30
+ </div>
31
+ <p className="text-xs text-gray-400 mt-3">End-to-end per-query latency including retrieval + LLM call. Does not include Tavily web search.</p>
32
+ </div>
33
+ )
34
+ }
eval-dashboard/src/components/MetricCard.jsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function MetricCard({ label, value, prevValue, description, unit = '%' }) {
2
+ const display = value != null ? (unit === '%' ? `${(value * 100).toFixed(1)}%` : `${value}`) : '—'
3
+
4
+ let delta = null
5
+ let deltaClass = 'text-gray-400'
6
+ let deltaSign = ''
7
+ if (value != null && prevValue != null) {
8
+ const diff = unit === '%' ? (value - prevValue) * 100 : value - prevValue
9
+ delta = diff.toFixed(unit === '%' ? 1 : 0)
10
+ if (diff > 0) { deltaClass = 'text-emerald-600'; deltaSign = '+' }
11
+ else if (diff < 0) { deltaClass = 'text-red-500'; deltaSign = '' }
12
+ else { deltaClass = 'text-gray-400' }
13
+ }
14
+
15
+ const scoreColor = value == null ? 'text-gray-400'
16
+ : value >= 0.8 ? 'text-emerald-600'
17
+ : value >= 0.6 ? 'text-yellow-600'
18
+ : 'text-red-500'
19
+
20
+ return (
21
+ <div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-5 flex flex-col gap-1">
22
+ <span className="text-xs font-medium text-gray-400 uppercase tracking-wider">{label}</span>
23
+ <div className="flex items-end gap-2 mt-1">
24
+ <span className={`text-3xl font-bold tabular-nums ${scoreColor}`}>{display}</span>
25
+ {delta != null && (
26
+ <span className={`text-sm font-medium mb-1 ${deltaClass}`}>
27
+ {deltaSign}{delta}{unit === '%' ? 'pp' : unit}
28
+ </span>
29
+ )}
30
+ </div>
31
+ <p className="text-xs text-gray-400 mt-1 leading-relaxed">{description}</p>
32
+ </div>
33
+ )
34
+ }
eval-dashboard/src/components/RunTable.jsx ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+
3
+ function score(v) {
4
+ if (v == null) return '—'
5
+ return `${(v * 100).toFixed(0)}%`
6
+ }
7
+
8
+ function scoreColor(v) {
9
+ if (v == null) return 'text-gray-300'
10
+ if (v >= 0.8) return 'text-emerald-600'
11
+ if (v >= 0.6) return 'text-yellow-600'
12
+ return 'text-red-500'
13
+ }
14
+
15
+ function Row({ item, idx }) {
16
+ const [open, setOpen] = useState(false)
17
+ return (
18
+ <>
19
+ <tr
20
+ className="hover:bg-gray-50 cursor-pointer border-t border-gray-100"
21
+ onClick={() => setOpen(v => !v)}
22
+ >
23
+ <td className="px-4 py-2.5 text-xs text-gray-500 font-mono w-8">{idx + 1}</td>
24
+ <td className="px-4 py-2.5 text-sm text-gray-700 max-w-xs">
25
+ <span className="line-clamp-2">{item.query}</span>
26
+ </td>
27
+ <td className={`px-4 py-2.5 text-sm font-mono font-medium text-center ${scoreColor(item.answer_correctness)}`}>
28
+ {score(item.answer_correctness)}
29
+ </td>
30
+ <td className={`px-4 py-2.5 text-sm font-mono font-medium text-center ${scoreColor(item.answer_relevancy)}`}>
31
+ {score(item.answer_relevancy)}
32
+ </td>
33
+ <td className={`px-4 py-2.5 text-sm font-mono font-medium text-center ${scoreColor(item.context_recall)}`}>
34
+ {score(item.context_recall)}
35
+ </td>
36
+ <td className={`px-4 py-2.5 text-sm font-mono font-medium text-center ${scoreColor(item.precision_at_5)}`}>
37
+ {score(item.precision_at_5)}
38
+ </td>
39
+ <td className="px-4 py-2.5 text-xs text-gray-400 font-mono text-right">
40
+ {item.latency_ms ? `${item.latency_ms}ms` : '—'}
41
+ </td>
42
+ <td className="px-4 py-2.5 text-gray-300 text-center">
43
+ <span>{open ? '▲' : '▼'}</span>
44
+ </td>
45
+ </tr>
46
+ {open && (
47
+ <tr className="bg-gray-50 border-t border-gray-100">
48
+ <td colSpan={8} className="px-6 py-4">
49
+ <div className="grid grid-cols-2 gap-4 text-xs">
50
+ <div>
51
+ <p className="font-semibold text-gray-500 mb-1">Generated answer</p>
52
+ <p className="text-gray-700 whitespace-pre-wrap leading-relaxed">{item.answer}</p>
53
+ </div>
54
+ <div>
55
+ <p className="font-semibold text-gray-500 mb-1">Ground truth</p>
56
+ <p className="text-gray-700 whitespace-pre-wrap leading-relaxed">{item.ground_truth}</p>
57
+ </div>
58
+ {item.correctness_reason && (
59
+ <div className="col-span-2">
60
+ <p className="font-semibold text-gray-500 mb-1">Correctness judge note</p>
61
+ <p className="text-gray-600 italic">{item.correctness_reason}</p>
62
+ </div>
63
+ )}
64
+ </div>
65
+ </td>
66
+ </tr>
67
+ )}
68
+ </>
69
+ )
70
+ }
71
+
72
+ export default function RunTable({ perQuery }) {
73
+ if (!perQuery?.length) return null
74
+ return (
75
+ <div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
76
+ <div className="px-6 py-4 border-b border-gray-100">
77
+ <h2 className="text-sm font-semibold text-gray-700">Per-Query Breakdown</h2>
78
+ <p className="text-xs text-gray-400 mt-0.5">Click any row to see generated answer vs ground truth</p>
79
+ </div>
80
+ <div className="overflow-x-auto">
81
+ <table className="w-full text-left">
82
+ <thead>
83
+ <tr className="bg-gray-50">
84
+ <th className="px-4 py-2.5 text-xs text-gray-400 font-medium">#</th>
85
+ <th className="px-4 py-2.5 text-xs text-gray-400 font-medium">Query</th>
86
+ <th className="px-4 py-2.5 text-xs text-gray-400 font-medium text-center">Correctness</th>
87
+ <th className="px-4 py-2.5 text-xs text-gray-400 font-medium text-center">Relevancy</th>
88
+ <th className="px-4 py-2.5 text-xs text-gray-400 font-medium text-center">Recall</th>
89
+ <th className="px-4 py-2.5 text-xs text-gray-400 font-medium text-center">P@5</th>
90
+ <th className="px-4 py-2.5 text-xs text-gray-400 font-medium text-right">Latency</th>
91
+ <th className="px-4 py-2.5 text-xs text-gray-400 font-medium text-center"></th>
92
+ </tr>
93
+ </thead>
94
+ <tbody>
95
+ {perQuery.map((item, i) => <Row key={i} item={item} idx={i} />)}
96
+ </tbody>
97
+ </table>
98
+ </div>
99
+ </div>
100
+ )
101
+ }
eval-dashboard/src/index.css ADDED
@@ -0,0 +1 @@
 
 
1
+ @import "tailwindcss";
eval-dashboard/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
eval-dashboard/vercel.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "buildCommand": "npm run build",
3
+ "outputDirectory": "dist",
4
+ "framework": "vite"
5
+ }
eval-dashboard/vite.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+
5
+ export default defineConfig({
6
+ plugins: [react(), tailwindcss()],
7
+ })
frontend/src/components/ChatArea.jsx CHANGED
@@ -6,7 +6,7 @@ export default function ChatArea({ onEvalEntry, hasDocuments, suggestedQuestion,
6
  const [messages, setMessages] = useState([]);
7
  const [input, setInput] = useState("");
8
  const [loading, setLoading] = useState(false);
9
- const [webSearch, setWebSearch] = useState(false);
10
 
11
  useEffect(() => {
12
  if (suggestedQuestion) {
@@ -32,14 +32,11 @@ export default function ChatArea({ onEvalEntry, hasDocuments, suggestedQuestion,
32
  role: "assistant",
33
  content: data.answer,
34
  sources: data.sources,
35
- faithfulness: data.faithfulness,
36
  },
37
  ]);
38
  onEvalEntry({
39
  query: question,
40
  answer: data.answer,
41
- faithfulness_score: data.faithfulness.score,
42
- reason: data.faithfulness.reason,
43
  });
44
  } catch (err) {
45
  const detail = err.response?.data?.detail || err.message;
@@ -115,31 +112,13 @@ export default function ChatArea({ onEvalEntry, hasDocuments, suggestedQuestion,
115
  {/* Input */}
116
  <form onSubmit={handleSend} className="p-4 bg-white border-t border-gray-100">
117
  <div className="flex gap-3 max-w-4xl mx-auto">
118
- <button
119
- type="button"
120
- onClick={() => setWebSearch((v) => !v)}
121
- disabled={loading || !hasDocuments}
122
- title={webSearch ? "Web search ON — click to disable" : "Web search OFF — click to enable"}
123
- className={`shrink-0 p-3 rounded-xl border text-sm font-medium transition-colors shadow-sm ${
124
- webSearch
125
- ? "bg-emerald-50 border-emerald-300 text-emerald-700 hover:bg-emerald-100"
126
- : "bg-gray-50 border-gray-200 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
127
- } disabled:opacity-40 disabled:cursor-not-allowed`}
128
- >
129
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
130
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
131
- d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
132
- </svg>
133
- </button>
134
  <input
135
  type="text"
136
  value={input}
137
  onChange={(e) => setInput(e.target.value)}
138
  placeholder={
139
  hasDocuments
140
- ? webSearch
141
- ? "Ask anything — searching docs + web..."
142
- : "Ask about your documents..."
143
  : "Upload documents first to start chatting"
144
  }
145
  className="flex-1 px-4 py-3 border border-gray-200 rounded-xl text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent disabled:bg-gray-50 disabled:text-gray-400 transition-shadow"
@@ -153,11 +132,6 @@ export default function ChatArea({ onEvalEntry, hasDocuments, suggestedQuestion,
153
  Send
154
  </button>
155
  </div>
156
- {webSearch && hasDocuments && (
157
- <p className="text-center text-xs text-emerald-600 mt-2">
158
- Web search active — answers will include live web context
159
- </p>
160
- )}
161
  </form>
162
  </div>
163
  );
 
6
  const [messages, setMessages] = useState([]);
7
  const [input, setInput] = useState("");
8
  const [loading, setLoading] = useState(false);
9
+ const webSearch = true;
10
 
11
  useEffect(() => {
12
  if (suggestedQuestion) {
 
32
  role: "assistant",
33
  content: data.answer,
34
  sources: data.sources,
 
35
  },
36
  ]);
37
  onEvalEntry({
38
  query: question,
39
  answer: data.answer,
 
 
40
  });
41
  } catch (err) {
42
  const detail = err.response?.data?.detail || err.message;
 
112
  {/* Input */}
113
  <form onSubmit={handleSend} className="p-4 bg-white border-t border-gray-100">
114
  <div className="flex gap-3 max-w-4xl mx-auto">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  <input
116
  type="text"
117
  value={input}
118
  onChange={(e) => setInput(e.target.value)}
119
  placeholder={
120
  hasDocuments
121
+ ? "Ask anything — searching docs + web..."
 
 
122
  : "Upload documents first to start chatting"
123
  }
124
  className="flex-1 px-4 py-3 border border-gray-200 rounded-xl text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent disabled:bg-gray-50 disabled:text-gray-400 transition-shadow"
 
132
  Send
133
  </button>
134
  </div>
 
 
 
 
 
135
  </form>
136
  </div>
137
  );
frontend/src/components/MessageBubble.jsx CHANGED
@@ -26,56 +26,6 @@ function WebSourcesList({ sources }) {
26
  );
27
  }
28
 
29
- function FaithfulnessBadge({ faithfulness }) {
30
- if (!faithfulness || faithfulness.score < 0) {
31
- return (
32
- <span className="inline-flex items-center gap-1.5 text-xs text-gray-400 bg-gray-100 px-2.5 py-1 rounded-full">
33
- <span className="w-1.5 h-1.5 rounded-full bg-gray-400" />
34
- Eval failed
35
- </span>
36
- );
37
- }
38
-
39
- const { score, reason } = faithfulness;
40
-
41
- let dotColor, bgColor, textColor, label;
42
- if (score >= 4) {
43
- dotColor = "bg-green-500";
44
- bgColor = "bg-green-50";
45
- textColor = "text-green-700";
46
- label = "Faithful";
47
- } else if (score === 3) {
48
- dotColor = "bg-yellow-500";
49
- bgColor = "bg-yellow-50";
50
- textColor = "text-yellow-700";
51
- label = "Moderate";
52
- } else {
53
- dotColor = "bg-red-500";
54
- bgColor = "bg-red-50";
55
- textColor = "text-red-700";
56
- label = "Low";
57
- }
58
-
59
- return (
60
- <span className="inline-flex items-center gap-1.5">
61
- <span
62
- className={`inline-flex items-center gap-1.5 text-xs font-medium px-2.5 py-1 rounded-full ${bgColor} ${textColor}`}
63
- title={reason}
64
- >
65
- <span className={`w-1.5 h-1.5 rounded-full ${dotColor}`} />
66
- {label} ({score}/5)
67
- </span>
68
- <span className="group relative">
69
- <svg className="w-3.5 h-3.5 text-gray-400 cursor-help" fill="currentColor" viewBox="0 0 20 20">
70
- <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
71
- </svg>
72
- <span className="pointer-events-none absolute bottom-6 left-0 z-10 w-56 rounded-lg bg-gray-800 px-2.5 py-2 text-xs text-white opacity-0 group-hover:opacity-100 transition-opacity shadow-lg">
73
- LLM-as-judge score (1–5). Measures how grounded the answer is in the retrieved documents. 4–5 = faithful, 3 = partially supported, 1–2 = hallucination risk.
74
- </span>
75
- </span>
76
- </span>
77
- );
78
- }
79
 
80
  function CitedText({ text, onCitationClick }) {
81
  if (!text) return null;
@@ -135,12 +85,6 @@ export default function MessageBubble({ message }) {
135
  <WebSourcesList sources={message.sources} />
136
  )}
137
 
138
- {!isUser && message.faithfulness && (
139
- <div className="mt-3">
140
- <FaithfulnessBadge faithfulness={message.faithfulness} />
141
- </div>
142
- )}
143
-
144
  {!isUser && message.sources && (
145
  <SourceExpander sources={message.sources} />
146
  )}
 
26
  );
27
  }
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  function CitedText({ text, onCitationClick }) {
31
  if (!text) return null;
 
85
  <WebSourcesList sources={message.sources} />
86
  )}
87
 
 
 
 
 
 
 
88
  {!isUser && message.sources && (
89
  <SourceExpander sources={message.sources} />
90
  )}
scripts/run_eval_versioned.py ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Versioned eval script for Prism. Writes timestamped JSON to eval-dashboard/public/data/runs/
3
+ and updates eval-dashboard/public/data/index.json.
4
+
5
+ Metrics:
6
+ answer_correctness — LLM judge (8B): generated answer vs ground_truth, 0–1
7
+ answer_relevancy — RAGAS: does answer address the question?
8
+ context_recall — RAGAS: did retrieval surface all needed chunks?
9
+ precision_at_5 — (relevant chunks in top-5) / 5, source + keyword match
10
+ latency_p50/p95/p99 — per-query end-to-end timing (retrieval + LLM, no Tavily)
11
+
12
+ Usage:
13
+ python scripts/run_eval_versioned.py --version v2.0 --tag "baseline" --n 50
14
+ python scripts/run_eval_versioned.py --version v2.1 --tag "HyDE enabled" --n 50
15
+
16
+ Prerequisites:
17
+ pip install 'ragas>=0.2.0,<0.3.0' datasets
18
+ .env with GROQ_API_KEY + EURON_API_KEY
19
+ scripts/run_ingest.py run first (ChromaDB populated)
20
+ """
21
+
22
+ import argparse
23
+ import json
24
+ import math
25
+ import os
26
+ import sys
27
+ import time
28
+ from datetime import datetime, date
29
+ from pathlib import Path
30
+
31
+ import numpy as np
32
+
33
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
34
+ from dotenv import load_dotenv
35
+ load_dotenv()
36
+
37
+ EVAL_PAIRS_PATH = Path(__file__).resolve().parent.parent / "data" / "ground_truth" / "eval_pairs.json"
38
+ DASHBOARD_RUNS_DIR = Path(__file__).resolve().parent.parent / "eval-dashboard" / "public" / "data" / "runs"
39
+ DASHBOARD_INDEX_PATH = Path(__file__).resolve().parent.parent / "eval-dashboard" / "public" / "data" / "index.json"
40
+
41
+ CORRECTNESS_PROMPT = """\
42
+ You are an evaluation judge. Score the generated answer against the reference answer.
43
+
44
+ Use a 1–5 integer scale:
45
+ 5 — All key facts present and correct
46
+ 4 — Most key facts correct, minor omissions or imprecision
47
+ 3 — Some key facts correct, moderate gaps
48
+ 2 — Few facts correct, significant errors or hallucinations
49
+ 1 — Completely wrong, irrelevant, or contradicts reference
50
+
51
+ Reference answer: {ground_truth}
52
+
53
+ Generated answer: {answer}
54
+
55
+ Respond with JSON only — a single object, nothing else:
56
+ {{"score": 4, "reason": "one sentence"}}"""
57
+
58
+
59
+ def _build_retriever(config, workspace_id="default"):
60
+ from server.bm25_index import build_from_vectorstore
61
+ from server.reranker import load_reranker
62
+ from server.retriever import HybridRetriever
63
+ from langchain_chroma import Chroma
64
+ from langchain_openai import OpenAIEmbeddings
65
+
66
+ print("Loading vectorstore...")
67
+ embeddings = OpenAIEmbeddings(
68
+ model="text-embedding-3-small",
69
+ openai_api_key=os.getenv("EURON_API_KEY", ""),
70
+ openai_api_base="https://api.euron.one/api/v1/euri",
71
+ )
72
+ vectorstore = Chroma(
73
+ collection_name=workspace_id,
74
+ embedding_function=embeddings,
75
+ persist_directory="./chroma_db",
76
+ )
77
+ if vectorstore._collection.count() == 0:
78
+ print(f"ERROR: ChromaDB collection '{workspace_id}' empty. Run scripts/run_ingest.py first.")
79
+ sys.exit(1)
80
+
81
+ print("Building BM25 index...")
82
+ build_from_vectorstore(vectorstore, workspace_id=workspace_id)
83
+
84
+ print("Loading reranker...")
85
+ load_reranker()
86
+
87
+ retrieval_cfg = config.get("retrieval", {})
88
+ retriever = HybridRetriever(
89
+ vectorstore=vectorstore,
90
+ dense_weight=retrieval_cfg.get("dense_weight", 0.7),
91
+ sparse_weight=retrieval_cfg.get("sparse_weight", 0.3),
92
+ retrieve_k=retrieval_cfg.get("retrieve_k", 10),
93
+ rerank_k=retrieval_cfg.get("rerank_k", 5),
94
+ workspace_id=workspace_id,
95
+ use_hyde=retrieval_cfg.get("hyde_enabled", False),
96
+ )
97
+ return retriever
98
+
99
+
100
+ def _make_llm(model: str, temperature: float = 0.1, max_tokens: int = 500):
101
+ from langchain_groq import ChatGroq
102
+ return ChatGroq(
103
+ model=model,
104
+ api_key=os.getenv("GROQ_API_KEY", ""),
105
+ temperature=temperature,
106
+ max_tokens=max_tokens,
107
+ )
108
+
109
+
110
+ def _answer_query(llm, retriever, query: str) -> tuple[str, list, list[dict]]:
111
+ """Returns (answer, lc_docs, chunk_dicts)."""
112
+ from langchain_core.messages import HumanMessage, SystemMessage
113
+
114
+ docs = retriever.invoke(query)
115
+ contexts = [d.page_content for d in docs]
116
+ chunk_dicts = [
117
+ {"content": d.page_content, "source": d.metadata.get("source", "")}
118
+ for d in docs
119
+ ]
120
+ ctx_text = "\n\n".join(f"[Doc {i+1}]\n{c}" for i, c in enumerate(contexts))
121
+ messages = [
122
+ SystemMessage(content=(
123
+ "You are a research assistant. Answer using only the provided context. "
124
+ "Be concise and precise. State which document supports your answer."
125
+ )),
126
+ HumanMessage(content=f"Context:\n{ctx_text}\n\nQuestion: {query}\n\nAnswer:"),
127
+ ]
128
+ response = llm.invoke(messages)
129
+ return response.content.strip(), docs, chunk_dicts
130
+
131
+
132
+ def _score_correctness(judge_llm, answer: str, ground_truth: str) -> tuple[float, str]:
133
+ """Returns (score 0–1, reason). Judge uses 1–5 integer scale normalized to 0–1."""
134
+ from langchain_core.messages import HumanMessage
135
+ import re
136
+ prompt = CORRECTNESS_PROMPT.format(ground_truth=ground_truth, answer=answer)
137
+ try:
138
+ resp = judge_llm.invoke([HumanMessage(content=prompt)])
139
+ raw = resp.content.strip()
140
+ # extract JSON block
141
+ start = raw.find("{")
142
+ end = raw.rfind("}") + 1
143
+ if start == -1:
144
+ raise ValueError(f"No JSON in response: {raw[:120]}")
145
+ parsed = json.loads(raw[start:end])
146
+ raw_score = float(parsed["score"])
147
+ # normalize 1–5 → 0–1; if model returns 0–1 directly, keep as-is
148
+ score = (raw_score - 1) / 4 if raw_score > 1 else raw_score
149
+ score = max(0.0, min(1.0, score))
150
+ return round(score, 4), parsed.get("reason", "")
151
+ except json.JSONDecodeError:
152
+ # fallback: find first integer 1–5 in response
153
+ m = re.search(r'\b([1-5])\b', raw)
154
+ if m:
155
+ raw_score = int(m.group(1))
156
+ score = (raw_score - 1) / 4
157
+ return round(score, 4), f"fallback parse from: {raw[:80]}"
158
+ print(f" correctness judge parse failed: {raw[:120]}")
159
+ return None, f"parse error: {raw[:80]}"
160
+ except Exception as e:
161
+ print(f" correctness judge error: {e}")
162
+ return None, f"error: {e}"
163
+
164
+
165
+ def _compute_percentile(values: list[float], p: int) -> int:
166
+ if not values:
167
+ return 0
168
+ return int(np.percentile(values, p))
169
+
170
+
171
+ def main():
172
+ parser = argparse.ArgumentParser()
173
+ parser.add_argument("--version", required=True, help="Version tag e.g. v2.1")
174
+ parser.add_argument("--tag", required=True, help="Short description e.g. 'HyDE enabled'")
175
+ parser.add_argument("--n", type=int, default=50, help="Number of eval pairs (default 50)")
176
+ parser.add_argument("--workspace", default="default", help="ChromaDB workspace collection name")
177
+ parser.add_argument("--judge-model", default="llama-3.1-8b-instant",
178
+ help="Groq judge model for correctness + RAGAS (default: llama-3.1-8b-instant)")
179
+ args = parser.parse_args()
180
+
181
+ print(f"=== Prism Eval — {args.version} | {args.tag} ===\n")
182
+
183
+ missing = [k for k in ("GROQ_API_KEY", "EURON_API_KEY") if not os.getenv(k)]
184
+ if missing:
185
+ print(f"ERROR: Missing env vars: {', '.join(missing)}")
186
+ sys.exit(1)
187
+
188
+ try:
189
+ from ragas import evaluate, EvaluationDataset, SingleTurnSample, RunConfig
190
+ from ragas.metrics import AnswerRelevancy, ContextRecall
191
+ from ragas.llms import LangchainLLMWrapper
192
+ from ragas.embeddings import LangchainEmbeddingsWrapper
193
+ from langchain_openai import OpenAIEmbeddings as _OAIEmb
194
+ except ImportError as e:
195
+ print(f"ERROR: {e}\nInstall: pip install 'ragas>=0.2.0,<0.3.0' datasets")
196
+ sys.exit(1)
197
+
198
+ from server.utils import load_config
199
+ from server.eval.precision import compute_precision_at_k
200
+
201
+ config = load_config()
202
+ retriever = _build_retriever(config, workspace_id=args.workspace)
203
+ answer_llm = _make_llm(config["llm"]["model"], temperature=0.1, max_tokens=500)
204
+ judge_llm = _make_llm(args.judge_model, temperature=0.0, max_tokens=200)
205
+
206
+ ragas_llm = LangchainLLMWrapper(_make_llm(args.judge_model, temperature=0.0, max_tokens=500))
207
+ ragas_emb = LangchainEmbeddingsWrapper(
208
+ _OAIEmb(
209
+ model="text-embedding-3-small",
210
+ openai_api_key=os.getenv("EURON_API_KEY", ""),
211
+ openai_api_base="https://api.euron.one/api/v1/euri",
212
+ )
213
+ )
214
+
215
+ with open(EVAL_PAIRS_PATH) as f:
216
+ all_pairs = json.load(f)
217
+ pairs = all_pairs[:args.n]
218
+ print(f"Evaluating {len(pairs)} pairs with judge={args.judge_model}\n")
219
+
220
+ per_query = []
221
+ ragas_samples = []
222
+ latencies = []
223
+
224
+ for i, pair in enumerate(pairs):
225
+ query = pair["query"]
226
+ ground_truth = pair.get("ground_truth", "")
227
+ print(f"[{i+1:02d}/{len(pairs)}] {query[:70]}")
228
+
229
+ try:
230
+ t0 = time.time()
231
+ answer, lc_docs, chunk_dicts = _answer_query(answer_llm, retriever, query)
232
+ latency_ms = int((time.time() - t0) * 1000)
233
+ latencies.append(latency_ms)
234
+
235
+ correctness, correctness_reason = _score_correctness(judge_llm, answer, ground_truth)
236
+ precision = compute_precision_at_k(query, chunk_dicts, pair, k=5)
237
+
238
+ print(f" correctness={correctness:.2f} p@5={precision:.2f} latency={latency_ms}ms")
239
+
240
+ ragas_samples.append(SingleTurnSample(
241
+ user_input=query,
242
+ response=answer,
243
+ retrieved_contexts=[d.page_content for d in lc_docs],
244
+ reference=ground_truth,
245
+ ))
246
+
247
+ per_query.append({
248
+ "query": query,
249
+ "answer": answer,
250
+ "ground_truth": ground_truth,
251
+ "answer_correctness": round(correctness, 4) if correctness is not None else None,
252
+ "correctness_reason": correctness_reason,
253
+ "answer_relevancy": None, # filled after RAGAS run
254
+ "context_recall": None, # filled after RAGAS run
255
+ "precision_at_5": precision,
256
+ "latency_ms": latency_ms,
257
+ "retrieved_sources": [c["source"] for c in chunk_dicts],
258
+ })
259
+
260
+ except Exception as e:
261
+ print(f" SKIP: {e}")
262
+
263
+ # RAGAS batch eval — max_workers=2 to avoid Groq free-tier rate limit timeouts
264
+ print(f"\nRunning RAGAS on {len(ragas_samples)} samples (answer_relevancy + context_recall)...")
265
+ print("Using max_workers=2 to avoid Groq rate limits — will take ~15-20 min for 50 samples")
266
+ dataset = EvaluationDataset(samples=ragas_samples)
267
+ ragas_cfg = RunConfig(timeout=120, max_workers=2, max_retries=5)
268
+ results = evaluate(
269
+ dataset=dataset,
270
+ metrics=[
271
+ AnswerRelevancy(llm=ragas_llm, embeddings=ragas_emb),
272
+ ContextRecall(llm=ragas_llm),
273
+ ],
274
+ run_config=ragas_cfg,
275
+ )
276
+ scores_df = results.to_pandas()
277
+ print(f"RAGAS columns: {list(scores_df.columns)}")
278
+
279
+ def _safe_float(val):
280
+ """Return float or None — never NaN."""
281
+ try:
282
+ f = float(val)
283
+ return None if math.isnan(f) else round(f, 4)
284
+ except (TypeError, ValueError):
285
+ return None
286
+
287
+ # patch per_query with RAGAS per-sample scores
288
+ ragas_idx = 0
289
+ for item in per_query:
290
+ if ragas_idx < len(scores_df):
291
+ row = scores_df.iloc[ragas_idx]
292
+ item["answer_relevancy"] = _safe_float(row.get("answer_relevancy"))
293
+ item["context_recall"] = _safe_float(row.get("context_recall"))
294
+ ragas_idx += 1
295
+
296
+ def safe_mean(key):
297
+ vals = [r[key] for r in per_query if r.get(key) is not None]
298
+ return round(float(np.mean(vals)), 4) if vals else None
299
+
300
+ metrics = {
301
+ "answer_correctness": safe_mean("answer_correctness"),
302
+ "answer_relevancy": safe_mean("answer_relevancy"),
303
+ "context_recall": safe_mean("context_recall"),
304
+ "precision_at_5": safe_mean("precision_at_5"),
305
+ "latency_p50_ms": _compute_percentile(latencies, 50),
306
+ "latency_p95_ms": _compute_percentile(latencies, 95),
307
+ "latency_p99_ms": _compute_percentile(latencies, 99),
308
+ }
309
+
310
+ retrieval_cfg = config.get("retrieval", {})
311
+ run_data = {
312
+ "version": args.version,
313
+ "tag": args.tag,
314
+ "computed_at": datetime.now().isoformat(timespec="seconds"),
315
+ "sample_count": len(per_query),
316
+ "config": {
317
+ "hyde_enabled": retrieval_cfg.get("hyde_enabled", False),
318
+ "retrieve_k": retrieval_cfg.get("retrieve_k", 10),
319
+ "rerank_k": retrieval_cfg.get("rerank_k", 5),
320
+ "llm": config.get("llm", {}).get("model", "unknown"),
321
+ "judge_model": args.judge_model,
322
+ "workspace": args.workspace,
323
+ },
324
+ "metrics": metrics,
325
+ "per_query": per_query,
326
+ }
327
+
328
+ # Write run file
329
+ DASHBOARD_RUNS_DIR.mkdir(parents=True, exist_ok=True)
330
+ run_filename = f"{args.version}_{date.today().strftime('%Y%m%d')}.json"
331
+ run_path = DASHBOARD_RUNS_DIR / run_filename
332
+ with open(run_path, "w") as f:
333
+ json.dump(run_data, f, indent=2)
334
+ print(f"\nRun written to: {run_path}")
335
+
336
+ # Update index
337
+ if DASHBOARD_INDEX_PATH.exists():
338
+ with open(DASHBOARD_INDEX_PATH) as f:
339
+ index = json.load(f)
340
+ else:
341
+ index = []
342
+
343
+ # Replace existing entry for same version, else append
344
+ entry = {"version": args.version, "tag": args.tag, "date": str(date.today()), "file": run_filename}
345
+ index = [e for e in index if e["version"] != args.version]
346
+ index.append(entry)
347
+ index.sort(key=lambda e: e["date"])
348
+
349
+ with open(DASHBOARD_INDEX_PATH, "w") as f:
350
+ json.dump(index, f, indent=2)
351
+ print(f"Index updated: {DASHBOARD_INDEX_PATH}")
352
+
353
+ print(f"\n=== Results — {args.version} ===")
354
+ for k, v in metrics.items():
355
+ print(f" {k:25s}: {v}")
356
+ print(f"\nCommit eval-dashboard/public/data/ to update the live dashboard.")
357
+
358
+
359
+ if __name__ == "__main__":
360
+ main()
server/routes/chat.py CHANGED
@@ -5,7 +5,6 @@ from pydantic import BaseModel
5
 
6
  from server.chain import run_query_with_web, condense_question
7
  from server.memory import clear_memory
8
- from server.eval.faithfulness import score_faithfulness
9
  from server.utils import setup_logger
10
 
11
  logger = setup_logger(__name__)
@@ -15,7 +14,7 @@ router = APIRouter()
15
 
16
  class ChatRequest(BaseModel):
17
  question: str
18
- web_search: bool = False
19
 
20
 
21
  @router.post("/chat")
@@ -64,12 +63,9 @@ async def chat(
64
  if "citation_index" not in src or src["citation_index"] is None:
65
  src["citation_index"] = i + 1
66
 
67
- faithfulness = score_faithfulness(result["answer"], all_sources)
68
-
69
  logger.info(
70
- "RESPONSE | workspace=%s | faithfulness=%s | rag_sources=%d | web_sources=%d",
71
  workspace,
72
- faithfulness["score"],
73
  len(result["source_documents"]),
74
  len(web_sources),
75
  )
@@ -78,8 +74,6 @@ async def chat(
78
  "query": body.question,
79
  "answer": result["answer"],
80
  "contexts": [doc["content"] for doc in all_sources],
81
- "faithfulness_score": faithfulness["score"],
82
- "reason": faithfulness["reason"],
83
  })
84
 
85
  retrieval_method = result.get("retrieval_method", "hybrid+rerank")
@@ -91,7 +85,6 @@ async def chat(
91
  return {
92
  "answer": result["answer"],
93
  "sources": all_sources,
94
- "faithfulness": faithfulness,
95
  "retrieval_method": retrieval_method,
96
  }
97
 
 
5
 
6
  from server.chain import run_query_with_web, condense_question
7
  from server.memory import clear_memory
 
8
  from server.utils import setup_logger
9
 
10
  logger = setup_logger(__name__)
 
14
 
15
  class ChatRequest(BaseModel):
16
  question: str
17
+ web_search: bool = True
18
 
19
 
20
  @router.post("/chat")
 
63
  if "citation_index" not in src or src["citation_index"] is None:
64
  src["citation_index"] = i + 1
65
 
 
 
66
  logger.info(
67
+ "RESPONSE | workspace=%s | rag_sources=%d | web_sources=%d",
68
  workspace,
 
69
  len(result["source_documents"]),
70
  len(web_sources),
71
  )
 
74
  "query": body.question,
75
  "answer": result["answer"],
76
  "contexts": [doc["content"] for doc in all_sources],
 
 
77
  })
78
 
79
  retrieval_method = result.get("retrieval_method", "hybrid+rerank")
 
85
  return {
86
  "answer": result["answer"],
87
  "sources": all_sources,
 
88
  "retrieval_method": retrieval_method,
89
  }
90