diff --git a/.gitignore b/.gitignore index 857d3ac7d7..6cc536e8d3 100644 --- a/.gitignore +++ b/.gitignore @@ -25,8 +25,10 @@ coverage/ *.coverage *.coveragexml -# JetBrains IntelliJ IDEA +# Code editors .idea +.windsurf +.cursor # Hidden macOS files .DS_Store @@ -49,3 +51,4 @@ packages/cli/temp # React Router .react-router apps/www/build +*env.backup.* diff --git a/apps/ai-api/.ai-env.example b/apps/ai-api/.ai-env.example new file mode 100644 index 0000000000..95bcf5c3f0 --- /dev/null +++ b/apps/ai-api/.ai-env.example @@ -0,0 +1,32 @@ +# =========================== +# AI/Search Environment (.ai-env.example) +# Copy to .ai-env and fill values. This file is NOT loaded automatically by shells. +# Our scripts try several candidate paths to load .ai-env. +# =========================== + +# ---- Meilisearch (self-hosted) ---- +# Base URL to your Meilisearch instance +MEILISEARCH_API_URL=http://localhost:7700 + +# Admin key (created via /keys using the master key). Used by ingest/setup scripts and admin-only endpoints +MEILISEARCH_ADMIN_KEY=REPLACE_WITH_CREATED_ADMIN_KEY + +# Search key (created via /keys). Used by the backend for search-only client if exposed +MEILISEARCH_SEARCH_KEY=REPLACE_WITH_CREATED_SEARCH_KEY + +# Index name used across scripts and server +MEILISEARCH_PROJECT_NAME=designsystemet-search + +# Embedder UID (must match what you set via setup:embedder) +MEILISEARCH_EMBEDDER_UID=azure-openai-small + +# ---- Azure OpenAI (embeddings + chat) ---- +AZURE_KEY=REPLACE +AZURE_ENDPOINT=REPLACE +AZURE_API_VERSION=2024-08-01-preview +AZURE_EMBEDDING_DEPLOY_SMALL=text-embedding-3-small +AZURE_GPT_DEPLOY=gpt-4o + +# Notes: +# - Use the SAME embedding model for ingestion and query. Do NOT mix models. +# - After self-hosting Meilisearch, create Admin/Search keys and paste them here. diff --git a/apps/ai-api/AI_SEARCH_CHECKLIST.md b/apps/ai-api/AI_SEARCH_CHECKLIST.md new file mode 100644 index 0000000000..afddaa5d59 --- /dev/null +++ b/apps/ai-api/AI_SEARCH_CHECKLIST.md @@ -0,0 +1,257 @@ +# AI Search Setup Checklist + +Use this checklist to ensure all steps are completed correctly. + +## Prerequisites ✓ +- [ ] Node.js >= 22.17.1 installed +- [ ] pnpm 10.13.1 installed +- [ ] Docker and Docker Compose installed +- [ ] Repository cloned and accessible +- [ ] Azure OpenAI credentials received (in `.ai-env` file) + +--- + +## Setup Steps + +### 1. Install Dependencies +- [ ] Run `pnpm install` from repo root + +### 2. Meilisearch Setup +- [ ] Navigate to `infra/meilisearch/` +- [ ] Copy `.env.example` to `.env` +- [ ] Set `MEILI_MASTER_KEY` to a long random string (64+ chars) +- [ ] Start container: `docker compose -f infra/meilisearch/docker-compose.yml up -d` +- [ ] Verify running: `docker ps | grep meilisearch` +- [ ] Meilisearch accessible at `http://localhost:7700` + +### 3. Create API Keys +- [ ] Export master key: `export MEILI_MASTER_KEY=your-master-key` +- [ ] Run: `pnpm -w -C apps/ai-api run setup:create-keys` +- [ ] Copy **Admin Key** (save it) +- [ ] Copy **Search Key** (save it) + +### 4. Configure Environment +- [ ] Copy `.ai-env.example` to `.ai-env` (repo root) +- [ ] Paste Admin Key into `MEILISEARCH_ADMIN_KEY` +- [ ] Paste Search Key into `MEILISEARCH_SEARCH_KEY` +- [ ] Verify Azure credentials are present: + - [ ] `AZURE_KEY` + - [ ] `AZURE_ENDPOINT` + - [ ] `AZURE_API_VERSION` + - [ ] `AZURE_EMBEDDING_DEPLOY_SMALL` + - [ ] `AZURE_GPT_DEPLOY` + +### 5. Initialize Search Index +- [ ] Navigate to `apps/ai-api/` +- [ ] Run: `pnpm run setup` (runs all setup steps - takes 5-10 min) +- [ ] Verify output shows ~768 documents indexed at the end + +### 6. Start AI API +- [ ] From `apps/ai-api/`: `pnpm run dev` +- [ ] Verify server starts on port 3001 +- [ ] Test health: `curl http://localhost:3001/health` +- [ ] Test search: `curl -X POST http://localhost:3001/api/search -H "Content-Type: application/json" -d '{"query": "button"}'` + +### 7. Start WWW Frontend +- [ ] Open new terminal +- [ ] From repo root: `pnpm run www` +- [ ] Verify server starts on port 5173 +- [ ] Open browser to `http://localhost:5173` + +### 8. Test Search Feature +- [ ] Click search icon (magnifying glass) in header +- [ ] Search dialog opens +- [ ] Try single-word query: `button` (Quick mode) +- [ ] Try multi-word query: `how to use button component` (Quick + Smart mode) +- [ ] Verify results appear +- [ ] Verify AI answer appears for multi-word queries +- [ ] Click "Show more" to expand AI answer +- [ ] Verify sources are clickable +- [ ] Test synonym: search `modal` (should find Dialog) + +--- + +## Verification Tests + +### Meilisearch +```bash +# Container is running +docker ps | grep meilisearch + +# Check logs (should show no errors) +docker compose -f infra/meilisearch/docker-compose.yml logs --tail=50 + +# Check health +curl http://localhost:7700/health +``` + +### AI API +```bash +# Health check +curl http://localhost:3001/health + +# Quick search +curl -X POST http://localhost:3001/api/search \ + -H "Content-Type: application/json" \ + -d '{"query": "button"}' + +# AI search +curl -X POST http://localhost:3001/api/ai-search \ + -H "Content-Type: application/json" \ + -d '{"query": "how do I use the button component"}' + +# Check index status (or re-run full setup) +cd apps/ai-api +pnpm run setup +``` + +### Frontend +- [ ] Open `http://localhost:5173` in browser +- [ ] No console errors +- [ ] Search icon visible in header +- [ ] Search dialog opens on click +- [ ] Search input is focused +- [ ] Suggestions appear when empty +- [ ] Results appear when typing +- [ ] Links are clickable +- [ ] Dialog closes on Escape or X button + +--- + +## Common Issues + +### "Connection refused" to Meilisearch +- [ ] Check Docker container is running: `docker ps` +- [ ] Check port 7700 is not in use: `lsof -i :7700` +- [ ] Restart container: `docker compose -f infra/meilisearch/docker-compose.yml restart` + +### "Invalid API key" +- [ ] Verify `.ai-env` has correct Admin/Search keys +- [ ] Recreate keys if needed (step 3) +- [ ] Ensure no extra spaces in `.ai-env` values + +### "No documents found" +- [ ] Re-run complete setup: `pnpm run setup` +- [ ] Check for errors in setup output +- [ ] Verify `.ai-env` has correct Azure credentials + +### "Azure OpenAI error" +- [ ] Verify Azure credentials in `.ai-env` +- [ ] Check Azure endpoint is accessible +- [ ] Verify deployment names match (text-embedding-3-small, gpt-4o) + +### Search returns no results +- [ ] Re-run complete setup: `pnpm run setup` +- [ ] Check Meilisearch logs for errors +- [ ] Verify documents were ingested (shown at end of setup) + +### Frontend can't connect to API +- [ ] Verify AI API is running on port 3001 +- [ ] Check CORS configuration in `apps/ai-api/src/server.ts` +- [ ] Check browser console for CORS errors + +--- + +## Environment Files Checklist + +### `.ai-env` (repo root) - REQUIRED +```bash +MEILISEARCH_API_URL=http://localhost:7700 +MEILISEARCH_ADMIN_KEY= +MEILISEARCH_SEARCH_KEY= +MEILISEARCH_PROJECT_NAME=designsystemet-search +MEILISEARCH_EMBEDDER_UID=azure-openai-small +AZURE_KEY= +AZURE_ENDPOINT= +AZURE_API_VERSION=2024-08-01-preview +AZURE_EMBEDDING_DEPLOY_SMALL=text-embedding-3-small +AZURE_GPT_DEPLOY=gpt-4o +``` + +### `infra/meilisearch/.env` - REQUIRED +```bash +MEILI_MASTER_KEY= +MEILI_ENV=production +MEILI_NO_ANALYTICS=true +MEILI_EXPERIMENTAL_ENABLE_VECTOR_SEARCH=enabled +MEILI_EXPERIMENTAL_ENABLE_EMBEDDERS=enabled +``` + +--- + +## Running Services (3 Terminals) + +### Terminal 1: Meilisearch +```bash +cd infra/meilisearch +docker compose up +# Keep running, watch logs +``` + +### Terminal 2: AI API +```bash +cd apps/ai-api +pnpm run dev +# Keep running, watch logs +``` + +### Terminal 3: WWW Frontend +```bash +# From repo root +pnpm run www +# Keep running, watch logs +``` + +--- + +## Success Criteria + +✅ All three services running without errors +✅ Search dialog opens in browser +✅ Single-word queries return Quick results +✅ Multi-word queries return both Quick and Smart results +✅ AI answers are formatted with markdown +✅ Sources are expandable and clickable +✅ Synonyms work (e.g., "modal" finds Dialog) +✅ No CORS errors in browser console +✅ Health endpoint returns `{"status": "ok"}` + +--- + +## Cleanup (Optional) + +To stop all services: + +```bash +# Stop frontend (Ctrl+C in Terminal 3) +# Stop AI API (Ctrl+C in Terminal 2) + +# Stop Meilisearch container +docker compose -f infra/meilisearch/docker-compose.yml down + +# To also remove data volume (WARNING: deletes all indexed documents) +docker compose -f infra/meilisearch/docker-compose.yml down -v +``` + +--- + +## Quick Start (After Initial Setup) + +Once everything is configured, you only need to run: + +```bash +# Terminal 1 +docker compose -f infra/meilisearch/docker-compose.yml up -d + +# Terminal 2 +cd apps/ai-api && pnpm run dev + +# Terminal 3 +pnpm run www +``` + +Then open `http://localhost:5173` and start searching! + +--- + +**Status**: [ ] Setup Complete | [ ] Tested and Working | [ ] Ready for Development diff --git a/apps/ai-api/AI_SEARCH_QUICK_START.md b/apps/ai-api/AI_SEARCH_QUICK_START.md new file mode 100644 index 0000000000..95bb2b9571 --- /dev/null +++ b/apps/ai-api/AI_SEARCH_QUICK_START.md @@ -0,0 +1,348 @@ +# AI Search Quick Start Guide + +**TL;DR**: Get the AI search running in 10 minutes. + +**Updated**: Step 5 uses individual commands for reliability. Each step is quick and clear! ✨ + +--- + +## Prerequisites Check + +```bash +# Check Node.js version (need >= 22.17.1) +node --version + +# Check pnpm version (need 10.13.1) +pnpm --version + +# Check Docker +docker --version +docker compose version +``` + +### Don't Have Docker? + +Download from https://www.docker.com/products/docker-desktop/ + +This also install Docker Compose and Docker CLI, the tools we use to run Meilisearch. It may require a restart after install. + +--- + +## 5-Step Setup + +### 1️⃣ Install Dependencies (2 min) + +```bash +cd /path/to/designsystemet +pnpm install +``` + +### 2️⃣ Start Meilisearch (1 min) + +```bash +# Create environment file +cd infra/meilisearch +cp .env.example .env + +# Edit .env and set MEILI_MASTER_KEY to a long random string +# Example: MEILI_MASTER_KEY=my-super-secret-key-that-is-very-long-and-random-12345678 + +# Start container +docker compose up -d + +# Verify it's running +docker ps | grep meilisearch +``` + +### 3️⃣ Create API Keys (1 min) + +```bash +# Set master key (use the same value from .env) +export MEILI_MASTER_KEY=my-super-secret-key-that-is-very-long-and-random-12345678 + +# Generate keys +cd ../../ # back to repo root +pnpm -w -C apps/ai-api run setup:create-keys + +# Copy the two keys that are printed: +# - Admin Key: meili_admin_xxxxx +# - Search Key: meili_search_xxxxx +``` + +### 4️⃣ Configure Environment (1 min) + +```bash +# Create .ai-env from example +cp .ai-env.example .ai-env + +# Edit .ai-env and paste the keys from step 3 +nano .ai-env + +# Make sure these are set: +# MEILISEARCH_ADMIN_KEY=meili_admin_xxxxx +# MEILISEARCH_SEARCH_KEY=meili_search_xxxxx +# AZURE_KEY= +# AZURE_ENDPOINT= +``` + +### 5️⃣ Initialize Search Index (5-10 min) + +```bash +cd apps/ai-api + +# Step 1: Configure embedder +pnpm run setup:embedder + +# Step 2: Ingest documents (takes 5-10 minutes) +pnpm run setup:ingest + +# Step 3: Configure synonyms +pnpm run setup:synonyms + +# Step 4: Verify setup +pnpm run check:meili +# Should show: "📊 Index 'designsystemet-search': 621 documents" +``` + +--- + +## Running the System + +Open **3 terminal windows**: + +### Terminal 1: Meilisearch +```bash +docker compose -f infra/meilisearch/docker-compose.yml up +``` + +### Terminal 2: AI API +```bash +cd apps/ai-api +pnpm run dev +``` + +### Terminal 3: Frontend +```bash +# From repo root +pnpm run www +``` + +Then open: **http://localhost:5173** + +--- + +## Quick Test + +1. Click the **search icon** (magnifying glass) in the header +2. Type: **button** +3. You should see Quick results appear +4. Type: **how to use button component** +5. You should see both Quick results and an AI answer + +--- + +## Common Commands + +### Check Status +```bash +# Meilisearch health +curl http://localhost:7700/health + +# AI API health +curl http://localhost:3001/health + +# Check index +cd apps/ai-api +pnpm run check:meili +``` + +### Restart Services +```bash +# Restart Meilisearch +docker compose -f infra/meilisearch/docker-compose.yml restart + +# Restart AI API (Ctrl+C and run again) +cd apps/ai-api +pnpm run dev + +# Restart Frontend (Ctrl+C and run again) +pnpm run www +``` + +### Re-initialize Data +```bash +cd apps/ai-api + +# Re-run all setup steps +pnpm run setup:embedder +pnpm run setup:ingest +pnpm run setup:synonyms +pnpm run check:meili +``` + +--- + +## Troubleshooting + +### "Connection refused" to Meilisearch +```bash +# Check if running +docker ps | grep meilisearch + +# If not running, start it +docker compose -f infra/meilisearch/docker-compose.yml up -d + +# Check logs +docker compose -f infra/meilisearch/docker-compose.yml logs +``` + +### "Invalid API key" +```bash +# Verify keys in .ai-env +cat .ai-env | grep MEILISEARCH + +# If wrong, recreate keys +export MEILI_MASTER_KEY=your-master-key +pnpm -w -C apps/ai-api run setup:create-keys + +# Update .ai-env with new keys +``` + +### No search results +```bash +# Check if documents are indexed +cd apps/ai-api +pnpm run check:meili + +# If 0 documents, re-run ingestion +pnpm run setup:ingest +pnpm run check:meili +``` + +### Frontend can't connect to API +```bash +# Verify AI API is running +curl http://localhost:3001/health + +# Check if port 3001 is in use +lsof -i :3001 + +# Check browser console for CORS errors +``` + +### Azure OpenAI errors +```bash +# Verify Azure credentials +cat .ai-env | grep AZURE + +# Test Azure connection +cd apps/ai-api +pnpm run test:smoke +``` + +--- + +## Port Reference + +| Service | Port | URL | +|---------|------|-----| +| Meilisearch | 7700 | http://localhost:7700 | +| AI API | 3001 | http://localhost:3001 | +| WWW Frontend | 5173 | http://localhost:5173 | + +--- + +## File Locations + +| File | Purpose | +|------|---------| +| `.ai-env` | AI API environment variables | +| `infra/meilisearch/.env` | Meilisearch Docker environment | +| `apps/ai-api/src/server.ts` | API server code | +| `internal/components/src/search-dialog/` | Search UI component | +| `apps/ai-api/scripts/` | Setup and utility scripts | + +--- + +## Test Queries + +Try these to verify everything works: + +| Query | Expected Result | +|-------|----------------| +| `button` | Quick results for Button component | +| `modal` | Quick results for Dialog (via synonym) | +| `how to use button` | Quick results + AI answer | +| `design tokens` | Results about design tokens | +| `dropdown` | Quick results for Select (via synonym) | + +--- + +## Stop All Services + +```bash +# Stop frontend (Ctrl+C in Terminal 3) +# Stop AI API (Ctrl+C in Terminal 2) + +# Stop Meilisearch +docker compose -f infra/meilisearch/docker-compose.yml down +``` + +--- + +## Daily Workflow (After Initial Setup) + +```bash +# Start Meilisearch (Terminal 1) +docker compose -f infra/meilisearch/docker-compose.yml up -d + +# Start AI API (Terminal 2) +cd apps/ai-api && pnpm run dev + +# Start Frontend (Terminal 3) +pnpm run www + +# Open browser +open http://localhost:5173 +``` + +--- + +## Environment Variables Quick Reference + +### Required in `.ai-env` +```bash +MEILISEARCH_API_URL=http://localhost:7700 +MEILISEARCH_ADMIN_KEY= +MEILISEARCH_SEARCH_KEY= +MEILISEARCH_PROJECT_NAME=designsystemet-search +MEILISEARCH_EMBEDDER_UID=azure-openai-small +AZURE_KEY= +AZURE_ENDPOINT= +AZURE_API_VERSION=2024-08-01-preview +AZURE_EMBEDDING_DEPLOY_SMALL=text-embedding-3-small +AZURE_GPT_DEPLOY=gpt-4o +``` + +### Required in `infra/meilisearch/.env` +```bash +MEILI_MASTER_KEY= +MEILI_ENV=production +MEILI_NO_ANALYTICS=true +MEILI_EXPERIMENTAL_ENABLE_VECTOR_SEARCH=enabled +MEILI_EXPERIMENTAL_ENABLE_EMBEDDERS=enabled +``` + +--- + +## Help & Support + +For detailed information, see: +- `AI_SEARCH_SETUP_GUIDE.md` - Full setup guide +- `AI_SEARCH_CHECKLIST.md` - Step-by-step checklist +- `AI_SEARCH_ARCHITECTURE.md` - System architecture +- `infra/meilisearch/README.md` - Meilisearch details + +--- + +**You're ready to go! 🚀** diff --git a/apps/ai-api/AI_SEARCH_SETUP_COMPLETE.md b/apps/ai-api/AI_SEARCH_SETUP_COMPLETE.md new file mode 100644 index 0000000000..d4e72e7dc1 --- /dev/null +++ b/apps/ai-api/AI_SEARCH_SETUP_COMPLETE.md @@ -0,0 +1,236 @@ +# AI Search Setup - Complete ✅ + +**Date**: October 2, 2025 +**Status**: Fully operational and tested + +--- + +## System Overview + +The AI-powered search system for Designsystemet is now fully functional with: + +- **621 documents indexed** with vector embeddings +- **32 synonym groups** (100 total synonyms) for improved search +- **Hybrid search**: Vector similarity + keyword matching +- **Two search modes**: Quick (fast results) + Chat (AI-powered answers) + +--- + +## Services Running + +| Service | URL | Status | Details | +|---------|-----|--------|---------| +| Meilisearch | http://localhost:7700 | 🟢 Running | 621 documents, vector search enabled | +| AI API | http://localhost:3001 | 🟢 Running | Health check passed, search tested | +| WWW Frontend | http://localhost:5174 | 🟢 Running | Search UI integrated in header | + +--- + +## What Was Fixed + +### Critical Fix: Vector Field Format + +**Problem**: Documents were using `vector` field, but Meilisearch's `userProvided` embedder expects `_vectors` with embedder name. + +**Solution**: Updated `apps/ai-api/scripts/ingest.mjs` to use: +```javascript +_vectors: { + [process.env.MEILISEARCH_EMBEDDER_UID || 'azure-openai-small']: vectors[index] +} +``` + +### Other Fixes + +1. **check-meili.mjs**: Fixed document count display (was showing `undefined`) +2. **Vector store feature**: Automatically enabled in fresh Meilisearch instances +3. **Environment loading**: Scripts correctly load from `apps/.ai-env` + +--- + +## Setup Steps (For Reference) + +1. ✅ Start Meilisearch: `docker compose -f infra/meilisearch/docker-compose.yml up -d` +2. ✅ Create API keys: `MEILI_MASTER_KEY=xxx pnpm run setup:create-keys` +3. ✅ Update `.ai-env` with keys +4. ✅ Configure embedder: `pnpm run setup:embedder` +5. ✅ Ingest documents: `pnpm run setup:ingest` (5-10 min) +6. ✅ Configure synonyms: `pnpm run setup:synonyms` +7. ✅ Verify: `pnpm run check:meili` +8. ✅ Start AI API: `pnpm run dev` +9. ✅ Start frontend: `pnpm run www` (from repo root) + +--- + +## Testing Results + +### Search API Tests + +**Test 1: Direct component search** +```bash +curl -X POST http://localhost:3001/api/search \ + -H "Content-Type: application/json" \ + -d '{"query": "button"}' +``` +✅ Returns Button component documentation + +**Test 2: Synonym search (modal → dialog)** +```bash +curl -X POST http://localhost:3001/api/search \ + -H "Content-Type: application/json" \ + -d '{"query": "modal"}' +``` +✅ Returns Dialog component documentation + +**Test 3: Synonym search (dropdown → select)** +```bash +curl -X POST http://localhost:3001/api/search \ + -H "Content-Type: application/json" \ + -d '{"query": "dropdown"}' +``` +✅ Returns Select component documentation + +### Health Check +```bash +curl http://localhost:3001/health +``` +✅ Returns: `{"status":"ok","timestamp":"...","meilisearch":"ok"}` + +--- + +## Key Files Modified + +### Core Changes +- `apps/ai-api/scripts/ingest.mjs` - Fixed vector field format +- `apps/ai-api/scripts/setup-embedder.mjs` - Cleaned up comments +- `apps/ai-api/scripts/check-meili.mjs` - Fixed document count display + +### Documentation +- `AI_SEARCH_QUICK_START.md` - Updated with individual commands +- `AI_SEARCH_CHECKLIST.md` - Simplified setup steps +- `apps/ai-api/README.md` - Complete command reference + +--- + +## Synonym Examples + +The system understands common variations: + +| User searches for | Finds component | +|-------------------|-----------------| +| "modal" | Dialog | +| "dropdown" | Select | +| "popup" | Popover | +| "textfield" | Input | +| "accordion" | Details | +| "cta" | Button | +| "autocomplete" | Suggestion | +| "toggle" | Switch | + +--- + +## Next Steps for Frontend Developer + +### Daily Development +1. Start services (if not running): + ```bash + # Terminal 1 + docker compose -f infra/meilisearch/docker-compose.yml up -d + + # Terminal 2 + cd apps/ai-api && pnpm run dev + + # Terminal 3 + pnpm run www + ``` + +2. Test search at http://localhost:5174 +3. Click search icon (🔍) in header +4. Try searches: "button", "modal", "dropdown", etc. + +### If Something Breaks + +**No search results?** +```bash +cd apps/ai-api +pnpm run check:meili # Check document count +``` + +**Meilisearch not responding?** +```bash +docker ps | grep meilisearch # Check if running +docker compose -f infra/meilisearch/docker-compose.yml restart +``` + +**Need to re-ingest?** +```bash +cd apps/ai-api +pnpm run setup:ingest +pnpm run check:meili +``` + +--- + +## Performance Notes + +- **Ingestion time**: ~5-10 minutes for 621 documents +- **Search latency**: <100ms for Quick mode +- **Vector dimensions**: 1536 (text-embedding-3-small) +- **Cache TTL**: 30 minutes for AI responses + +--- + +## Architecture Summary + +``` +┌─────────────┐ +│ Browser │ +│ localhost: │ +│ 5174 │ +└──────┬──────┘ + │ + ▼ +┌─────────────┐ +│ AI API │ +│ localhost: │ +│ 3001 │ +└──────┬──────┘ + │ + ├──────────────┐ + │ │ + ▼ ▼ +┌─────────────┐ ┌──────────────┐ +│ Meilisearch │ │ Azure OpenAI │ +│ localhost: │ │ (GPT-4o) │ +│ 7700 │ │ │ +│ │ │ Embeddings: │ +│ 621 docs │ │ text-embed- │ +│ w/ vectors │ │ 3-small │ +└─────────────┘ └──────────────┘ +``` + +--- + +## Success Metrics + +- ✅ 621 documents indexed successfully +- ✅ Vector search working with proper embeddings +- ✅ Synonym matching functional (100 synonyms) +- ✅ Health checks passing +- ✅ Search API responding correctly +- ✅ Frontend integrated and accessible +- ✅ All services running stably + +--- + +## Contact & Support + +For issues or questions: +1. Check `AI_SEARCH_QUICK_START.md` for common problems +2. Check `AI_SEARCH_CHECKLIST.md` for step-by-step verification +3. Check `apps/ai-api/README.md` for command reference + +--- + +**Setup completed successfully! 🎉** + +The AI search system is ready for use and testing. diff --git a/apps/ai-api/README.md b/apps/ai-api/README.md new file mode 100644 index 0000000000..33a9b1a2d5 --- /dev/null +++ b/apps/ai-api/README.md @@ -0,0 +1,167 @@ +# AI API - Designsystemet Search Backend + +Backend API for AI-powered search in Designsystemet documentation. + +## Quick Start + +```bash +# 1. Make sure Meilisearch is running +docker compose -f ../../infra/meilisearch/docker-compose.yml up -d + +# 2. Configure environment (see root .ai-env.example) +# Make sure ".ai-env" is in apps/ai-api with all required values + +# 3. Run complete setup (first time, when things break, big changes, etc.) +pnpm run setup + +# 4. Start the API server +# Server will be available at `http://localhost:3001` +pnpm run dev +``` + +## Available Commands + +### Main Commands +- `pnpm run setup` - **Run this first!** Complete setup: embedder + ingest + synonyms + verify +- `pnpm run dev` - Start development server with hot reload +- `pnpm run build` - Build for production +- `pnpm run start` - Start production server + +### Setup Commands (individual steps) +- `pnpm run setup:embedder` - Configure Azure OpenAI embedder +- `pnpm run setup:ingest` - Ingest 768+ documents (5-10 min) +- `pnpm run setup:synonyms` - Configure 32 synonym groups +- `pnpm run setup:create-keys` - Create Meilisearch Admin/Search keys + +### Testing & Verification +- `pnpm run check:meili` - Verify Meilisearch status and document count +- `pnpm run test:smoke` - Test Azure OpenAI connection +- `pnpm run test:rag` - Test RAG endpoint + +## API Endpoints + +### `GET /health` +Health check endpoint. + +**Response:** +```json +{ + "status": "ok", + "timestamp": "2025-10-02T10:00:00.000Z", + "meilisearch": "ok" +} +``` + +### `POST /api/search` +Fast keyword + vector search (Quick mode). + +**Request:** +```json +{ + "query": "button" +} +``` + +**Response:** +```json +{ + "results": [ + { + "title": "Button", + "content": "Button component for user actions...", + "url": "/components/button", + "type": "component" + } + ] +} +``` + +### `POST /api/ai-search` +AI-powered conversational search (Smart mode). + +**Request:** +```json +{ + "query": "how do I use the button component" +} +``` + +**Response:** +```json +{ + "answer": "The Button component is used for...", + "sources": [ + { + "title": "Button Component", + "url": "/components/button" + } + ] +} +``` + +## Environment Variables + +Required in `.ai-env` (repo root): + +```bash +# Meilisearch +MEILISEARCH_API_URL=http://localhost:7700 +MEILISEARCH_ADMIN_KEY= +MEILISEARCH_SEARCH_KEY= +MEILISEARCH_PROJECT_NAME=designsystemet-search +MEILISEARCH_EMBEDDER_UID=azure-openai-small + +# Azure OpenAI +AZURE_KEY= +AZURE_ENDPOINT= +AZURE_API_VERSION=2024-08-01-preview +AZURE_EMBEDDING_DEPLOY_SMALL=text-embedding-3-small +AZURE_GPT_DEPLOY=gpt-4o +``` + +## Troubleshooting + +### "Connection refused" to Meilisearch +```bash +# Check if Meilisearch is running +docker ps | grep meilisearch + +# Start if not running +docker compose -f ../../infra/meilisearch/docker-compose.yml up -d +``` + +### No search results / Something not working +```bash +# Re-run complete setup (fixes most issues) +pnpm run setup +``` + +### "Invalid API key" +```bash +# Recreate keys +export MEILI_MASTER_KEY=your-master-key +pnpm run setup:create-keys + +# Update .ai-env with new keys +``` + +## Architecture + +- **Express** server with TypeScript +- **Meilisearch** for vector + keyword search +- **Azure OpenAI** for embeddings (text-embedding-3-small) and chat (GPT-4o) +- **NodeCache** for response caching (30min TTL) +- **CORS** enabled for localhost:5173 and production + +## Documentation + +See repo root for complete guides: +- `AI_SEARCH_QUICK_START.md` - 10-minute setup guide +- `AI_SEARCH_CHECKLIST.md` - Step-by-step checklist + +## Notes + +- **Always use the same embedding model** for ingestion and search (text-embedding-3-small) +- Response caching reduces Azure OpenAI costs +- Document deduplication prevents "part 1/2" clutter +- 32 synonym groups improve search accuracy (e.g., "modal" → Dialog) diff --git a/apps/ai-api/package-lock.json b/apps/ai-api/package-lock.json new file mode 100644 index 0000000000..bfd53732bb --- /dev/null +++ b/apps/ai-api/package-lock.json @@ -0,0 +1,2483 @@ +{ + "name": "@apps/ai-api", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@apps/ai-api", + "version": "0.1.0", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^17.2.1", + "express": "^5.1.0", + "ioredis": "^5.6.1", + "meilisearch": "^0.51.0", + "node-cache": "^5.1.2", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/node": "^24.1.0", + "@types/node-cache": "^4.1.3", + "ts-node-dev": "^2.0.0", + "tsx": "^4.20.3", + "typescript": "^5.8.3" + } + }, + "../../node_modules/.pnpm/ioredis@5.6.1/node_modules/ioredis": { + "version": "5.6.1", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "devDependencies": { + "@ioredis/interface-generator": "^1.3.0", + "@semantic-release/changelog": "^6.0.1", + "@semantic-release/commit-analyzer": "^9.0.2", + "@semantic-release/git": "^10.0.1", + "@types/chai": "^4.3.0", + "@types/chai-as-promised": "^7.1.5", + "@types/debug": "^4.1.5", + "@types/lodash.defaults": "^4.2.7", + "@types/lodash.isarguments": "^3.1.7", + "@types/mocha": "^9.1.0", + "@types/node": "^14.18.12", + "@types/redis-errors": "^1.2.1", + "@types/sinon": "^10.0.11", + "@typescript-eslint/eslint-plugin": "^5.48.1", + "@typescript-eslint/parser": "^5.48.1", + "chai": "^4.3.6", + "chai-as-promised": "^7.1.1", + "eslint": "^8.31.0", + "eslint-config-prettier": "^8.6.0", + "mocha": "^9.2.1", + "nyc": "^15.1.0", + "prettier": "^2.6.1", + "semantic-release": "^19.0.2", + "server-destroy": "^1.0.1", + "sinon": "^13.0.1", + "ts-node": "^10.4.0", + "tsd": "^0.19.1", + "typedoc": "^0.22.18", + "typescript": "^4.6.3", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript": { + "version": "5.8.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "devDependencies": { + "@dprint/formatter": "^0.4.1", + "@dprint/typescript": "0.93.3", + "@esfx/canceltoken": "^1.0.0", + "@eslint/js": "^9.17.0", + "@octokit/rest": "^21.0.2", + "@types/chai": "^4.3.20", + "@types/diff": "^5.2.3", + "@types/minimist": "^1.2.5", + "@types/mocha": "^10.0.10", + "@types/ms": "^0.7.34", + "@types/node": "latest", + "@types/source-map-support": "^0.5.10", + "@types/which": "^3.0.4", + "@typescript-eslint/rule-tester": "^8.18.1", + "@typescript-eslint/type-utils": "^8.18.1", + "@typescript-eslint/utils": "^8.18.1", + "azure-devops-node-api": "^14.1.0", + "c8": "^10.1.3", + "chai": "^4.5.0", + "chalk": "^4.1.2", + "chokidar": "^3.6.0", + "diff": "^5.2.0", + "dprint": "^0.47.6", + "esbuild": "^0.24.0", + "eslint": "^9.17.0", + "eslint-formatter-autolinkable-stylish": "^1.4.0", + "eslint-plugin-regexp": "^2.7.0", + "fast-xml-parser": "^4.5.1", + "glob": "^10.4.5", + "globals": "^15.13.0", + "hereby": "^1.10.0", + "jsonc-parser": "^3.3.1", + "knip": "^5.41.0", + "minimist": "^1.2.8", + "mocha": "^10.8.2", + "mocha-fivemat-progress-reporter": "^0.1.0", + "monocart-coverage-reports": "^2.11.4", + "ms": "^2.1.3", + "playwright": "^1.49.1", + "source-map-support": "^0.5.21", + "tslib": "^2.8.1", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.1", + "which": "^3.0.1" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", + "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/node-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@types/node-cache/-/node-cache-4.1.3.tgz", + "integrity": "sha512-3hsqnv3H1zkOhjygJaJUYmgz5+FcPO3vejBX7cE9/cnuINOJYrzkfOnUCvpwGe9kMZANIHJA7J5pOdeyv52OEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ioredis": { + "resolved": "../../node_modules/.pnpm/ioredis@5.6.1/node_modules/ioredis", + "link": true + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/meilisearch": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.51.0.tgz", + "integrity": "sha512-IuNsYyT8r/QLhU33XDZdXWUT6cA/nACCHHZc+NHkNuaU55LELRxff1uBJhpJcwjYaAPEEmeWrzBhYl2XlhJdAg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "license": "MIT", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tsx": { + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", + "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "resolved": "../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript", + "link": true + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/apps/ai-api/package.json b/apps/ai-api/package.json new file mode 100644 index 0000000000..6650a8b018 --- /dev/null +++ b/apps/ai-api/package.json @@ -0,0 +1,37 @@ +{ + "name": "@apps/ai-api", + "private": true, + "type": "module", + "version": "0.1.0", + "scripts": { + "dev": "tsx watch src/server.ts", + "build": "tsc", + "start": "node dist/server.js", + "setup": "node scripts/setup.mjs", + "test:smoke": "node scripts/ai-smoke-test.mjs", + "test:rag": "node scripts/test-rag-endpoint.mjs", + "setup:ingest": "node scripts/ingest.mjs", + "setup:embedder": "node scripts/setup-embedder.mjs", + "setup:synonyms": "node scripts/setup-synonyms.mjs", + "setup:create-keys": "node scripts/create-meili-keys.mjs", + "check:meili": "node scripts/check-meili.mjs" + }, + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^17.2.1", + "express": "^5.1.0", + "ioredis": "^5.6.1", + "meilisearch": "^0.51.0", + "node-cache": "^5.1.2", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/node": "^24.1.0", + "@types/node-cache": "^4.1.3", + "ts-node-dev": "^2.0.0", + "tsx": "^4.20.3", + "typescript": "^5.8.3" + } +} diff --git a/apps/ai-api/scripts/ai-smoke-test.mjs b/apps/ai-api/scripts/ai-smoke-test.mjs new file mode 100644 index 0000000000..2cd568c3f0 --- /dev/null +++ b/apps/ai-api/scripts/ai-smoke-test.mjs @@ -0,0 +1,122 @@ +#!/usr/bin/env node + +// Quick smoke test for AI infra: Azure embeddings, Azure chat, and Meilisearch. +// Usage: node scripts/ai-smoke-test.mjs (ensure `.ai-env` is loaded in your shell) + +import fs from 'node:fs'; +import path from 'node:path'; +import { Meilisearch } from 'meilisearch'; + +// TODO: Put env file in /ai-api/.ai-env +const envPathCandidates = [ + path.resolve(process.cwd(), '../.ai-env'), + path.resolve(process.cwd(), 'apps/.ai-env'), + path.resolve(process.cwd(), '.ai-env'), +]; + +for (const p of envPathCandidates) { + if (fs.existsSync(p)) { + const lines = fs + .readFileSync(p, 'utf8') + .split(/\r?\n/) + .filter((l) => l.trim() && !l.startsWith('#')); + for (const line of lines) { + const [k, ...rest] = line.split('='); + if (!(k in process.env)) process.env[k] = rest.join('='); + } + console.log(`Loaded env vars from ${p}`); + break; + } +} + +const env = process.env; + +function assertEnv(name) { + if (!env[name]) throw new Error(`Environment variable ${name} is required.`); +} + +try { + [ + 'AZURE_KEY', + 'AZURE_ENDPOINT', + 'AZURE_API_VERSION', + 'AZURE_EMBEDDING_DEPLOY_SMALL', + 'AZURE_GPT_DEPLOY', + 'MEILISEARCH_ADMIN_KEY', + 'MEILISEARCH_API_URL', + ].forEach(assertEnv); +} catch (e) { + console.error( + `❌ ${e.message}\nLoad env vars with: export $(grep -v '^#' apps/.ai-env | xargs)`, + ); + process.exit(1); +} + +async function testEmbedding() { + const url = `${env.AZURE_ENDPOINT}/openai/deployments/${env.AZURE_EMBEDDING_DEPLOY_SMALL}/embeddings?api-version=${env.AZURE_API_VERSION}`; + const resp = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'api-key': env.AZURE_KEY, + }, + body: JSON.stringify({ + input: 'What is the answer to life, the universe and everything?', + }), + }); + if (!resp.ok) + throw new Error(`Embedding failed: ${resp.status} ${resp.statusText}`); + console.log('✔ Azure embedding endpoint reachable'); +} + +async function testChat() { + const url = `${env.AZURE_ENDPOINT}/openai/deployments/${env.AZURE_GPT_DEPLOY}/chat/completions?api-version=${env.AZURE_API_VERSION}`; + const resp = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'api-key': env.AZURE_KEY, + }, + body: JSON.stringify({ + messages: [ + { + role: 'user', + content: + 'What is the answer to life, the universe and everything? in short.', + }, + ], + max_tokens: 3, + temperature: 0, + }), + }); + if (!resp.ok) + throw new Error(`Chat failed: ${resp.status} ${resp.statusText}`); + const data = await resp.json(); + console.log( + '✔ Azure chat endpoint reachable - response:', + data.choices?.[0]?.message?.content, + ); +} + +async function testMeili() { + const client = new Meilisearch({ + host: env.MEILISEARCH_API_URL, + apiKey: env.MEILISEARCH_ADMIN_KEY, + }); + const indexes = await client.getIndexes(); + console.log( + `✔ Meilisearch reachable - ${Array.isArray(indexes) ? indexes.length : Object.keys(indexes).length} indexes found`, + ); +} + +(async () => { + try { + await testEmbedding(); + await testChat(); + await testMeili(); + console.log('\n🎉 All smoke tests passed'); + } catch (err) { + console.error('Smoke test error →', err.message); + process.exit(1); + } +})(); diff --git a/apps/ai-api/scripts/check-embedder-status.mjs b/apps/ai-api/scripts/check-embedder-status.mjs new file mode 100644 index 0000000000..4ddfe13d3c --- /dev/null +++ b/apps/ai-api/scripts/check-embedder-status.mjs @@ -0,0 +1,136 @@ +#!/usr/bin/env node +// Check current embedder configuration in Meilisearch + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Meilisearch } from 'meilisearch'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// Load environment variables +const envPathCandidates = [ + path.resolve(__dirname, '../../.ai-env'), + path.resolve(__dirname, '../../../apps/.ai-env'), + path.resolve(__dirname, '../../../.ai-env'), +]; + +for (const p of envPathCandidates) { + if (fs.existsSync(p)) { + const lines = fs + .readFileSync(p, 'utf8') + .split(/\r?\n/) + .filter((l) => l.trim() && !l.startsWith('#')); + for (const line of lines) { + const [k, ...rest] = line.split('='); + if (!(k in process.env)) process.env[k] = rest.join('='); + } + console.log(`Loaded env vars from ${p}`); + break; + } +} + +const env = process.env; +const INDEX_NAME = env.MEILISEARCH_PROJECT_NAME || 'designsystemet-search'; + +// Initialize client +const meiliClient = new Meilisearch({ + host: env.MEILISEARCH_API_URL, + apiKey: env.MEILISEARCH_ADMIN_KEY, +}); + +async function checkEmbedderStatus() { + try { + const index = meiliClient.index(INDEX_NAME); + + console.log('🔍 Checking embedder configuration...'); + + // Get current index settings + const settings = await index.getSettings(); + console.log('📋 Current index settings:'); + console.log(' Embedders:', settings.embedders || 'None configured'); + console.log( + ' Filterable attributes:', + settings.filterableAttributes || 'None configured', + ); + console.log( + ' Searchable attributes:', + settings.searchableAttributes || 'All attributes', + ); + console.log(''); + + // Check what embedder UID the server expects + const expectedEmbedderUID = + env.MEILISEARCH_EMBEDDER_UID || 'azure-openai-small'; + console.log( + `🎯 Expected embedder UID is set (value not shown for security).`, + ); + + // Test if embedder exists by trying to use it + try { + console.log('🧪 Testing vector search with embedder...'); + const testQuery = 'Button component'; + + // Generate test embedding + const testEmbedding = await generateTestEmbedding(testQuery); + + // Try hybrid search + const hybridResult = await index.search(testQuery, { + limit: 1, + vector: testEmbedding, + hybrid: { + semanticRatio: 0.7, + embedder: expectedEmbedderUID, + }, + }); + + console.log('✅ Hybrid search with embedder works!'); + console.log(` Found ${hybridResult.hits.length} results`); + } catch (embedderError) { + console.log( + '❌ Hybrid search with embedder failed:', + embedderError.message, + ); + + // Try vector-only search + try { + console.log('🧪 Testing vector-only search...'); + const testEmbedding = await generateTestEmbedding('Button component'); + + const vectorResult = await index.search('Button component', { + limit: 1, + vector: testEmbedding, + }); + + console.log('✅ Vector-only search works!'); + console.log(` Found ${vectorResult.hits.length} results`); + } catch (vectorError) { + console.log('❌ Vector-only search also failed:', vectorError.message); + } + } + } catch (error) { + console.error('❌ Failed to check embedder status:', error); + } +} + +async function generateTestEmbedding(text) { + const url = `${env.AZURE_ENDPOINT}/openai/deployments/${env.AZURE_EMBEDDING_DEPLOY_SMALL}/embeddings?api-version=${env.AZURE_API_VERSION}`; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'api-key': env.AZURE_KEY, + }, + body: JSON.stringify({ input: text }), + }); + + if (!response.ok) { + throw new Error(`Embedding failed: ${response.status}`); + } + + const data = await response.json(); + return data.data[0].embedding; +} + +checkEmbedderStatus(); diff --git a/apps/ai-api/scripts/check-meili.mjs b/apps/ai-api/scripts/check-meili.mjs new file mode 100644 index 0000000000..ce3260489d --- /dev/null +++ b/apps/ai-api/scripts/check-meili.mjs @@ -0,0 +1,93 @@ +// Quick health check for self-hosted Meilisearch +// Usage: node scripts/check-meili.mjs +// Loads .ai-env from common locations to read MEILISEARCH_API_URL and MEILISEARCH_PROJECT_NAME + +import fs from 'node:fs'; +import path from 'node:path'; +import url from 'node:url'; +import dotenv from 'dotenv'; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + +const candidateEnvPaths = [ + path.resolve(__dirname, '../../.ai-env'), + path.resolve(__dirname, '../../../apps/.ai-env'), + path.resolve(__dirname, '../../../.ai-env'), +]; +for (const p of candidateEnvPaths) { + if (fs.existsSync(p)) { + dotenv.config({ path: p }); + break; + } +} + +const API_URL = process.env.MEILISEARCH_API_URL || 'http://localhost:7700'; +const INDEX_NAME = + process.env.MEILISEARCH_PROJECT_NAME || 'designsystemet-search'; +const ADMIN_KEY = + process.env.MEILISEARCH_ADMIN_KEY || process.env.MEILI_MASTER_KEY || ''; + +async function check() { + const healthRes = await fetch(`${API_URL}/health`); + const health = healthRes.ok + ? await healthRes.json() + : { error: `${healthRes.status} ${healthRes.statusText}` }; + console.log(`Health:`, health); + + const headers = ADMIN_KEY ? { Authorization: `Bearer ${ADMIN_KEY}` } : {}; + const idxRes = await fetch(`${API_URL}/indexes`, { headers }); + if (!idxRes.ok) { + const text = await idxRes.text(); + console.error( + `Indexes error: ${idxRes.status} ${idxRes.statusText} - ${text}`, + ); + return; + } + const indexData = await idxRes.json(); + const indexes = indexData.results || []; + console.log( + `Indexes (${indexes.length}):`, + indexes.map((i) => i.uid).join(', ') || '(none)', + ); + + // Get detailed stats for our index + const statsRes = await fetch(`${API_URL}/indexes/${INDEX_NAME}/stats`, { + headers, + }); + if (statsRes.ok) { + const stats = await statsRes.json(); + console.log( + `\n📊 Index '${INDEX_NAME}': ${stats.numberOfDocuments} documents`, + ); + } + + // Optional: show embedder settings if available + const embRes = await fetch( + `${API_URL}/indexes/${INDEX_NAME}/settings/embedders`, + { headers }, + ); + if (embRes.ok) { + const emb = await embRes.json(); + console.log(`Embedders for '${INDEX_NAME}':`, JSON.stringify(emb, null, 2)); + } else { + console.log( + `Embedders endpoint unavailable or not configured for '${INDEX_NAME}'`, + ); + } + + // Version (auth protected in v1.x) + const verRes = await fetch(`${API_URL}/version`, { headers }); + if (verRes.ok) { + const ver = await verRes.json(); + console.log(`Version:`, ver); + } else { + console.log( + `Version endpoint not available without auth (${verRes.status})`, + ); + } +} + +check().catch((err) => { + console.error('❌ Check failed:', err.message); + process.exit(1); +}); diff --git a/apps/ai-api/scripts/create-meili-keys.mjs b/apps/ai-api/scripts/create-meili-keys.mjs new file mode 100644 index 0000000000..8ba0c5d761 --- /dev/null +++ b/apps/ai-api/scripts/create-meili-keys.mjs @@ -0,0 +1,96 @@ +// Create Meilisearch Admin and Search keys using the master key +// Usage: node scripts/create-meili-keys.mjs +// Env required: +// MEILISEARCH_API_URL (e.g., http://localhost:7700) +// MEILI_MASTER_KEY (from your Meili server) +// MEILISEARCH_PROJECT_NAME (optional, defaults to 'designsystemet-search') + +import fs from 'node:fs'; +import path from 'node:path'; +import url from 'node:url'; +import dotenv from 'dotenv'; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + +// Load .ai-env from common locations (optional) +const candidateEnvPaths = [ + path.resolve(__dirname, '../../.ai-env'), + path.resolve(__dirname, '../../../apps/.ai-env'), + path.resolve(__dirname, '../../../.ai-env'), +]; +for (const p of candidateEnvPaths) { + if (fs.existsSync(p)) { + dotenv.config({ path: p }); + break; + } +} + +const API_URL = process.env.MEILISEARCH_API_URL; +const MASTER_KEY = process.env.MEILI_MASTER_KEY; +const INDEX_NAME = + process.env.MEILISEARCH_PROJECT_NAME || 'designsystemet-search'; + +function assertEnv(name) { + if (!process.env[name]) { + throw new Error(`Missing required env: ${name}`); + } +} + +try { + assertEnv('MEILISEARCH_API_URL'); + assertEnv('MEILI_MASTER_KEY'); +} catch (e) { + console.error(`❌ ${e.message}`); + console.error( + 'Set MEILI_MASTER_KEY in your environment (not in .ai-env), e.g.:', + ); + console.error(' export MEILI_MASTER_KEY=...'); + console.error( + 'And ensure MEILISEARCH_API_URL is set in .ai-env or your shell.', + ); + process.exit(1); +} + +async function createKey(description, actions, indexes) { + const res = await fetch(`${API_URL}/keys`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${MASTER_KEY}`, + }, + body: JSON.stringify({ description, actions, indexes, expiresAt: null }), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error( + `Failed to create key '${description}': ${res.status} ${res.statusText} - ${text}`, + ); + } + return res.json(); +} + +(async () => { + try { + console.log(`Creating keys on ${API_URL} for index '${INDEX_NAME}'...`); + + const admin = await createKey('Admin key', ['*'], ['*']); + const search = await createKey('Search key', ['search'], [INDEX_NAME]); + + console.log('\n✅ Keys created successfully'); + console.log('---'); + console.log(`Admin Key: ${admin.key}`); + console.log(`Search Key: ${search.key}`); + console.log('---'); + console.log('Next steps:'); + console.log('1) Put Admin key into .ai-env as MEILISEARCH_ADMIN_KEY'); + console.log('2) Put Search key into .ai-env as MEILISEARCH_SEARCH_KEY'); + console.log( + '3) Run: pnpm -w -C apps/ai-api run setup:embedder (if you use Meili embedders)', + ); + console.log('4) Run: pnpm -w -C apps/ai-api run setup:ingest'); + console.log('5) Run: pnpm -w -C apps/ai-api run setup:synonyms'); + } catch (err) { + console.error(`❌ ${err.message}`); + process.exit(1); + } +})(); diff --git a/apps/ai-api/scripts/debug-ingestion.mjs b/apps/ai-api/scripts/debug-ingestion.mjs new file mode 100644 index 0000000000..dcd36b7c19 --- /dev/null +++ b/apps/ai-api/scripts/debug-ingestion.mjs @@ -0,0 +1,289 @@ +#!/usr/bin/env node + +// Debug ingestion to see what document structure is actually being created + +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// Load environment variables +const envPathCandidates = [ + path.resolve(__dirname, '../../.ai-env'), + path.resolve(__dirname, '../../../apps/.ai-env'), + path.resolve(__dirname, '../../../.ai-env'), +]; + +for (const p of envPathCandidates) { + if (fs.existsSync(p)) { + const lines = fs + .readFileSync(p, 'utf8') + .split(/\r?\n/) + .filter((l) => l.trim() && !l.startsWith('#')); + for (const line of lines) { + const [k, ...rest] = line.split('='); + if (!(k in process.env)) process.env[k] = rest.join('='); + } + console.log(`Loaded env vars from ${p}`); + break; + } +} + +// const env = process.env; +const REPO_ROOT = path.resolve(__dirname, '../../..'); +// const INDEX_NAME = env.MEILISEARCH_PROJECT_NAME || 'designsystemet-search'; + +// Copy relevant functions from ingest.mjs to test locally +function extractLanguageFromPath(filePath) { + if (filePath.includes('/en/')) return 'en'; + if (filePath.includes('/no/')) return 'no'; + return 'en'; // default to English +} + +function extractTextFromFile(filePath, content) { + const ext = path.extname(filePath).toLowerCase(); + const lang = extractLanguageFromPath(filePath); + + if (ext === '.md' || ext === '.mdx') { + // Extract frontmatter and content + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); + if (frontmatterMatch) { + const frontmatter = frontmatterMatch[1]; + const body = frontmatterMatch[2]; + + // Extract title from frontmatter + const titleMatch = frontmatter.match(/title:\s*["']?([^"'\n]+)["']?/); + const title = titleMatch ? titleMatch[1] : path.basename(filePath, ext); + + return { title, content: body.trim(), lang }; + } + + // No frontmatter, use first heading as title + const headingMatch = content.match(/^#\s+(.+)$/m); + const title = headingMatch ? headingMatch[1] : path.basename(filePath, ext); + + return { title, content: content.trim(), lang }; + } + + // Default fallback + return { title: path.basename(filePath, ext), content: content.trim(), lang }; +} + +function generateUrl(filePath) { + const relativePath = path.relative(REPO_ROOT, filePath); + + // Component documentation -> Storybook + if (relativePath.includes('packages/react/src/components/')) { + const componentName = relativePath.split('/').slice(-2, -1)[0]; + return `https://storybook.designsystemet.no/?path=/docs/komponenter-${componentName.toLowerCase()}--docs`; + } + + // Utilities -> Storybook + if (relativePath.includes('packages/react/src/utilities/')) { + const utilityName = relativePath.split('/').slice(-2, -1)[0]; + return `https://storybook.designsystemet.no/?path=/docs/utilities-${utilityName.toLowerCase()}--docs`; + } + + // Website content -> designsystemet.no + if (relativePath.includes('apps/www/app/content/')) { + const contentPath = relativePath.replace('apps/www/app/content/', ''); + return `https://designsystemet.no/content/${contentPath}`; + } + + // Fallback + return `https://designsystemet.no/${relativePath}`; +} + +function chunkText(text, maxTokens = 300) { + const sentences = text.split(/[.!?]+/).filter((s) => s.trim()); + const chunks = []; + let currentChunk = ''; + + for (const sentence of sentences) { + const potentialChunk = currentChunk + sentence + '.'; + if (potentialChunk.length > maxTokens * 4 && currentChunk) { + chunks.push(currentChunk.trim()); + currentChunk = sentence + '.'; + } else { + currentChunk = potentialChunk; + } + } + + if (currentChunk.trim()) { + chunks.push(currentChunk.trim()); + } + + return chunks.length > 0 ? chunks : [text]; +} + +async function debugIngestion() { + try { + console.log('🔍 Debugging ingestion document structure...\n'); + + // Test with a single Button component file + const testFilePath = path.resolve( + REPO_ROOT, + 'packages/react/src/components/Button/Button.mdx', + ); + + console.log(`📁 Testing with file: ${testFilePath}`); + console.log(`📂 File exists: ${fs.existsSync(testFilePath)}`); + + if (fs.existsSync(testFilePath)) { + // Process the Button.mdx file + const content = fs.readFileSync(testFilePath, 'utf8'); + const { + title, + content: extractedContent, + lang, + } = extractTextFromFile(testFilePath, content); + + console.log('\n📊 Extracted data:'); + console.log(` Title: ${title}`); + console.log(` Lang: ${lang}`); + console.log(` Content length: ${extractedContent.length}`); + console.log( + ` Content preview: ${extractedContent.substring(0, 100)}...`, + ); + + // Create chunks + const chunks = chunkText(extractedContent); + console.log(` Chunk count: ${chunks.length}`); + + // Generate document structure + const url = generateUrl(testFilePath); + const relativePath = path.relative(REPO_ROOT, testFilePath); + + console.log(` URL: ${url}`); + console.log(` Relative path: ${relativePath}`); + + // Create document like the ingestion script does + const document = { + id: crypto + .createHash('md5') + .update(testFilePath + chunks[0] + 0) + .digest('hex'), + title: chunks.length > 1 ? `${title} (Part 1)` : title, + content: chunks[0], + url, + file_path: relativePath, + lang, + type: relativePath.includes('components/') + ? 'component' + : relativePath.includes('design-tokens/') + ? 'design-token' + : relativePath.includes('figma/') + ? 'figma' + : relativePath.includes('themebuilder/') + ? 'themebuilder' + : relativePath.includes('best-practices/') + ? 'best-practice' + : 'general', + vector: null, // Skip embedding for debugging + }; + + console.log('\n📄 Document structure:'); + console.log(JSON.stringify(document, null, 2)); + + return; + } + if (!fs.existsSync(testFilePath)) { + console.log('❌ Test file not found, looking for alternatives...'); + + // Find a component directory + const componentsDir = path.resolve( + REPO_ROOT, + 'packages/react/src/components', + ); + const componentDirs = fs.readdirSync(componentsDir).filter((name) => { + const fullPath = path.join(componentsDir, name); + return fs.statSync(fullPath).isDirectory(); + }); + + console.log( + `📂 Found component directories: ${componentDirs.slice(0, 3).join(', ')}`, + ); + + if (componentDirs.length > 0) { + const buttonDir = path.join(componentsDir, 'Button'); + console.log(`🎯 Testing with Button directory: ${buttonDir}`); + + // List files in Button directory + const buttonFiles = fs.readdirSync(buttonDir); + console.log(`📄 Files in Button directory: ${buttonFiles.join(', ')}`); + + // Find an MDX file + const mdxFile = buttonFiles.find((f) => f.endsWith('.mdx')); + if (mdxFile) { + const mdxPath = path.join(buttonDir, mdxFile); + console.log(`📝 Testing with MDX file: ${mdxPath}`); + + // Process this file + const content = fs.readFileSync(mdxPath, 'utf8'); + const { + title, + content: extractedContent, + lang, + } = extractTextFromFile(mdxPath, content); + + console.log('\n📊 Extracted data:'); + console.log(` Title: ${title}`); + console.log(` Lang: ${lang}`); + console.log(` Content length: ${extractedContent.length}`); + console.log( + ` Content preview: ${extractedContent.substring(0, 100)}...`, + ); + + // Create chunks + const chunks = chunkText(extractedContent); + console.log(` Chunk count: ${chunks.length}`); + + // Generate document structure + const url = generateUrl(mdxPath); + const relativePath = path.relative(REPO_ROOT, mdxPath); + + console.log(` URL: ${url}`); + console.log(` Relative path: ${relativePath}`); + + // Create document like the ingestion script does + const document = { + id: crypto + .createHash('md5') + .update(mdxPath + chunks[0] + 0) + .digest('hex'), + title: chunks.length > 1 ? `${title} (Part 1)` : title, + content: chunks[0], + url, + file_path: relativePath, + lang, + type: relativePath.includes('components/') + ? 'component' + : relativePath.includes('design-tokens/') + ? 'design-token' + : relativePath.includes('figma/') + ? 'figma' + : relativePath.includes('themebuilder/') + ? 'themebuilder' + : relativePath.includes('best-practices/') + ? 'best-practice' + : 'general', + vector: null, // Skip embedding for debugging + }; + + console.log('\n📄 Document structure:'); + console.log(JSON.stringify(document, null, 2)); + + return; + } + } + } + + console.log('❌ Could not find suitable test file'); + } catch (error) { + console.error('❌ Debug failed:', error); + } +} + +debugIngestion(); diff --git a/apps/ai-api/scripts/debug-search.mjs b/apps/ai-api/scripts/debug-search.mjs new file mode 100644 index 0000000000..d1aa020b7f --- /dev/null +++ b/apps/ai-api/scripts/debug-search.mjs @@ -0,0 +1,93 @@ +#!/usr/bin/env node +// Debug script to check what's in Meilisearch index + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Meilisearch } from 'meilisearch'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// Load environment variables +const envPathCandidates = [ + path.resolve(__dirname, '../../.ai-env'), + path.resolve(__dirname, '../../../apps/.ai-env'), + path.resolve(__dirname, '../../../.ai-env'), +]; + +for (const p of envPathCandidates) { + if (fs.existsSync(p)) { + const lines = fs + .readFileSync(p, 'utf8') + .split(/\r?\n/) + .filter((l) => l.trim() && !l.startsWith('#')); + for (const line of lines) { + const [k, ...rest] = line.split('='); + if (!(k in process.env)) process.env[k] = rest.join('='); + } + console.log(`Loaded env vars from ${p}`); + break; + } +} + +const env = process.env; +const INDEX_NAME = env.MEILISEARCH_PROJECT_NAME || 'designsystemet-search'; + +// Initialize client +const meiliClient = new Meilisearch({ + host: env.MEILISEARCH_API_URL, + apiKey: env.MEILISEARCH_ADMIN_KEY, +}); + +async function debugSearch() { + try { + const index = meiliClient.index(INDEX_NAME); + + // Check index stats + console.log('📊 Index Stats:'); + const stats = await index.getStats(); + console.log(` Documents: ${stats.numberOfDocuments}`); + console.log(` Index size: ${stats.databaseSize} bytes`); + console.log(''); + + // Search for Button component specifically + console.log('🔍 Searching for "Button component":'); + const buttonResults = await index.search('Button component', { limit: 5 }); + console.log(` Found ${buttonResults.hits.length} results:`); + + buttonResults.hits.forEach((hit, i) => { + console.log(` ${i + 1}. ${hit.title} (${hit.type || 'unknown type'})`); + console.log(` URL: ${hit.url}`); + console.log(` Content preview: ${hit.content.substring(0, 100)}...`); + console.log(''); + }); + + // Search for "Button" only + console.log('🔍 Searching for "Button":'); + const simpleButtonResults = await index.search('Button', { limit: 5 }); + console.log(` Found ${simpleButtonResults.hits.length} results:`); + + simpleButtonResults.hits.forEach((hit, i) => { + console.log(` ${i + 1}. ${hit.title} (${hit.type || 'unknown type'})`); + console.log(` URL: ${hit.url}`); + console.log(` Content preview: ${hit.content.substring(0, 100)}...`); + console.log(''); + }); + + // Check what component types we have + console.log('🔍 Searching for documents with type "combined":'); + const componentResults = await index.search('', { + limit: 10, + filter: 'type = combined', + }); + console.log(` Found ${componentResults.hits.length} component documents:`); + + componentResults.hits.forEach((hit, i) => { + console.log(` ${i + 1}. ${hit.title}`); + }); + } catch (error) { + console.error('❌ Debug search failed:', error); + } +} + +debugSearch(); diff --git a/apps/ai-api/scripts/ingest.mjs b/apps/ai-api/scripts/ingest.mjs new file mode 100644 index 0000000000..2db757bfea --- /dev/null +++ b/apps/ai-api/scripts/ingest.mjs @@ -0,0 +1,614 @@ +#!/usr/bin/env node + +// Ingestion script: walks repo, chunks content, embeds content, and stores in Meilisearch +// Usage: node scripts/ingest.mjs [--force] [--dry-run] + +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Meilisearch } from 'meilisearch'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// Load environment variables +const envPathCandidates = [ + path.resolve(__dirname, '../../.ai-env'), + path.resolve(__dirname, '../../../apps/.ai-env'), + path.resolve(__dirname, '../../../.ai-env'), +]; + +for (const p of envPathCandidates) { + if (fs.existsSync(p)) { + const lines = fs + .readFileSync(p, 'utf8') + .split(/\r?\n/) + .filter((l) => l.trim() && !l.startsWith('#')); + for (const line of lines) { + const [k, ...rest] = line.split('='); + if (!(k in process.env)) process.env[k] = rest.join('='); + } + console.log(`Loaded env vars from ${p}`); + break; + } +} + +const env = process.env; + +// Configuration +const REPO_ROOT = path.resolve(__dirname, '../../..'); +const CONTENT_DIRS = [ + // Component documentation (MDX + stories) + 'packages/react/src/components', + // Utility/hook documentation + 'packages/react/src/utilities', + // Design token documentation + 'apps/www/app/content/fundamentals/*/design-tokens', + // Figma documentation + 'apps/www/app/content/fundamentals/*/figma', + // Themebuilder documentation + 'apps/www/app/content/fundamentals/*/themebuilder', + // Best practices documentation + 'apps/www/app/content/best-practices', + // General website content + 'apps/www/app/content', + // Theme CSS files + 'packages/theme/brand', + 'packages/theme/src/themes', +]; +const FILE_PATTERNS = /\.(md|mdx|tsx|css)$/i; +const CHUNK_SIZE = 300; // tokens (approximate) +// const CHUNK_OVERLAP = 50; // Could be used to create more context between chunks +const INDEX_NAME = env.MEILISEARCH_PROJECT_NAME || 'designsystemet-search'; + +// Initialize clients +const meiliClient = new Meilisearch({ + host: env.MEILISEARCH_API_URL, + apiKey: env.MEILISEARCH_ADMIN_KEY, +}); + +// Helper functions +function extractLanguageFromPath(filePath) { + // Extract language from path structure + if (filePath.includes('/en/')) return 'en'; + if (filePath.includes('/no/')) return 'no'; + return 'en'; // default to English +} + +function extractTextFromFile(filePath, content) { + const ext = path.extname(filePath).toLowerCase(); + const lang = extractLanguageFromPath(filePath); + + if (ext === '.md' || ext === '.mdx') { + // Extract frontmatter and content + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); + if (frontmatterMatch) { + const frontmatter = frontmatterMatch[1]; + const body = frontmatterMatch[2]; + + // Extract title from frontmatter + const titleMatch = frontmatter.match(/title:\s*["']?([^"'\n]+)["']?/); + const title = titleMatch ? titleMatch[1] : path.basename(filePath, ext); + + return { title, content: body.trim(), lang }; + } + + // No frontmatter, use first heading as title + const headingMatch = content.match(/^#\s+(.+)$/m); + const title = headingMatch ? headingMatch[1] : path.basename(filePath, ext); + + return { title, content: content.trim(), lang }; + } + + if (ext === '.tsx') { + // Extract JSDoc comments and component names + const componentMatch = content.match( + /export\s+(?:default\s+)?(?:function|const)\s+(\w+)/, + ); + const title = componentMatch + ? `${componentMatch[1]} Component` + : path.basename(filePath, ext); + + // Extract JSDoc comments + const jsdocMatches = content.match(/\/\*\*[\s\S]*?\*\//g) || []; + const comments = jsdocMatches + .map((comment) => comment.replace(/\/\*\*|\*\/|\s*\*\s?/g, '').trim()) + .join('\n\n'); + + return { title, content: comments || `Component: ${title}`, lang }; + } + + if (ext === '.css') { + // Extract CSS custom properties and their values + const customProps = content.match(/--[\w-]+:\s*[^;]+;/g) || []; + const title = `${path.basename(filePath, ext)} Theme`; + + if (customProps.length > 0) { + const propList = customProps.slice(0, 20).join('\n'); // Limit to first 20 props + const content_text = `CSS Custom Properties:\n\n${propList}\n\n${customProps.length > 20 ? `...and ${customProps.length - 20} more properties` : ''}`; + return { title, content: content_text, lang }; + } + + return { title, content: `CSS theme file: ${title}`, lang }; + } + + return { title: path.basename(filePath, ext), content: content.trim(), lang }; +} + +function chunkText(text, maxTokens = CHUNK_SIZE) { + // Simple chunking by sentences/paragraphs + const sentences = text.split(/[.!?]\s+/).filter((s) => s.trim()); + const chunks = []; + let currentChunk = ''; + + for (const sentence of sentences) { + const potentialChunk = currentChunk + (currentChunk ? '. ' : '') + sentence; + + // Rough token estimation (1 token ≈ 4 characters) + if (potentialChunk.length / 4 > maxTokens && currentChunk) { + chunks.push(currentChunk.trim()); + currentChunk = sentence; + } else { + currentChunk = potentialChunk; + } + } + + if (currentChunk.trim()) chunks.push(currentChunk.trim()); + + return chunks.length > 0 ? chunks : [text]; +} + +async function combineComponentFiles(componentDir) { + // Combine MDX documentation with stories for a single component + const componentName = path.basename(componentDir); + const files = fs.readdirSync(componentDir); + + let mdxContent = ''; + let storiesContent = ''; + let title = componentName; + + // Read MDX file + const mdxFile = files.find((f) => f.endsWith('.mdx')); + if (mdxFile) { + const mdxPath = path.join(componentDir, mdxFile); + const mdxRaw = fs.readFileSync(mdxPath, 'utf8'); + const extracted = extractTextFromFile(mdxPath, mdxRaw); + title = extracted.title; + mdxContent = extracted.content; + } + + // Read stories file + const storiesFile = files.find((f) => f.endsWith('.stories.tsx')); + if (storiesFile) { + const storiesPath = path.join(componentDir, storiesFile); + const storiesRaw = fs.readFileSync(storiesPath, 'utf8'); + + // Extract story names and descriptions + const storyMatches = + storiesRaw.match(/export const (\w+)[^=]*=[\s\S]*?(?=export|Z)/g) || []; + const stories = storyMatches + .map((story) => { + const nameMatch = story.match(/export const (\w+)/); + return nameMatch ? nameMatch[1] : ''; + }) + .filter(Boolean); + + if (stories.length > 0) { + storiesContent = `\n\nExamples:\n${stories.join(', ')}`; + } + } + + // Generate Storybook URL + const storybookUrl = `https://storybook.designsystemet.no/?path=/docs/komponenter-${componentName.toLowerCase()}--docs`; + + const combinedContent = mdxContent + storiesContent; + + return { + title: `${title} Component`, + content: combinedContent, + url: storybookUrl, + lang: 'en', // Components are primarily English + type: 'component', + }; +} + +function generateUrl(filePath) { + // Convert file path to URL + const relativePath = path.relative(REPO_ROOT, filePath); + + if (relativePath.startsWith('packages/react/src/components/')) { + // Component files are handled by combineComponentFiles - this shouldn't be called for them + const componentName = relativePath.split('/')[4]; + return `https://storybook.designsystemet.no/?path=/docs/komponenter-${componentName?.toLowerCase()}--docs`; + } + + if (relativePath.startsWith('packages/react/src/utilities/')) { + // Utility/hook documentation + const utilityName = relativePath.split('/').slice(-2, -1)[0]; // Get parent directory name + return `https://storybook.designsystemet.no/?path=/docs/utilities-${utilityName?.toLowerCase()}--docs`; + } + + if (relativePath.startsWith('packages/theme/')) { + // Theme CSS files + return `https://designsystemet.no/en/fundamentals/design-tokens/colors`; + } + + if (relativePath.startsWith('apps/www/app/content/')) { + // Website content + const urlPath = relativePath + .replace('apps/www/app/content/', '') + .replace(/\.(md|mdx)$/, '') + .replace('/index', ''); + + return `https://designsystemet.no/${urlPath}`; + } + + // Fallback + return `https://designsystemet.no/`; +} + +async function embedText(text) { + const url = `${env.AZURE_ENDPOINT}/openai/deployments/${env.AZURE_EMBEDDING_DEPLOY_SMALL}/embeddings?api-version=${env.AZURE_API_VERSION}`; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'api-key': env.AZURE_KEY, + }, + body: JSON.stringify({ input: text }), + }); + + if (!response.ok) { + throw new Error( + `Embedding failed: ${response.status} ${response.statusText}`, + ); + } + + const data = await response.json(); + return data.data[0].embedding; +} + +async function processFile(filePath, dryRun = false) { + try { + const content = fs.readFileSync(filePath, 'utf8'); + const { + title, + content: extractedContent, + lang, + } = extractTextFromFile(filePath, content); + + if (!extractedContent || extractedContent.trim().length < 50) { + return []; // Skip very short content + } + + const chunks = chunkText(extractedContent); + const url = generateUrl(filePath); + const relativePath = path.relative(REPO_ROOT, filePath); + + // Generate embeddings for all chunks if not dry run + const vectors = dryRun + ? [] + : await Promise.all(chunks.map((chunk) => embedText(chunk))); + + const documents = chunks.map((chunk, index) => { + const id = crypto + .createHash('md5') + .update(filePath + chunk + index) + .digest('hex'); + + return { + id, + title: title, // Clean title without (Part X) + content: chunk, + url, + file_path: relativePath, + lang, + type: relativePath.includes('components/') + ? 'component' + : relativePath.includes('design-tokens/') + ? 'design-token' + : relativePath.includes('figma/') + ? 'figma' + : relativePath.includes('themebuilder/') + ? 'themebuilder' + : relativePath.includes('best-practices/') + ? 'best-practice' + : 'general', + // Metadata for chunk tracking (doesn't affect search quality) + part_index: chunks.length > 1 ? index + 1 : null, + total_parts: chunks.length > 1 ? chunks.length : null, + // Use _vectors with embedder name for Meilisearch + _vectors: dryRun + ? null + : { + [process.env.MEILISEARCH_EMBEDDER_UID || 'azure-openai-small']: + vectors[index], + }, + }; + }); + + if (!dryRun) { + console.log(` ✓ ${title} (${chunks.length} chunks, ${lang})`); + } + + return documents; + } catch (error) { + console.error(` ✗ Error processing ${filePath}:`, error.message); + return []; + } +} + +async function processComponentDirectory(componentDir, dryRun = false) { + try { + const combinedData = await combineComponentFiles(componentDir); + + if (!combinedData.content || combinedData.content.trim().length < 50) { + return []; + } + + const chunks = chunkText(combinedData.content); + const relativePath = path.relative(REPO_ROOT, componentDir); + + // Generate embeddings for all chunks if not dry run + const vectors = dryRun + ? [] + : await Promise.all(chunks.map((chunk) => embedText(chunk))); + + const documents = chunks.map((chunk, index) => { + const id = crypto + .createHash('md5') + .update(componentDir + chunk + index) + .digest('hex'); + + return { + id, + title: + chunks.length > 1 + ? `${combinedData.title} (Part ${index + 1})` + : combinedData.title, + content: chunk, + url: combinedData.url, + file_path: relativePath, + lang: combinedData.lang, + type: 'component', + // Use _vectors with embedder name for Meilisearch + _vectors: dryRun + ? null + : { + [process.env.MEILISEARCH_EMBEDDER_UID || 'azure-openai-small']: + vectors[index], + }, + }; + }); + + if (!dryRun) { + console.log( + ` ✓ ${combinedData.title} (${chunks.length} chunks, combined)`, + ); + } + + return documents; + } catch (error) { + console.error( + ` ✗ Error processing component directory ${componentDir}:`, + error.message, + ); + return []; + } +} + +async function walkDirectory(dir) { + const files = []; + const componentDirs = []; + + function walk(currentDir) { + if (!fs.existsSync(currentDir)) return; + + const entries = fs.readdirSync(currentDir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(currentDir, entry.name); + + if ( + entry.isDirectory() && + !entry.name.startsWith('.') && + entry.name !== 'node_modules' + ) { + // Check if this is a component directory (has both .mdx and .stories.tsx) + if (currentDir.includes('packages/react/src/components')) { + const dirFiles = fs.readdirSync(fullPath); + const hasMdx = dirFiles.some((f) => f.endsWith('.mdx')); + const hasStories = dirFiles.some((f) => f.endsWith('.stories.tsx')); + + if (hasMdx && hasStories) { + componentDirs.push(fullPath); + continue; // Skip walking into component dirs - we'll process them specially + } + } + + walk(fullPath); + } else if (entry.isFile() && FILE_PATTERNS.test(entry.name)) { + // Skip individual component files if they're in a component directory + const isComponentFile = + fullPath.includes('packages/react/src/components') && + (entry.name.endsWith('.mdx') || entry.name.endsWith('.stories.tsx')); + if (!isComponentFile) { + files.push(fullPath); + } + } + } + } + + walk(dir); + return { files, componentDirs }; +} + +async function setupIndex() { + try { + // Try to get existing index + await meiliClient.getIndex(INDEX_NAME); + console.log(`📊 Using existing index: ${INDEX_NAME}`); + } catch (_error) { + // On error, create new index + console.log(`📊 Creating new index: ${INDEX_NAME}`); + await meiliClient.createIndex(INDEX_NAME, { primaryKey: 'id' }); + + // Wait for index creation + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + const index = meiliClient.index(INDEX_NAME); + + // Configure index settings + await index.updateSettings({ + searchableAttributes: ['title', 'content'], + displayedAttributes: ['id', 'title', 'url', 'content', 'file_path'], + filterableAttributes: ['file_path'], + sortableAttributes: ['title'], + // Note: synonyms are managed by setup-synonyms.mjs, synonyms are not affected by running the ingest script + }); + + console.log(`⚙️ Index settings updated`); + return index; +} + +async function main() { + const args = process.argv.slice(2); + const dryRun = args.includes('--dry-run'); + const testMode = args.includes('--test'); + + console.log(`🚀 Starting enhanced ingestion ${dryRun ? '(dry run)' : ''}`); + console.log(`📁 Repo root: ${REPO_ROOT}`); + + // Collect all files and component directories + const allFiles = []; + const allComponentDirs = []; + + for (const dir of CONTENT_DIRS) { + const fullDir = path.join(REPO_ROOT, dir); + if (fullDir.includes('*')) { + // Handle glob patterns for language-specific directories + const basePath = fullDir.split('*')[0]; + const suffix = fullDir.split('*')[1]; + + for (const lang of ['en', 'no']) { + const langDir = basePath + lang + suffix; + if (fs.existsSync(langDir)) { + const { files, componentDirs } = await walkDirectory(langDir); + allFiles.push(...files); + allComponentDirs.push(...componentDirs); + console.log( + `📂 Found ${files.length} files and ${componentDirs.length} component dirs in ${lang}${suffix}`, + ); + } + } + } else if (fs.existsSync(fullDir)) { + const { files, componentDirs } = await walkDirectory(fullDir); + allFiles.push(...files); + allComponentDirs.push(...componentDirs); + console.log( + `📂 Found ${files.length} files and ${componentDirs.length} component dirs in ${dir}`, + ); + } + } + + console.log(`📄 Total files to process: ${allFiles.length}`); + console.log( + `📦 Total component directories to process: ${allComponentDirs.length}`, + ); + + // In test mode, only process first 3 files and 3 component dirs + const filesToProcess = testMode ? allFiles.slice(0, 3) : allFiles; + const componentDirsToProcess = testMode + ? allComponentDirs.slice(0, 3) + : allComponentDirs; + + if (testMode) { + console.log( + `🧪 Test mode: processing only ${filesToProcess.length} files and ${componentDirsToProcess.length} component dirs`, + ); + } + + if (!dryRun) { + // Setup Meilisearch index + const index = await setupIndex(); + + // Process files and collect documents + const allDocuments = []; + + // Process individual files + for (const filePath of filesToProcess) { + const documents = await processFile(filePath, dryRun); + allDocuments.push(...documents); + } + + // Process component directories (combine MDX + stories) + for (const componentDir of componentDirsToProcess) { + const documents = await processComponentDirectory(componentDir, dryRun); + allDocuments.push(...documents); + } + + if (allDocuments.length > 0) { + console.log( + `📤 Uploading ${allDocuments.length} documents to Meilisearch...`, + ); + const task = await index.addDocuments(allDocuments); + console.log(`✅ Upload task queued: ${task.taskUid}`); + } + } else { + // Dry run: just show what would be processed + let totalChunks = 0; + const samplesToShow = testMode ? filesToProcess : allFiles.slice(0, 3); + const componentSamplesToShow = testMode + ? componentDirsToProcess + : allComponentDirs.slice(0, 3); + + console.log('\n📄 Sample files:'); + for (const filePath of samplesToShow) { + const documents = await processFile(filePath, true); + totalChunks += documents.length; + + if (documents.length > 0) { + console.log(` → ${documents[0].title} (${documents[0].lang})`); + console.log(` → ${documents[0].url}`); + console.log(` → ${documents[0].content.substring(0, 100)}...`); + console.log(''); + } + } + + console.log('\n📦 Sample component directories:'); + for (const componentDir of componentSamplesToShow) { + const documents = await processComponentDirectory(componentDir, true); + totalChunks += documents.length; + + if (documents.length > 0) { + console.log(` → ${documents[0].title} (${documents[0].lang})`); + console.log(` → ${documents[0].url}`); + console.log(` → ${documents[0].content.substring(0, 100)}...`); + console.log(''); + } + } + + if (!testMode) { + const fileEstimate = + totalChunks * (allFiles.length / Math.max(samplesToShow.length, 1)); + const componentEstimate = + totalChunks * + (allComponentDirs.length / Math.max(componentSamplesToShow.length, 1)); + console.log( + `📊 Would create ~${Math.round(fileEstimate + componentEstimate)} total chunks`, + ); + } else { + console.log(`📊 Would create ${totalChunks} total chunks`); + } + } + + console.log('🎉 Enhanced ingestion complete!'); +} + +// Run the script +main().catch((error) => { + console.error('💥 Ingestion failed:', error); + process.exit(1); +}); diff --git a/apps/ai-api/scripts/inspect-document-fields.mjs b/apps/ai-api/scripts/inspect-document-fields.mjs new file mode 100644 index 0000000000..885a3b66ec --- /dev/null +++ b/apps/ai-api/scripts/inspect-document-fields.mjs @@ -0,0 +1,115 @@ +#!/usr/bin/env node +// Inspect what fields are actually present in our documents + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Meilisearch } from 'meilisearch'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// Load environment variables +const envPathCandidates = [ + path.resolve(__dirname, '../../.ai-env'), + path.resolve(__dirname, '../../../apps/.ai-env'), + path.resolve(__dirname, '../../../.ai-env'), +]; + +for (const p of envPathCandidates) { + if (fs.existsSync(p)) { + const lines = fs + .readFileSync(p, 'utf8') + .split(/\r?\n/) + .filter((l) => l.trim() && !l.startsWith('#')); + for (const line of lines) { + const [k, ...rest] = line.split('='); + if (!(k in process.env)) process.env[k] = rest.join('='); + } + console.log(`Loaded env vars from ${p}`); + break; + } +} + +const env = process.env; +const INDEX_NAME = env.MEILISEARCH_PROJECT_NAME || 'designsystemet-search'; + +const meiliClient = new Meilisearch({ + host: env.MEILISEARCH_API_URL, + apiKey: env.MEILISEARCH_ADMIN_KEY, +}); + +async function inspectDocumentFields() { + try { + const index = meiliClient.index(INDEX_NAME); + + console.log('🔍 Inspecting document fields...\n'); + + // Get a few sample documents to see their structure + const results = await index.search('', { limit: 10 }); + + console.log(`📊 Found ${results.hits.length} sample documents\n`); + + // Analyze field presence across documents + const fieldAnalysis = {}; + const sampleValues = {}; + + results.hits.forEach((doc) => { + Object.keys(doc).forEach((field) => { + if (!fieldAnalysis[field]) { + fieldAnalysis[field] = 0; + sampleValues[field] = []; + } + fieldAnalysis[field]++; + + // Store a few sample values for each field + if (sampleValues[field].length < 3) { + const value = doc[field]; + const shortValue = + typeof value === 'string' && value.length > 50 + ? value.substring(0, 50) + '...' + : value; + sampleValues[field].push(shortValue); + } + }); + }); + + console.log('📋 Field Analysis:'); + Object.entries(fieldAnalysis).forEach(([field, count]) => { + const percentage = ((count / results.hits.length) * 100).toFixed(0); + console.log( + ` ${field}: ${count}/${results.hits.length} documents (${percentage}%)`, + ); + + // Show sample values + if (sampleValues[field].length > 0) { + const samples = sampleValues[field] + .slice(0, 2) + .map((v) => `"${v}"`) + .join(', '); + console.log(` Sample values: ${samples}`); + } + console.log(''); + }); + + // Check specifically for documents that should have a 'type' field + console.log('🔍 Looking for documents with specific patterns...\n'); + + // Search for Button component specifically + const buttonResults = await index.search('Button Component', { limit: 3 }); + console.log( + `📝 Button Component documents (${buttonResults.hits.length} found):`, + ); + buttonResults.hits.forEach((doc, i) => { + console.log(` ${i + 1}. "${doc.title}"`); + console.log(` ID: ${doc.id}`); + console.log(` Type: ${doc.type || 'MISSING'}`); + console.log(` Lang: ${doc.lang || 'MISSING'}`); + console.log(` URL: ${doc.url}`); + console.log(''); + }); + } catch (error) { + console.error('❌ Inspection failed:', error); + } +} + +inspectDocumentFields(); diff --git a/apps/ai-api/scripts/setup-embedder.mjs b/apps/ai-api/scripts/setup-embedder.mjs new file mode 100755 index 0000000000..cdfc9b83ab --- /dev/null +++ b/apps/ai-api/scripts/setup-embedder.mjs @@ -0,0 +1,103 @@ +#!/usr/bin/env node +// Setup script to register an embedder with Meilisearch +// Usage: node scripts/setup-embedder.mjs + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import fetch from 'node-fetch'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// Load environment variables +const envPathCandidates = [ + path.resolve(__dirname, '../../.ai-env'), + path.resolve(__dirname, '../../../apps/.ai-env'), + path.resolve(__dirname, '../../../.ai-env'), +]; + +for (const p of envPathCandidates) { + if (fs.existsSync(p)) { + const lines = fs + .readFileSync(p, 'utf8') + .split(/\r?\n/) + .filter((l) => l.trim() && !l.startsWith('#')); + for (const line of lines) { + const [k, ...rest] = line.split('='); + if (!(k in process.env)) process.env[k] = rest.join('='); + } + console.log(`Loaded env vars from ${p}`); + break; + } +} + +const env = process.env; + +// Configuration +const EMBEDDER_UID = env.MEILISEARCH_EMBEDDER_UID || 'azure-openai-small'; +const VECTOR_DIMENSIONS = 1536; // For text-embedding-3-small + +async function setupEmbedder() { + if (!env.MEILISEARCH_API_URL) { + console.error('Error: MEILISEARCH_API_URL is not defined in environment'); + process.exit(1); + } + + if (!env.MEILISEARCH_ADMIN_KEY) { + console.error('Error: MEILISEARCH_ADMIN_KEY is not defined in environment'); + process.exit(1); + } + + const INDEX_NAME = env.MEILISEARCH_PROJECT_NAME || 'designsystemet-search'; + + try { + // Since we generate embeddings ourselves (see ingest.mjs), + // "source: 'userProvided'" tells MeiliSearch to use those + const embedderConfig = { + [EMBEDDER_UID]: { + source: 'userProvided', + dimensions: VECTOR_DIMENSIONS, + }, + }; + + // Update embedders settings for the index + const response = await fetch( + `${env.MEILISEARCH_API_URL}/indexes/${INDEX_NAME}/settings/embedders`, + { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${env.MEILISEARCH_ADMIN_KEY}`, + }, + body: JSON.stringify(embedderConfig), + }, + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error( + `Failed to create embedder: ${response.status} ${response.statusText}`, + ); + console.error(errorText); + process.exit(1); + } + + const result = await response.json(); + console.log('Response:'); + console.log(JSON.stringify(result, null, 2)); + + console.log('\nNext steps:'); + console.log( + '2. Wait for the embedder setup task to complete (check task status if needed)', + ); + console.log( + '3. Make sure you have documents with embeddings ingested (run: node scripts/ingest.mjs)', + ); + console.log('4. Test the search with: npm run test-rag'); + } catch (error) { + console.error('Error setting up embedder:', error); + process.exit(1); + } +} + +setupEmbedder(); diff --git a/apps/ai-api/scripts/setup-synonyms.mjs b/apps/ai-api/scripts/setup-synonyms.mjs new file mode 100644 index 0000000000..c843f66b29 --- /dev/null +++ b/apps/ai-api/scripts/setup-synonyms.mjs @@ -0,0 +1,135 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import path from 'node:path'; +import dotenv from 'dotenv'; +import { Meilisearch } from 'meilisearch'; + +// Load environment variables +const envPathCandidates = [ + path.resolve(process.cwd(), '../.ai-env'), + path.resolve(process.cwd(), 'apps/.ai-env'), + path.resolve(process.cwd(), '.ai-env'), +]; + +for (const p of envPathCandidates) { + if (fs.existsSync(p)) { + dotenv.config({ path: p }); + console.log(`Loaded env vars from ${p}`); + break; + } +} + +const client = new Meilisearch({ + host: process.env.MEILISEARCH_API_URL || '', + apiKey: process.env.MEILISEARCH_ADMIN_KEY || '', +}); + +const INDEX_NAME = + process.env.MEILISEARCH_PROJECT_NAME || 'designsystemet-search'; + +// TODO: Update synonyms with Norwegian terms +// TODO: Update synonyms with designer terms/Figma terms +// TODO: check https://designsystems.surf/ for other synonyms + +// Convert grouped synonyms to Meilisearch "mutual association format": +// https://www.meilisearch.com/docs/learn/relevancy/synonyms#mutual-association +// Each term must explicitly be made bidirectional so Meilisearch can find synonyms in both directions +// Example: alert -> notification, message, toast, banner +// Example: notification -> alert, message, toast, banner +// Example: message -> alert, notification, toast, banner +// Example: toast -> alert, notification, message, banner +// Example: banner -> alert, notification, message, toast +function createMutualAssociations(groups) { + const synonyms = {}; + Object.values(groups).forEach((group) => { + group.forEach((term) => { + synonyms[term] = group.filter((synonym) => synonym !== term); + }); + }); + return synonyms; +} + +// Synonym groups based on actual Designsystemet components (as of 2025-07-31) +const synonymGroups = { + alert: ['alert', 'notification', 'message', 'toast', 'banner'], + avatar: ['avatar', 'profile picture', 'user image'], + badge: ['badge', 'label', 'status indicator'], + breadcrumbs: ['breadcrumbs', 'breadcrumb', 'navigation path'], + button: ['button', 'cta', 'call-to-action', 'call to action', 'action'], + card: ['card', 'tile', 'panel'], + checkbox: ['checkbox', 'check', 'checkmark'], + chip: ['chip', 'tag', 'pill'], + details: ['details', 'accordion', 'collapsible', 'expandable'], + dialog: ['dialog', 'modal', 'overlay'], + divider: ['divider', 'separator', 'line'], + dropdown: ['dropdown', 'menu'], + errorsummary: ['errorsummary', 'error summary', 'validation errors'], + field: ['field', 'form field'], + fieldset: ['fieldset', 'form group', 'field group'], + input: ['input', 'textfield', 'text field', 'inputfield'], + link: ['link', 'anchor', 'hyperlink'], + list: ['list', 'menu', 'items'], + loaders: ['loaders', 'spinner', 'loading', 'progress'], + pagination: ['pagination', 'pager', 'page navigation'], + popover: ['popover', 'popup'], + radio: ['radio', 'radio button', 'option'], + search: ['search', 'search box', 'filter'], + select: ['select', 'dropdown', 'picker'], + skiplink: ['skiplink', 'skip link', 'accessibility link'], + suggestion: [ + 'suggestion', + 'autocomplete', + 'combobox', + 'typeahead', + 'multiselect', + 'multi select', + ], + switch: ['switch', 'toggle'], + table: ['table', 'datagrid', 'data table'], + tabs: ['tabs', 'tab', 'tabpanel'], + textarea: ['textarea', 'text area', 'multiline input'], + togglegroup: ['togglegroup', 'toggle group', 'button group'], + tooltip: ['tooltip', 'hint', 'help text'], +}; + +// Generate proper Meilisearch synonym format +const synonyms = createMutualAssociations(synonymGroups); + +async function setupSynonyms() { + try { + console.log(`Setting up synonyms for index: ${INDEX_NAME}`); + + const index = client.index(INDEX_NAME); + + // Update synonyms settings + console.log('Configuring synonyms...'); + const task = await index.updateSynonyms(synonyms); + + console.log(`✅ Synonym update task enqueued with ID: ${task.taskUid}`); + console.log('📝 Synonyms are being configured in Meilisearch...'); + + // Display configured synonyms + console.log('\n🔧 Configured synonym groups:'); + Object.entries(synonyms).forEach(([key, values]) => { + console.log(` ${key}: [${values.join(', ')}]`); + }); + + console.log(`\n🔍 Total synonym groups: ${Object.keys(synonyms).length}`); + console.log('\n💡 Examples of improved search:'); + console.log(' - "modal" will now find Dialog component'); + console.log(' - "dropdown" will now find Select component'); + console.log(' - "popup" will now find Popover/Tooltip components'); + console.log(' - "textfield" will now find Input component'); + console.log('\n✨ Synonym configuration complete!'); + console.log( + 'Note: Changes may take a few moments to propagate in Meilisearch.', + ); + } catch (error) { + console.error('❌ Error setting up synonyms:', error); + process.exit(1); + } +} + +// Run the setup +setupSynonyms(); diff --git a/apps/ai-api/scripts/setup.mjs b/apps/ai-api/scripts/setup.mjs new file mode 100644 index 0000000000..df254828fd --- /dev/null +++ b/apps/ai-api/scripts/setup.mjs @@ -0,0 +1,141 @@ +#!/usr/bin/env node + +/** + * Complete setup script for AI Search + * Runs all initialization steps in sequence: + * 1. Configure embedder + * 2. Ingest documents + * 3. Configure synonyms + * 4. Verify setup + */ + +import { spawn } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// ANSI color codes for pretty output +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + red: '\x1b[31m', +}; + +function log(message, color = colors.reset) { + console.log(`${color}${message}${colors.reset}`); +} + +function logStep(step, total, message) { + log( + `\n${colors.bright}[${step}/${total}] ${message}${colors.reset}`, + colors.blue, + ); +} + +function runScript(scriptPath) { + return new Promise((resolve, reject) => { + const child = spawn('node', [scriptPath], { + stdio: 'inherit', + cwd: path.dirname(scriptPath), + env: process.env, // Pass environment variables to child processes + }); + + child.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Script exited with code ${code}`)); + } + }); + + child.on('error', (error) => { + reject(error); + }); + }); +} + +async function main() { + log( + '\n╔════════════════════════════════════════════════════════════╗', + colors.bright, + ); + log( + '║ AI Search - Complete Setup ║', + colors.bright, + ); + log( + '╚════════════════════════════════════════════════════════════╝\n', + colors.bright, + ); + + const steps = [ + { + name: 'Configure Embedder', + script: path.join(__dirname, 'setup-embedder.mjs'), + description: 'Setting up Azure OpenAI embedder in Meilisearch', + }, + { + name: 'Ingest Documents', + script: path.join(__dirname, 'ingest.mjs'), + description: 'Ingesting 768+ documents (this takes 5-10 minutes)', + }, + { + name: 'Configure Synonyms', + script: path.join(__dirname, 'setup-synonyms.mjs'), + description: 'Configuring 32 synonym groups for better search', + }, + { + name: 'Verify Setup', + script: path.join(__dirname, 'check-meili.mjs'), + description: 'Checking Meilisearch index and document count', + }, + ]; + + const startTime = Date.now(); + + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + logStep(i + 1, steps.length, step.name); + log(` ${step.description}`, colors.yellow); + + try { + await runScript(step.script); + log(` ✓ ${step.name} completed`, colors.green); + } catch (error) { + log(` ✗ ${step.name} failed: ${error.message}`, colors.red); + log('\n❌ Setup failed. Please check the error above.\n', colors.red); + process.exit(1); + } + } + + const duration = Math.round((Date.now() - startTime) / 1000); + const minutes = Math.floor(duration / 60); + const seconds = duration % 60; + + log( + '\n╔════════════════════════════════════════════════════════════╗', + colors.green, + ); + log( + '║ ✓ Setup Complete! ║', + colors.green, + ); + log( + '╚════════════════════════════════════════════════════════════╝', + colors.green, + ); + log(`\n Time taken: ${minutes}m ${seconds}s\n`, colors.bright); + log('Next steps:', colors.bright); + log(' 1. Start AI API: pnpm run dev', colors.yellow); + log(' 2. Start frontend: pnpm run www (from repo root)', colors.yellow); + log(' 3. Open browser: http://localhost:5173\n', colors.yellow); +} + +main().catch((error) => { + log(`\n❌ Setup failed: ${error.message}\n`, colors.red); + process.exit(1); +}); diff --git a/apps/ai-api/scripts/test-filtering.mjs b/apps/ai-api/scripts/test-filtering.mjs new file mode 100644 index 0000000000..c7505ceb3e --- /dev/null +++ b/apps/ai-api/scripts/test-filtering.mjs @@ -0,0 +1,123 @@ +#!/usr/bin/env node +// Test the new filterable attributes + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Meilisearch } from 'meilisearch'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// Load environment variables +const envPathCandidates = [ + path.resolve(__dirname, '../../.ai-env'), + path.resolve(__dirname, '../../../apps/.ai-env'), + path.resolve(__dirname, '../../../.ai-env'), +]; + +for (const p of envPathCandidates) { + if (fs.existsSync(p)) { + const lines = fs + .readFileSync(p, 'utf8') + .split(/\r?\n/) + .filter((l) => l.trim() && !l.startsWith('#')); + for (const line of lines) { + const [k, ...rest] = line.split('='); + if (!(k in process.env)) process.env[k] = rest.join('='); + } + console.log(`Loaded env vars from ${p}`); + break; + } +} + +const env = process.env; +const INDEX_NAME = env.MEILISEARCH_PROJECT_NAME || 'designsystemet-search'; + +const meiliClient = new Meilisearch({ + host: env.MEILISEARCH_API_URL, + apiKey: env.MEILISEARCH_ADMIN_KEY, +}); + +async function testFiltering() { + try { + const index = meiliClient.index(INDEX_NAME); + + console.log('🧪 Testing new filterable attributes...\n'); + + // Test 1: Filter by language + console.log('1️⃣ Testing language filtering (lang = "en"):'); + try { + const englishResults = await index.search('', { + limit: 5, + filter: 'lang = en', + }); + console.log( + ` ✅ Found ${englishResults.hits.length} English documents`, + ); + if (englishResults.hits.length > 0) { + console.log(` Sample: "${englishResults.hits[0].title}"`); + } + } catch (err) { + console.log(` ❌ Filter failed: ${err.message}`); + } + + // Test 2: Filter by type + console.log('\n2️⃣ Testing type filtering (type = "component"):'); + try { + const componentResults = await index.search('', { + limit: 5, + filter: 'type = component', + }); + console.log( + ` ✅ Found ${componentResults.hits.length} component documents`, + ); + if (componentResults.hits.length > 0) { + console.log(` Sample: "${componentResults.hits[0].title}"`); + } + } catch (err) { + console.log(` ❌ Filter failed: ${err.message}`); + } + + // Test 3: Filter by URL pattern + console.log('\n3️⃣ Testing URL filtering (url CONTAINS "storybook"):'); + try { + const storybookResults = await index.search('', { + limit: 5, + filter: 'url CONTAINS storybook', + }); + console.log( + ` ✅ Found ${storybookResults.hits.length} Storybook documents`, + ); + if (storybookResults.hits.length > 0) { + console.log(` Sample: "${storybookResults.hits[0].title}"`); + } + } catch (err) { + console.log(` ❌ Filter failed: ${err.message}`); + } + + // Test 4: Combined filtering + console.log( + '\n4️⃣ Testing combined filtering (lang = "en" AND url CONTAINS "storybook"):', + ); + try { + const combinedResults = await index.search('Button', { + limit: 3, + filter: 'lang = en AND url CONTAINS storybook', + }); + console.log( + ` ✅ Found ${combinedResults.hits.length} English Storybook documents matching "Button"`, + ); + combinedResults.hits.forEach((hit, i) => { + console.log(` ${i + 1}. "${hit.title}" (${hit.lang})`); + }); + } catch (err) { + console.log(` ❌ Filter failed: ${err.message}`); + } + + console.log('\n🎉 Filtering tests completed!'); + } catch (error) { + console.error('❌ Test failed:', error); + } +} + +testFiltering(); diff --git a/apps/ai-api/scripts/test-rag-endpoint.mjs b/apps/ai-api/scripts/test-rag-endpoint.mjs new file mode 100755 index 0000000000..5f43a4b2ac --- /dev/null +++ b/apps/ai-api/scripts/test-rag-endpoint.mjs @@ -0,0 +1,83 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import path, { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import dotenv from 'dotenv'; +import fetch from 'node-fetch'; + +// Get current file and directory +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Load environment variables from .ai-env +const envPathCandidates = [ + path.resolve(__dirname, '../../.ai-env'), + path.resolve(__dirname, '../.ai-env'), + path.resolve(__dirname, '.ai-env'), +]; + +for (const p of envPathCandidates) { + if (fs.existsSync(p)) { + dotenv.config({ path: p }); + console.log(`Loaded env vars from ${p}`); + break; + } +} + +async function testRAGEndpoint() { + console.log('Testing RAG API endpoint...'); + + const port = process.env.PORT || 3001; + const baseUrl = `http://localhost:${port}`; + const endpoint = `${baseUrl}/api/ai-search`; + + // Test queries + const queries = [ + 'What is Designsystemet?', + 'How do I use the Button component?', + 'Can you explain the Accordion component?', + 'What color tokens are available?', + ]; + + console.log(`Will send ${queries.length} test queries to: ${endpoint}`); + + for (const query of queries) { + console.log(`\n------------------------------`); + console.log(`Testing query: "${query}"`); + + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query }), + }); + + const result = await response.json(); + + if (response.ok) { + console.log('✅ Successful response:'); + console.log(`Query: ${result.query}`); + console.log(`Response time: ${result.responseTime}ms`); + console.log(`Sources: ${result.sources.length} documents`); + console.log('\nAnswer:'); + console.log(result.answer); + } else { + console.log('❌ Error response:'); + console.log(result); + } + } catch (error) { + console.error('❌ Error executing test:', error.message); + } + + // Wait 1 second between requests + await new Promise((resolve) => setTimeout(resolve, 1000)); + } +} + +// Run the test +testRAGEndpoint() + .then(() => console.log('\nTest completed')) + .catch((err) => console.error('Test failed:', err)); diff --git a/apps/ai-api/scripts/update-index-settings.mjs b/apps/ai-api/scripts/update-index-settings.mjs new file mode 100644 index 0000000000..bbe7ddf4bb --- /dev/null +++ b/apps/ai-api/scripts/update-index-settings.mjs @@ -0,0 +1,124 @@ +#!/usr/bin/env node +// Update Meilisearch index settings to make relevant fields filterable + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Meilisearch } from 'meilisearch'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// Load environment variables +const envPathCandidates = [ + path.resolve(__dirname, '../../.ai-env'), + path.resolve(__dirname, '../../../apps/.ai-env'), + path.resolve(__dirname, '../../../.ai-env'), +]; + +for (const p of envPathCandidates) { + if (fs.existsSync(p)) { + const lines = fs + .readFileSync(p, 'utf8') + .split(/\r?\n/) + .filter((l) => l.trim() && !l.startsWith('#')); + for (const line of lines) { + const [k, ...rest] = line.split('='); + if (!(k in process.env)) process.env[k] = rest.join('='); + } + console.log(`Loaded env vars from ${p}`); + break; + } +} + +const env = process.env; +const INDEX_NAME = env.MEILISEARCH_PROJECT_NAME || 'designsystemet-search'; + +// Initialize client +const meiliClient = new Meilisearch({ + host: env.MEILISEARCH_API_URL, + apiKey: env.MEILISEARCH_ADMIN_KEY, +}); + +async function updateIndexSettings() { + try { + const index = meiliClient.index(INDEX_NAME); + + console.log('🔍 Checking current index settings...'); + + // Get current settings + const currentSettings = await index.getSettings(); + console.log('📋 Current settings:'); + console.log( + ' Filterable attributes:', + currentSettings.filterableAttributes || 'None', + ); + console.log( + ' Searchable attributes:', + currentSettings.searchableAttributes || 'All', + ); + console.log( + ' Sortable attributes:', + currentSettings.sortableAttributes || 'None', + ); + console.log(''); + + // Define the attributes we want to make filterable + const newFilterableAttributes = [ + 'file_path', // Keep existing + 'type', // Enable filtering by document type (component, design-token, etc.) + 'lang', // Enable filtering by language (en, no) + 'url', // Enable filtering by URL patterns + ]; + + // Define sortable attributes (useful for result ranking) + const newSortableAttributes = [ + 'title', // Enable sorting by title + 'file_path', // Enable sorting by file path + ]; + + console.log('🚀 Updating index settings...'); + console.log(' New filterable attributes:', newFilterableAttributes); + console.log(' New sortable attributes:', newSortableAttributes); + + // Update filterable attributes + console.log('📝 Setting filterable attributes...'); + const filterableTask = await index.updateFilterableAttributes( + newFilterableAttributes, + ); + console.log( + `✅ Filterable attributes task queued: ${filterableTask.taskUid}`, + ); + + // Update sortable attributes + console.log('📝 Setting sortable attributes...'); + const sortableTask = await index.updateSortableAttributes( + newSortableAttributes, + ); + console.log(`✅ Sortable attributes task queued: ${sortableTask.taskUid}`); + + console.log(''); + console.log('🎉 Index settings update completed!'); + console.log(''); + console.log('📊 Benefits of these changes:'); + console.log( + ' • Filter by type: filter(type = "component") for components only', + ); + console.log( + ' • Filter by language: filter(lang = "en") for English docs only', + ); + console.log( + ' • Filter by URL patterns: filter(url CONTAINS "storybook") for component docs', + ); + console.log( + ' • Sort results by title or file path for better organization', + ); + console.log(''); + console.log('🧪 Test filtering with the debug script:'); + console.log(' node scripts/debug-search.mjs'); + } catch (error) { + console.error('❌ Failed to update index settings:', error); + process.exit(1); + } +} + +updateIndexSettings(); diff --git a/apps/ai-api/src/server.ts b/apps/ai-api/src/server.ts new file mode 100644 index 0000000000..1203583c48 --- /dev/null +++ b/apps/ai-api/src/server.ts @@ -0,0 +1,533 @@ +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; +import cors from 'cors'; +import dotenv from 'dotenv'; +import express from 'express'; +import { Meilisearch, type SearchResponse } from 'meilisearch'; +import NodeCache from 'node-cache'; + +// Interface for search result objects +interface SearchResult { + title?: string; + url?: string; + content?: string; + type?: string; + _score?: number; + [key: string]: unknown; // Allow for additional properties +} + +// Load environment variables +const envPathCandidates = [ + path.resolve(process.cwd(), '../.ai-env'), + path.resolve(process.cwd(), 'apps/.ai-env'), + path.resolve(process.cwd(), '.ai-env'), +]; + +for (const p of envPathCandidates) { + if (fs.existsSync(p)) { + dotenv.config({ path: p }); + console.log(`Loaded env vars from ${p}`); + break; + } +} + +// Meilisearch client for searching +const meiliSearchClient = new Meilisearch({ + host: process.env.MEILISEARCH_API_URL || '', + apiKey: process.env.MEILISEARCH_SEARCH_KEY || '', // Use search key for frontend-facing API +}); + +// Meilisearch client for admin operations (anything that isnt searching) +const meiliAdminClient = new Meilisearch({ + host: process.env.MEILISEARCH_API_URL || '', + apiKey: process.env.MEILISEARCH_ADMIN_KEY || '', +}); + +// Constants +const INDEX_NAME = + process.env.MEILISEARCH_PROJECT_NAME || 'designsystemet-search'; + +const app = express(); +const port = process.env.PORT || 3001; + +// Middleware +app.use(express.json()); +app.use( + cors({ + origin: process.env.CORS_ALLOWED_ORIGINS + ? process.env.CORS_ALLOWED_ORIGINS.split(',') + : [ + 'http://localhost:3000', + 'http://localhost:5173', + 'https://designsystemet.no', + ], + methods: ['GET', 'POST'], + allowedHeaders: ['Content-Type', 'Authorization'], + }), +); + +// Simple response cache with 30min TTL +const responseCache = new NodeCache({ stdTTL: 1800, checkperiod: 120 }); + +app.use(express.json()); + +// Health check endpoint +app.get('/health', async (_req, res) => { + try { + // Check if Meilisearch is reachable + const health = await meiliSearchClient.health(); + res.status(200).json({ + status: 'ok', + timestamp: new Date().toISOString(), + meilisearch: health.status === 'available' ? 'ok' : 'error', + }); + } catch (error) { + console.error('Health check error:', error); + res.status(500).json({ + status: 'error', + message: 'Service dependencies are not available', + timestamp: new Date().toISOString(), + }); + } +}); + +// Helper functions +async function embedText(text: string) { + if (!text || typeof text !== 'string' || !text.trim()) { + throw new Error('No text provided for embedding'); + } + + const url = `${process.env.AZURE_ENDPOINT}/openai/deployments/${process.env.AZURE_EMBEDDING_DEPLOY_SMALL}/embeddings?api-version=${process.env.AZURE_API_VERSION}`; + + const headers: Record = { + 'Content-Type': 'application/json', + 'api-key': process.env.AZURE_KEY || '', + }; + + const response = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify({ input: text }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Embedding failed: ${response.status} ${response.statusText} - ${errorText}`, + ); + } + + const data = await response.json(); + return data.data[0].embedding; +} + +async function generateChatCompletion( + messages: Array<{ role: string; content: string }>, +) { + const url = `${process.env.AZURE_ENDPOINT}/openai/deployments/${process.env.AZURE_GPT_DEPLOY}/chat/completions?api-version=${process.env.AZURE_API_VERSION}`; + + const headers: Record = { + 'Content-Type': 'application/json', + 'api-key': process.env.AZURE_KEY || '', + }; + + const response = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify({ + messages, + temperature: 0.2, + max_tokens: 800, + top_p: 0.9, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Chat completion failed: ${response.status} ${response.statusText} - ${errorText}`, + ); + } + + return await response.json(); +} + +function formatChatResponse( + content: string, + sources: Array<{ title: string; url: string }> = [], +) { + // Format sources with numbers and URLs + const formattedContent = content; + + // Extract source citations from content [1], [2], etc. + const citations = content.match(/\[\d+\]/g) || []; + + // Generate source information if we have citations and sources + let sourceInfo = ''; + if (citations.length > 0 && sources.length > 0) { + sourceInfo = '\n\nSources:\n'; + for (let i = 0; i < Math.min(citations.length, sources.length); i++) { + sourceInfo += `[${i + 1}] ${sources[i].title} - ${sources[i].url}\n`; + } + } + + return formattedContent + sourceInfo; +} + +/** + * Search function that tries vector search if possible, with fallback to keyword search. + * + * VECTOR SEARCH REQUIREMENTS: + * 1. Meilisearch v1.4+ is required for vector/embedder support + * 2. On Meilisearch Cloud, vector search must be enabled in project settings + * 3. An embedder must be registered (see setup-embedder.mjs script) + * 4. MEILISEARCH_EMBEDDER_UID environment variable should contain the embedder name + */ +async function vectorSearch(query: string, limit = 5) { + try { + // Try to generate embedding for semantic search + let embedding: number[] | null = null; + try { + embedding = await embedText(query); + } catch (embeddingError) { + console.warn( + 'Embedding generation failed, falling back to keyword search:', + embeddingError, + ); + } + + // Search Meilisearch - try vector search first, fall back to basic search if it fails + const index = meiliSearchClient.index(INDEX_NAME); + let searchResponse: SearchResponse; + + if (embedding) { + try { + // Try vector search with embedder + const embedderUid = + process.env.MEILISEARCH_EMBEDDER_UID || 'azure-openai-small'; + + // First, try full hybrid search with vector + embedder + try { + searchResponse = await index.search(query, { + limit: limit, + vector: embedding, + hybrid: { + semanticRatio: 0.7, // 70% vector search, 30% keyword search + embedder: embedderUid, + }, + }); + console.log('Hybrid search successful'); + } catch (hybridError) { + // If embedder-based search fails, try just vector search + console.warn('Hybrid search failed, vector-only:', hybridError); + searchResponse = await index.search(query, { + limit: limit, + vector: embedding, + }); + console.log('Vector-only search successful'); + } + } catch (vectorSearchError) { + console.warn( + 'Vector searches failed, trying basic search:', + vectorSearchError, + ); + searchResponse = await index.search(query, { limit }); + } + } else { + searchResponse = await index.search(query, { limit }); + } + + return searchResponse?.hits || []; + } catch (error) { + console.error('Search error:', error); + return []; + } +} + +// Quick search endpoint (fast, direct results without AI) +app.post('/api/search', async (req, res) => { + const { query } = req.body; + + if (!query || typeof query !== 'string' || !query.trim()) { + return res.status(400).json({ + error: 'Invalid or missing query parameter', + }); + } + + try { + // Get search results + const searchResults = await vectorSearch(query); + + // Deduplicate by URL - keep only the highest scoring result per document + const deduplicatedResults = deduplicateByDocument(searchResults); + // Regex to move language field in URL if present + const langRegex = /^(https?:\/\/[^/]+\/)([^/]+)\/(no|en)\/(.*)$/; + // Format results for quick search + const formattedResults = deduplicatedResults + .slice(0, 8) + .map((doc: SearchResult) => ({ + title: doc.title, + content: + doc.content?.substring(0, 200) + + ((doc.content?.length ?? 0) > 200 ? '...' : ''), + url: doc.url?.replace(langRegex, '$1$3/$2/$4'), + type: doc.type || 'component', + })); + + res.status(200).json({ + query, + results: formattedResults, + count: formattedResults.length, + deduplication: { + original: searchResults.length, + deduplicated: deduplicatedResults.length, + }, + }); + } catch (error: unknown) { + console.error('Quick search error:', error); + res.status(500).json({ + error: 'An error occurred processing your search', + message: + process.env.NODE_ENV === 'development' && error instanceof Error + ? error.message + : undefined, + }); + } +}); + +// Helper function to normalize URLs for deduplication +function normalizeUrl(url: string): string { + return url + .replace('/content/', '/') // Remove /content/ prefix variations + .replace(/\/$/, '') // Remove trailing slash + .toLowerCase() // Case insensitive matching + .replace(/[?#].*$/, ''); // Remove query params and fragments +} + +// Helper function to deduplicate search results by document URL +// Keeps the highest-scoring result/chunk for each normalized URL +function deduplicateByDocument(results: SearchResult[]): SearchResult[] { + const urlMap = new Map(); + + for (const result of results) { + // Don't show test content or content without url + if ( + !result.url || + result.title?.includes('.test') || + result.title?.toLowerCase().includes('test file') || + result.url.includes('/test/') + ) + continue; + + const normalizedUrl = normalizeUrl(result.url); + const existing = urlMap.get(normalizedUrl); + + // Keep the one with higher score, or first if no score available + if ( + !existing || + (result._score && existing._score && result._score > existing._score) || + (result._score && !existing._score) + ) { + urlMap.set(normalizedUrl, result); + } + } + + // Return results sorted by score (highest first) + return Array.from(urlMap.values()).sort( + (a, b) => (b._score || 0) - (a._score || 0), + ); +} + +// AI search endpoint with RAG pipeline +app.post('/api/ai-search', async (req, res) => { + const startTime = Date.now(); + const { query, conversationId = null } = req.body; + + if (!query || typeof query !== 'string' || !query.trim()) { + return res.status(400).json({ + error: 'Invalid or missing query parameter', + }); + } + + try { + // Generate cache key from query and conversation ID + const cacheKey = crypto + .createHash('md5') + .update(`${query}|${conversationId || ''}`) + .digest('hex'); + + // Check cache first + const cachedResponse = responseCache.get(cacheKey); + if (cachedResponse) { + return res.status(200).json({ + ...cachedResponse, + fromCache: true, + }); + } + + // Get search results + let searchResults: Array = []; + try { + searchResults = await vectorSearch(query); + if (searchResults.length > 0) { + console.log('First result:', { + title: searchResults[0].title, + url: searchResults[0].url, + content: searchResults[0].content?.substring(0, 100) + '...', // Log just the beginning + }); + } + } catch (searchError) { + console.error('Vector search error:', searchError); + // Fall back to empty results if search fails completely + searchResults = []; + } + + // Extract and format context from search results + const context = searchResults + .map((doc: SearchResult) => { + return `Title: ${doc.title}\n\nURL: ${doc.url}\n\nContent: ${doc.content}`; + }) + .join('\n\n'); + + console.log(`Context length: ${context.length} characters`); + console.log(`Number of sources: ${searchResults.length}`); + + // Source documents for citations + const sources = searchResults.map((doc: SearchResult) => ({ + title: doc.title, + url: doc.url, + })); + + // Prepare prompt + const messages = [ + { + role: 'system', + content: `You are an AI assistant for Digdir Designsystemet, a Norwegian design system for digital services. + Answer questions about the components, patterns, and guidelines based on the provided context. + Always be helpful, accurate, and concise. + + If the question can be answered using the provided context, use that information and cite your sources using [1], [2], etc. + If there's no relevant context, or the context doesn't contain the information needed, provide a general response based on your knowledge of design systems. + + Use Norwegian (bokmål) for most responses, but you can respond in English if the question is in English. + + Guidelines: + 1. Focus on providing accurate information about the design system's components and patterns. + 2. Include code examples when appropriate, formatted in markdown. + 3. When answering questions about components, mention their purpose, usage, and any important variants. + 4. Do not make up information about the design system that isn't in the provided context. + 5. If you're unsure about something, be honest about your uncertainty. + + Context: + ${context}`, + }, + { + role: 'user', + content: query, + }, + ]; + + // Generate completion + const completion = await generateChatCompletion(messages); + const response = completion.choices[0].message.content; + + // Format response with sources + const formattedResponse = formatChatResponse( + response, + sources as Array<{ title: string; url: string }>, + ); + + // Calculate response time + const responseTime = Date.now() - startTime; + + // Prepare final response object + const finalResponse = { + query, + answer: formattedResponse, + sources: sources, + responseTime, + }; + + // Cache the response + responseCache.set(cacheKey, finalResponse); + + // Send response + res.status(200).json(finalResponse); + } catch (error: unknown) { + console.error('AI search error:', error); + res.status(500).json({ + error: 'An error occurred processing your request', + message: + process.env.NODE_ENV === 'development' && error instanceof Error + ? error.message + : undefined, + }); + } +}); + +// Endpoint to clear cache (admin only) +app.post('/api/admin/clear-cache', (req, res) => { + const authHeader = req.headers.authorization; + const apiKey = authHeader?.split(' ')[1]; // Format: Bearer + + if (apiKey !== process.env.MEILISEARCH_ADMIN_KEY) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + const stats = responseCache.getStats(); + responseCache.flushAll(); + + res.status(200).json({ + message: 'Cache cleared successfully', + previousStats: stats, + }); +}); + +// Endpoint to get index info (admin only) +app.get('/api/admin/index-info', async (req, res) => { + const authHeader = req.headers.authorization; + const apiKey = authHeader?.split(' ')[1]; // Format: Bearer + + if (apiKey !== process.env.MEILISEARCH_ADMIN_KEY) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + try { + const index = meiliAdminClient.index(INDEX_NAME); + const stats = await index.getStats(); + const settings = await index.getSettings(); + + res.status(200).json({ + indexName: INDEX_NAME, + stats, + settings, + }); + } catch (error) { + console.error('Error fetching index info:', error); + res.status(500).json({ + error: 'An error occurred fetching index information', + message: + process.env.NODE_ENV === 'development' && error instanceof Error + ? error.message + : undefined, + }); + } +}); + +// Error handling middleware +app.use((err, _req: express.Request, res: express.Response) => { + console.error('Unhandled error:', err); + res.status(500).json({ + error: 'An unexpected error occurred', + message: + process.env.NODE_ENV === 'development' && err instanceof Error + ? err.message + : undefined, + }); +}); + +// Start the server +app.listen(port, () => { + console.log(`Server running at http://localhost:${port}`); +}); diff --git a/apps/ai-api/tsconfig.json b/apps/ai-api/tsconfig.json new file mode 100644 index 0000000000..594f43cb32 --- /dev/null +++ b/apps/ai-api/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "outDir": "dist", + "sourceMap": true, + "declaration": true, + "resolveJsonModule": true + }, + "include": ["src/**/*", "scripts/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/themebuilder/app/locales/en.ts b/apps/themebuilder/app/locales/en.ts index 19a55cb044..7ee9b71138 100644 --- a/apps/themebuilder/app/locales/en.ts +++ b/apps/themebuilder/app/locales/en.ts @@ -26,6 +26,11 @@ export default { 'go-to-homepage': 'Go to homepage', }, }, + mdx: { + error: { + loading: 'Could not load content', + }, + }, accessibility: { 'skip-link': 'Skip to main content', }, diff --git a/apps/themebuilder/app/locales/no.ts b/apps/themebuilder/app/locales/no.ts index 048d2adeff..44b5a021d3 100644 --- a/apps/themebuilder/app/locales/no.ts +++ b/apps/themebuilder/app/locales/no.ts @@ -25,6 +25,11 @@ export default { 'go-to-homepage': 'Gå til forsiden', }, }, + mdx: { + error: { + loading: 'Kunne ikke laste innhold', + }, + }, accessibility: { 'skip-link': 'Hopp til hovedinnhold', }, diff --git a/apps/themebuilder/tsconfig.json b/apps/themebuilder/tsconfig.json index 2e371d8cf9..e02fd883ae 100644 --- a/apps/themebuilder/tsconfig.json +++ b/apps/themebuilder/tsconfig.json @@ -10,11 +10,7 @@ "exclude": ["**/dist/**/*"], "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2022"], - "types": [ - "node", - "vite/client", - "../../packages/theme/src/themes/types.d.ts" - ], + "types": ["node", "vite/client", "../../packages/theme/brand/types.d.ts"], "target": "ES2022", "module": "ES2022", "moduleResolution": "bundler", diff --git a/apps/www/app/_utils/use-search.ts b/apps/www/app/_utils/use-search.ts new file mode 100644 index 0000000000..a00ea26d51 --- /dev/null +++ b/apps/www/app/_utils/use-search.ts @@ -0,0 +1,147 @@ +import type { QuickResult, SmartResult } from '@internal/components/src/types'; +import { useCallback, useEffect, useRef } from 'react'; +import { useFetcher } from 'react-router'; +import type { action as aiSearchAction } from '~/routes/api/ai-search'; +import type { action as searchAction } from '~/routes/api/search'; + +type PendingRequest = { + resolve: (value: T) => void; + reject: (error: Error) => void; + timeout: NodeJS.Timeout; +}; + +type SearchPromiseReturn = { + success: boolean; + results: QuickResult[]; + query: string; + error?: string; +}; + +type SearchPromise = (query: string) => Promise; + +type AiSearchPromiseReturn = { + success: boolean; + result: SmartResult; + query: string; + error?: string; +}; + +type AiSearchPromise = (query: string) => Promise; + +export const useSearch = () => { + const searchFetcher = useFetcher(); + const aiSearchFetcher = useFetcher(); + const pendingSearchRef = useRef | null>( + null, + ); + const pendingAiSearchRef = + useRef | null>(null); + + useEffect(() => { + if (searchFetcher.state === 'idle' && pendingSearchRef.current) { + clearTimeout(pendingSearchRef.current.timeout); + + if (searchFetcher.data) { + pendingSearchRef.current.resolve(searchFetcher.data); + } else { + pendingSearchRef.current.reject( + new Error('Search request failed or returned no data.'), + ); + } + pendingSearchRef.current = null; + } + }, [searchFetcher.state, searchFetcher.data]); + + useEffect(() => { + if (aiSearchFetcher.state === 'idle' && pendingAiSearchRef.current) { + clearTimeout(pendingAiSearchRef.current.timeout); + + if (aiSearchFetcher.data) { + pendingAiSearchRef.current.resolve(aiSearchFetcher.data); + } else { + pendingAiSearchRef.current.reject( + new Error('AI search request failed or returned no data.'), + ); + } + pendingAiSearchRef.current = null; + } + }, [aiSearchFetcher.state, aiSearchFetcher.data]); + + const handleSearch: SearchPromise = useCallback( + (query: string) => { + if (pendingSearchRef.current) { + clearTimeout(pendingSearchRef.current.timeout); + pendingSearchRef.current.reject(new Error('Request cancelled')); + pendingSearchRef.current = null; + } + + const promise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + if (pendingSearchRef.current) { + pendingSearchRef.current = null; + reject(new Error('Search request timed out')); + } + }, 30000); + + pendingSearchRef.current = { resolve, reject, timeout }; + + searchFetcher.submit( + { query }, + { method: 'post', action: '/api/search' }, + ); + }); + + return promise; + }, + [searchFetcher], + ); + + const handleAiSearch: AiSearchPromise = useCallback( + (query: string) => { + if (pendingAiSearchRef.current) { + clearTimeout(pendingAiSearchRef.current.timeout); + pendingAiSearchRef.current.reject(new Error('Request cancelled')); + pendingAiSearchRef.current = null; + } + + const promise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + if (pendingAiSearchRef.current) { + pendingAiSearchRef.current = null; + reject(new Error('AI search request timed out')); + } + }, 30000); + + pendingAiSearchRef.current = { resolve, reject, timeout }; + + aiSearchFetcher.submit( + { query }, + { method: 'post', action: '/api/ai-search' }, + ); + }); + + return promise; + }, + [aiSearchFetcher], + ); + + useEffect(() => { + return () => { + if (pendingSearchRef.current) { + clearTimeout(pendingSearchRef.current.timeout); + pendingSearchRef.current.reject(new Error('Component unmounted')); + pendingSearchRef.current = null; + } + if (pendingAiSearchRef.current) { + clearTimeout(pendingAiSearchRef.current.timeout); + pendingAiSearchRef.current.reject(new Error('Component unmounted')); + pendingAiSearchRef.current = null; + } + }; + }, []); + + return { + handleSearch, + handleAiSearch, + }; +}; diff --git a/apps/www/app/app.css b/apps/www/app/app.css index 9aec2b4884..991c27b519 100644 --- a/apps/www/app/app.css +++ b/apps/www/app/app.css @@ -30,6 +30,9 @@ --page-spacing-top: var(--ds-size-14); --page-spacing-bottom: var(--ds-size-14); --media-max-width: 800px; + &:has(dialog[open]) { + overflow: hidden; + } } body { @@ -37,6 +40,13 @@ body { font-feature-settings: 'cv05' 1; /* Enable lowercase l with tail */ margin: 0; padding: 0; + &:has(dialog[open]) { + overflow: scroll; + scrollbar-color: transparent transparent; + * { + scrollbar-color: initial; + } + } } /*make each section front page have tinted background */ diff --git a/apps/www/app/layouts/root/layout.tsx b/apps/www/app/layouts/root/layout.tsx index 900a0e1520..4bd8c4e39f 100644 --- a/apps/www/app/layouts/root/layout.tsx +++ b/apps/www/app/layouts/root/layout.tsx @@ -13,6 +13,7 @@ import { useChangeLanguage } from 'remix-i18next/react'; import { Figma } from '~/_components/logos/figma'; import { Github } from '~/_components/logos/github'; import { Slack } from '~/_components/logos/slack'; +import { useSearch } from '~/_utils/use-search'; import i18n from '~/i18n'; import type { Route as RootRoute } from './../../+types/root'; import type { Route } from './+types/layout'; @@ -50,6 +51,7 @@ const rightLinks: FooterLinkListItemProps[] = [ export default function RootLayout() { const { t } = useTranslation(); + const { lang, centerLinks, menu } = useRouteLoaderData('root') as Omit< RootRoute.ComponentProps['loaderData'], 'centerLinks' @@ -63,6 +65,8 @@ export default function RootLayout() { useChangeLanguage(lang); + const { handleSearch, handleAiSearch } = useSearch(); + return ( <> {t('accessibility.skip-link')} @@ -70,6 +74,8 @@ export default function RootLayout() { menu={menu} logoLink={`/${lang === 'no' ? 'no' : lang === 'en' ? 'en' : 'no'}`} themeSwitcher + onSearch={handleSearch} + onAiSearch={handleAiSearch} />
@@ -108,6 +114,7 @@ const ErrorWrapperRoot = ({ rightLinks, }: ErrorWrapperRootProps) => { const { t } = useTranslation(); + const { handleSearch, handleAiSearch } = useSearch(); return ( <> @@ -116,6 +123,8 @@ const ErrorWrapperRoot = ({ menu={menu} logoLink={`/${lang === 'no' ? 'no' : lang === 'en' ? 'en' : 'no'}`} themeSwitcher + onSearch={handleSearch} + onAiSearch={handleAiSearch} />
{children} diff --git a/apps/www/app/routes.ts b/apps/www/app/routes.ts index a8472646d9..72b3805451 100644 --- a/apps/www/app/routes.ts +++ b/apps/www/app/routes.ts @@ -10,6 +10,8 @@ export default [ route('/slack', 'routes/slack.tsx', { id: 'slack-redirect', }), + route('/api/search', 'routes/api/search.ts'), + route('/api/ai-search', 'routes/api/ai-search.ts'), layout('./layouts/root/layout.tsx', [ ...prefix('/:lang', [ index('routes/home/home.tsx', { diff --git a/apps/www/app/routes/api/ai-search.ts b/apps/www/app/routes/api/ai-search.ts new file mode 100644 index 0000000000..7e26c8db0e --- /dev/null +++ b/apps/www/app/routes/api/ai-search.ts @@ -0,0 +1,57 @@ +import { bundleMDX } from 'mdx-bundler'; +import type { ActionFunctionArgs } from 'react-router'; + +type SmartResult = { + content: string; + sources: { title: string; url: string }[]; +}; + +export async function action({ request }: ActionFunctionArgs) { + const formData = await request.formData(); + const query = formData.get('query') as string; + + if (!query) { + return Response.json({ error: 'Query is required', code: null }); + } + + try { + const response = await fetch(`http://localhost:3001/api/ai-search`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: query }), + }); + + if (!response.ok) { + throw new Error(`Search API error: ${response.status}`); + } + const data = await response.json(); + /*TODO: perhaps we should omit "Show more" button and make it always expanded for short answers. + Easiest way is to just check length of data.answer here with a threshold of say 300 characters + and add a boolean to SmartResult + */ + + const { code } = await bundleMDX({ + source: data.answer, + }); + const result: SmartResult = { + content: code || '', + sources: data.sources || [], + }; + + return Response.json({ + success: true as const, + result, + query, + }); + } catch (error) { + console.error('Search error:', error); + return Response.json({ + success: false as const, + error: + error instanceof Error + ? error.message + : 'Failed to process search results', + query, + }); + } +} diff --git a/apps/www/app/routes/api/search.ts b/apps/www/app/routes/api/search.ts new file mode 100644 index 0000000000..f50f763373 --- /dev/null +++ b/apps/www/app/routes/api/search.ts @@ -0,0 +1,51 @@ +import type { ActionFunctionArgs } from 'react-router'; + +type QuickResult = { + title: string; + content: string; + url: string; + type: 'component' | 'guide' | 'pattern' | 'blog'; + sources?: { title: string; url: string }[]; +}; + +export async function action({ request }: ActionFunctionArgs) { + const formData = await request.formData(); + const query = formData.get('query') as string; + + if (!query) { + return Response.json({ error: 'Query is required', code: null }); + } + + let results: QuickResult[] = []; + + try { + const searchResponse = await fetch(`http://localhost:3001/api/search`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: query }), + }); + + if (!searchResponse.ok) { + throw new Error(`Search API error: ${searchResponse.status}`); + } + const data = await searchResponse.json(); + results = data.results || []; + + return Response.json({ + success: true as const, + results, + query, + }); + } catch (error) { + console.error('Search error:', error); + return Response.json({ + success: false as const, + results, + error: + error instanceof Error + ? error.message + : 'Failed to process search results', + query, + }); + } +} diff --git a/infra/meilisearch/.env.example b/infra/meilisearch/.env.example new file mode 100644 index 0000000000..c60ff80434 --- /dev/null +++ b/infra/meilisearch/.env.example @@ -0,0 +1,12 @@ +# Meilisearch Docker Compose environment +# Copy to .env +# Set a long random master key +# Ex. (dont use): ZnG6H2v3K4X5Y6Z7+X8Y9Z0A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6 +MEILI_MASTER_KEY=CHANGE_ME_TO_A_LONG_RANDOM_SECRET + +# Optional; you probably don't need these +MEILI_ENV=production +MEILI_NO_ANALYTICS=trues +# Enable vector search and embedders if you use them +MEILI_EXPERIMENTAL_ENABLE_VECTOR_SEARCH=enabled +MEILI_EXPERIMENTAL_ENABLE_EMBEDDERS=enabled diff --git a/infra/meilisearch/.gitignore b/infra/meilisearch/.gitignore new file mode 100644 index 0000000000..2dcd3b2e4f --- /dev/null +++ b/infra/meilisearch/.gitignore @@ -0,0 +1,2 @@ +# Do not commit secrets +.env diff --git a/infra/meilisearch/README.md b/infra/meilisearch/README.md new file mode 100644 index 0000000000..93950a5762 --- /dev/null +++ b/infra/meilisearch/README.md @@ -0,0 +1,62 @@ +# Meilisearch (Self-Hosted) + +This directory contains a minimal Docker Compose setup for self-hosting Meilisearch with vector search and (optional) embedders API enabled. + +## Prerequisites +- Docker and Docker Compose +- A long random master key (kept outside of git) + +## Setup +1) Copy env template and set your secret master key +```bash +cp infra/meilisearch/.env.example infra/meilisearch/.env +# edit infra/meilisearch/.env and set MEILI_MASTER_KEY to a long random value +``` + +2) Start Meilisearch +```bash +# from repo root +docker compose -f infra/meilisearch/docker-compose.yml up -d +``` +- Service listens on port 7700. Put it behind HTTPS and restrict exposure in production. + +3) Create Admin/Search keys (using the master key) +```bash +# set in your shell for this session only (do not commit) +export MEILI_MASTER_KEY=...your-long-secret... +# create keys; prints Admin and Search keys +pnpm -w -C apps/ai-api run setup:create-keys +``` +Paste the printed Admin/Search keys into `.ai-env` as: +- `MEILISEARCH_ADMIN_KEY` +- `MEILISEARCH_SEARCH_KEY` + +4) Configure app env +```bash +cp .ai-env.example .ai-env +# edit .ai-env and set MEILISEARCH_API_URL, keys, and Azure values +``` + +5) Initialize index and data +```bash +# Only if you use Meili embedders (server-side embedder settings) +pnpm -w -C apps/ai-api run setup:embedder + +# Ingest docs and vectors +pnpm -w -C apps/ai-api run setup:ingest + +# Apply mutual (bidirectional) synonyms +pnpm -w -C apps/ai-api run setup:synonyms +``` + +6) Verify +```bash +pnpm -w -C apps/ai-api run check:meili +pnpm -w -C apps/ai-api run test:smoke +``` + +## Notes +- Keep the SAME embedding model for ingestion and query; do NOT mix models. +- If you recreate the index in the future, rerun `setup:synonyms`. +- For production, bind Meilisearch to localhost and expose via a TLS reverse proxy (e.g., Caddy/Nginx/Traefik). +- Schedule volume snapshots or `dumps` for backups. diff --git a/infra/meilisearch/docker-compose.yml b/infra/meilisearch/docker-compose.yml new file mode 100644 index 0000000000..f87623a450 --- /dev/null +++ b/infra/meilisearch/docker-compose.yml @@ -0,0 +1,23 @@ +services: + meilisearch: + image: getmeili/meilisearch:v1.7 + restart: always + # If you expose publicly, put it behind HTTPS (reverse proxy) and a firewall. + # For local-only binding behind a reverse proxy, uncomment the bind address line below. + ports: + - "7700:7700" # or use "127.0.0.1:7700:7700" to bind locally only + env_file: + - ./.env # copy .env.example -> .env and fill in secrets + environment: + MEILI_MASTER_KEY: ${MEILI_MASTER_KEY:?must be set} + MEILI_ENV: "production" + MEILI_DB_PATH: "/meili_data" + MEILI_NO_ANALYTICS: "true" + MEILI_EXPERIMENTAL_ENABLE_VECTOR_SEARCH: "enabled" + # Enable built-in embedders API if you use setup:embedder + MEILI_EXPERIMENTAL_ENABLE_EMBEDDERS: "enabled" + volumes: + - meili_data:/meili_data + +volumes: + meili_data: diff --git a/internal/components/package.json b/internal/components/package.json index b0be9daf45..7a3ee21838 100644 --- a/internal/components/package.json +++ b/internal/components/package.json @@ -26,6 +26,7 @@ "react-i18next": "^16.0.0", "react-router": "^7.9.3", "@radix-ui/react-slot": "^1.2.3", + "mdx-bundler": "^10.1.1", "react-dom": "^19.2.0" }, "devDependencies": { diff --git a/internal/components/src/header/header.module.css b/internal/components/src/header/header.module.css index 5c9f469965..5c07eaf9ad 100644 --- a/internal/components/src/header/header.module.css +++ b/internal/components/src/header/header.module.css @@ -148,3 +148,14 @@ .toggleButton svg { font-size: 1.5em; } +.searchButton { + border-radius: 1e3px; + --dsc-button-color: var(--ds-color-neutral-text-default); + background: linear-gradient( + 104deg, + var(--ds-color-accent-surface-hover) 16%, + var(--ds-color-warning-surface-hover) 50%, + var(--ds-color-danger-surface-hover) 75% + ) + padding-box; +} diff --git a/internal/components/src/header/header.tsx b/internal/components/src/header/header.tsx index b32204fce3..1394fad5b3 100644 --- a/internal/components/src/header/header.tsx +++ b/internal/components/src/header/header.tsx @@ -6,6 +6,7 @@ import { } from '@digdir/designsystemet-react'; import { LanguageIcon, + MagnifyingGlassIcon, MenuHamburgerIcon, MoonIcon, SunIcon, @@ -22,6 +23,8 @@ import { import { useTranslation } from 'react-i18next'; import { Link, useLocation } from 'react-router'; import { DsEmbledLogo, DsFullLogo } from '../logos/designsystemet'; +import { SearchDialog } from '../search-dialog'; +import type { OnAiSearch, OnSearch } from '../types'; import classes from './header.module.css'; type HeaderProps = { @@ -30,6 +33,9 @@ type HeaderProps = { themeSwitcher?: boolean; transparentBackground?: boolean; logoLink?: string; + onSearch?: OnSearch; + onAiSearch?: OnAiSearch; + className?: string; } & React.HTMLAttributes; /** @@ -64,6 +70,8 @@ const Header = ({ betaTag, themeSwitcher = false, transparentBackground = false, + onSearch, + onAiSearch, logoLink = '/', className, ...props @@ -85,6 +93,7 @@ const Header = ({ const [isHamburger, setIsHamburger] = useState(false); const [viewportWidth, setViewportWidth] = useState(0); const [langOpen, setLangOpen] = useState(false); + const [searchOpen, setSearchOpen] = useState(false); const menuRef = useRef(null); const headerRef = useRef(null); @@ -214,6 +223,15 @@ const Header = ({ ))} + {themeSwitcher && ( + setSearchOpen(false)} + /> ); }; diff --git a/internal/components/src/index.ts b/internal/components/src/index.ts index d904dd1f42..bff6b65b56 100644 --- a/internal/components/src/index.ts +++ b/internal/components/src/index.ts @@ -9,3 +9,4 @@ export type { FooterLinkListItemProps } from './footer/footer'; export { Footer } from './footer/footer'; export { Header } from './header/header'; export * from './logos'; +export { SearchDialog } from './search-dialog'; diff --git a/internal/components/src/mdx-components/mdx-components.module.css b/internal/components/src/mdx-components/mdx-components.module.css new file mode 100644 index 0000000000..5df163e201 --- /dev/null +++ b/internal/components/src/mdx-components/mdx-components.module.css @@ -0,0 +1,8 @@ +.tableWrapper { + overflow-x: auto; + max-width: 100%; + + @media (max-width: 768px) { + max-width: 340px; + } +} diff --git a/internal/components/src/mdx-components/mdx-components.tsx b/internal/components/src/mdx-components/mdx-components.tsx new file mode 100644 index 0000000000..afa8bb982d --- /dev/null +++ b/internal/components/src/mdx-components/mdx-components.tsx @@ -0,0 +1,120 @@ +import { + Card, + Details, + DetailsContent, + DetailsSummary, + Heading, + Link, + ListOrdered, + ListUnordered, + Paragraph, + type ParagraphProps, + Table, + TableBody, + TableCell, + TableFoot, + TableHead, + TableHeaderCell, + type TableProps, + TableRow, +} from '@digdir/designsystemet-react'; +import { getMDXComponent } from 'mdx-bundler/dist/client'; +import { type ComponentType, type JSX, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Link as RRLink } from 'react-router'; +import { CodeBlock } from '../code-block/code-block'; +import classes from './mdx-components.module.css'; + +const defaultComponents = { + Details, + DetailsContent, + DetailsSummary, + Card, + Table: (props: TableProps) => ( +
+ + + ), + TableHead, + TableRow, + TableHeaderCell, + TableBody, + TableFoot, + TableCell, + h1: (props: JSX.IntrinsicElements['h1']) => ( + + ), + h2: (props: JSX.IntrinsicElements['h2']) => ( + + ), + h3: (props: JSX.IntrinsicElements['h3']) => ( + + ), + h4: (props: JSX.IntrinsicElements['h4']) => ( + + ), + h5: (props: JSX.IntrinsicElements['h5']) => ( + + ), + h6: (props: JSX.IntrinsicElements['h6']) => ( + + ), + ol: (props: JSX.IntrinsicElements['ol']) => , + ul: (props: JSX.IntrinsicElements['ul']) => , + p: (props: ParagraphProps) => , + Link: ({ href, ...props }: JSX.IntrinsicElements['a']) => ( + + {props.children} + + ), + a: ({ href, ...props }: JSX.IntrinsicElements['a']) => ( + + {props.children} + + ), + pre: ({ + children: { + props: { children = '', className = '' }, + }, + }) => { + return ( + + {children} + + ); + }, + table: (props: TableProps) => ( +
+
+ + ), +}; + +export const MDXComponents = ({ + components, + code, +}: { + components?: Record>; + code?: string; +}) => { + const { t } = useTranslation(); + const Component = useMemo(() => { + if (!code) return null; + return getMDXComponent(code); + }, [code]); + + return ( + <> + {Component ? ( + + ) : ( + t('mdx.error.loading') + )} + + ); +}; diff --git a/internal/components/src/search-dialog/index.ts b/internal/components/src/search-dialog/index.ts new file mode 100644 index 0000000000..d041e79115 --- /dev/null +++ b/internal/components/src/search-dialog/index.ts @@ -0,0 +1 @@ +export { SearchDialog } from './search-dialog'; diff --git a/internal/components/src/search-dialog/search-dialog.module.css b/internal/components/src/search-dialog/search-dialog.module.css new file mode 100644 index 0000000000..43e971799b --- /dev/null +++ b/internal/components/src/search-dialog/search-dialog.module.css @@ -0,0 +1,320 @@ +.dialog { + --dsc-dialog-background: var(--ds-color-neutral-surface-default); + --this-border: var(--ds-border-width-default) solid var(--ds-color-neutral-border-subtle); + --_smart-collapsed-height: 240px; + width: min(77ch, 90vw); + max-width: unset; + max-height: 90vh; + overflow: visible; + border-radius: var(--ds-border-radius-md); + border: var(--this-border); + padding: 0; + box-shadow: var(--ds-shadow-lg); + transform: translate(-50%, 0); + top: 5vh; + &::backdrop { + opacity: 0.8; + /* always use the dark version of these colors but loose single source of truth */ + --ds-color-accent-surface-tinted: #172f4b; + --ds-color-accent-background-default: #0c1927; + background-image: linear-gradient(180deg, var(--ds-color-accent-surface-tinted), var(--ds-color-accent-background-default)); + } + &[open] { + display: flex; + flex-direction: column; + } + /* close button */ + > button { + position: absolute; + z-index: 1; + align-self: flex-end; + margin: 0; + } + + @media (max-width: 768px) { + width: 100%; + border-radius: 0; + } +} + +.aboveScroll { + border-bottom: var(--this-border); + background-color: var(--dsc-dialog-background); +} + +.searchBlock { + padding: var(--ds-size-6); +} +.title { + margin-bottom: var(--ds-size-6); +} + +.search { + width: 100%; + > input { + border: 3px solid transparent; + --bg: var(--ds-color-neutral-surface-default); + background: + linear-gradient(var(--bg) 0 0) padding-box, + linear-gradient( + 130deg, + var(--ds-color-neutral-border-default) 45%, + color-mix(in srgb, var(--ds-color-neutral-border-default), white 50%), + var(--ds-color-neutral-border-default) 55% + ) + border-box; + background-size: 250%; + animation: --glare 6s infinite ease-in-out; + &:focus { + --bg: var(--ds-color-neutral-background-default); + /*needs more design work*/ + outline-color: light-dark(var(--ds-color-accent-surface-active), transparent); + outline-offset: 0; + outline-width: 5px; + box-shadow: unset; + animation: none; + background: + linear-gradient(var(--bg) 0 0) padding-box, + linear-gradient(90deg, #cb4f53, #e8b43a, var(--ds-color-accent-base-default)) border-box; + background-size: 100%; + } + } +} +@keyframes --glare { + 0%, + 99% { + background-position: 100% 0%; + } + 99%, + 100% { + background-position: 0% 0%; + } +} + +.iconHeading { + --icon-size: 24px; + --gap: var(--ds-size-2); + margin-left: calc((var(--icon-size) + var(--gap)) * -1); + position: relative; + display: flex; + align-items: center; + width: fit-content; + gap: var(--gap); + margin-bottom: var(--ds-size-6); + > svg { + font-size: var(--icon-size); + } +} +.sparkles { + position: absolute; + inset: 0; + pointer-events: none; + will-change: scale; + & svg { + width: 40px; + position: absolute; + --delay: 0.15; + top: calc(var(--y, 50) * 1%); + left: calc(var(--x, 0) * 1%); + translate: -50% -50%; + scale: 0; + /* this animation causes search input to loose focus and I have not been able to figure out how to prevent this */ + /* animation: --sparkle 0.75s calc(0.1s + ((var(--delay) * var(--d)) * 1s)); */ + animation-fill-mode: forwards; + } + & svg:nth-of-type(1) { + fill: var(--ds-color-accent-base-default); + --x: 0; + --y: 40; + --s: 1.1; + --d: 2; + } + & svg:nth-of-type(2) { + fill: #e8b43a; + --x: 50; + --y: 100; + --s: 1.1; + --d: 3; + } + & svg:nth-of-type(3) { + fill: #cb4f53; + --x: 100; + --y: 10; + --s: 1.1; + --d: 4; + } +} + +@keyframes --sparkle { + 50% { + scale: var(--s, 1); + } +} + +/* .iconHeading:has(svg:hover) .sparkles svg { + animation-name: --sparkle-hover; + animation-duration: 0.75s; + animation-delay: calc(0.1s + ((var(--delay) * (var(--d) - 2)) * 1s)); +} */ + +/* @keyframes --sparkle-hover { + 50% { + scale: var(--s, 1); + } +} */ + +.resultsContainer { + overflow-y: auto; +} + +.results { + display: flex; + flex-direction: column; + gap: var(--ds-size-4); +} + +.resultsBlock { + padding: var(--ds-size-6); + padding-left: var(--ds-size-14); + &:empty { + display: none; + } +} + +.loading { + display: flex; + justify-content: center; + align-items: center; + padding: var(--ds-size-8); + color: var(--ds-color-neutral-text-subtle); +} + +.smartContent { + max-height: unset; + margin-bottom: var(--ds-size-4); +} + +@property --search-wipe-progress { + inherits: false; + initial-value: 0%; + syntax: ''; +} + +.smartCollapsed { + max-height: var(--_smart-collapsed-height); + overflow: hidden; + mask-image: linear-gradient(to bottom, white var(--search-wipe-lower), transparent var(--search-wipe-progress)); + --search-wipe-progress: 100%; + --search-wipe-lower: calc(var(--search-wipe-progress) - 40%); + transition: --search-wipe-progress 1s ease; + @starting-style { + --search-wipe-progress: 0%; + } +} + +.smartToggle { + width: 100%; +} + +.smartSkeletonLines { + max-height: var(--_smart-collapsed-height); + :global(.ds-skeleton) { + --dsc-skeleton-background: var(--ds-color-neutral-surface-active); + } +} + +.sources { + margin-top: var(--ds-size-4); + padding-top: var(--ds-size-3); + border-top: var(--this-border); +} + +.sourcesTitle { + color: var(--ds-color-neutral-text-subtle); + margin-bottom: var(--ds-size-2); +} + +.sourcesList { + list-style: none; + margin: 0; + padding: 0; +} + +.sourceItem { + margin-bottom: var(--ds-size-1); +} + +.sourceLink { + color: var(--ds-color-accent-text-default); +} + +.resultHeader { + display: flex; + justify-content: normal; + align-items: center; + gap: var(--ds-size-3); + margin-bottom: var(--ds-size-2); +} + +.resultTitle { + margin: 0; + font-size: var(--ds-font-size-4); + font-weight: var(--ds-font-weight-regular); + color: var(--ds-color-neutral-text-default); +} + +/* Quick search result styles */ +.quickResult { + --transition: 0.4s; + --delay: 0.1s; + @media (prefers-reduced-motion: reduce) { + --transition: 0s; + --delay: 0s; + } + transition: var(--transition) ease; + transition-property: opacity, translate; + transition-delay: calc(var(--i) * var(--delay)); + margin-bottom: var(--ds-size-6); + @starting-style { + opacity: 0; + translate: -16px 0; + } + :global(.ds-skeleton) { + --dsc-skeleton-background: var(--ds-color-neutral-surface-active); + } +} + +.quickResultLink { + margin-bottom: var(--ds-size-3); +} + +.quickResultContent { + margin: 0; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-wrap: pretty; +} +.quickResultUrl { + margin-top: var(--ds-size-3); + color: var(--ds-color-neutral-text-subtle); +} + +.noResults { + text-align: center; + padding: var(--ds-size-8); + color: var(--ds-color-neutral-text-subtle); +} + +.suggestionsTitle { + margin: 0 0 var(--ds-size-3) 0; + font-weight: var(--ds-font-weight-medium); +} + +.suggestionsList { + display: flex; + flex-wrap: wrap; + gap: var(--ds-size-3); +} diff --git a/internal/components/src/search-dialog/search-dialog.tsx b/internal/components/src/search-dialog/search-dialog.tsx new file mode 100644 index 0000000000..0396a328f0 --- /dev/null +++ b/internal/components/src/search-dialog/search-dialog.tsx @@ -0,0 +1,454 @@ +import { + Button, + Chip, + Dialog, + Heading, + Link, + Paragraph, + Search, + Skeleton, + Tag, +} from '@digdir/designsystemet-react'; +import { + ChevronDownIcon, + FileSearchIcon, + RobotSmileIcon, +} from '@navikt/aksel-icons'; +import cl from 'clsx/lite'; +import type { CSSProperties } from 'react'; +import { useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDebounceCallback } from '../_hooks/use-debounce-callback/use-debounce-callback'; +import { MDXComponents } from '../mdx-components/mdx-components'; +import type { OnAiSearch, OnSearch, QuickResult, SmartResult } from '../types'; +import classes from './search-dialog.module.css'; + +type SearchDialogProps = { + open: boolean; + onClose: () => void; + onSearch?: OnSearch; + onAiSearch?: OnAiSearch; +}; + +const Star = () => ( + +); + +export const SearchDialog = ({ + open, + onClose, + onSearch, + onAiSearch, +}: SearchDialogProps) => { + const { t } = useTranslation(); + const [query, setQuery] = useState(''); + const [quickResults, setQuickResults] = useState([]); + const [smartResult, setSmartResult] = useState(null); + const [isQuickLoading, setIsQuickLoading] = useState(false); + const [isSmartLoading, setIsSmartLoading] = useState(false); + const [isTyping, setIsTyping] = useState(false); + const [smartExpanded, setSmartExpanded] = useState(false); + const [visibleQuickCount, setVisibleQuickCount] = useState(8); + const latestQueryRef = useRef(''); + + const performSearch = async (searchQuery: string) => { + if (!searchQuery.trim()) { + setQuickResults([]); + setSmartResult(null); + setIsQuickLoading(false); + setIsSmartLoading(false); + return; + } + + latestQueryRef.current = searchQuery; + const isSingleWord = searchQuery.trim().split(/\s+/).length === 1; + setVisibleQuickCount(8); + setSmartExpanded(false); + setSmartResult(null); + + if (isSingleWord) { + // Quick only + setIsSmartLoading(false); + await handleSearch(searchQuery); + return; + } + + try { + const results = await Promise.allSettled([ + handleSearch(searchQuery), + handleAiSearch(searchQuery), + ]); + + results.forEach((result, index) => { + if (result.status === 'rejected') { + console.error( + `Search error (parallel, index ${index}):`, + result.reason, + ); + } + }); + } catch (e) { + console.error('Search error (parallel):', e); + } + }; + + const debouncedCallback = useDebounceCallback((value: string) => { + performSearch(value); + setIsTyping(false); + }, 1000); + + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setIsTyping(true); + setQuery(value); + debouncedCallback(value); + }; + + const handleClear = () => { + setQuery(''); + setQuickResults([]); + setSmartResult(null); + setIsQuickLoading(false); + setIsSmartLoading(false); + }; + + const handleClose = () => { + setQuery(''); + setQuickResults([]); + setSmartResult(null); + setIsQuickLoading(false); + setIsSmartLoading(false); + onClose(); + }; + + const handleSearch = async (searchQuery: string) => { + if (!onSearch) return Promise.resolve(); + setIsQuickLoading(true); + return onSearch(searchQuery) + .then((response) => { + if (response.success) { + const data = response.results; + if (latestQueryRef.current === searchQuery) { + setQuickResults(data || []); + } + } else { + if (latestQueryRef.current === searchQuery) setQuickResults([]); + } + }) + .catch((error) => { + console.warn('Search (quick):', error); + if (latestQueryRef.current === searchQuery) setQuickResults([]); + }) + .finally(() => { + if (latestQueryRef.current === searchQuery) setIsQuickLoading(false); + }); + }; + + const handleAiSearch = async (searchQuery: string) => { + if (!onAiSearch) return Promise.resolve(); + setIsSmartLoading(true); + return onAiSearch(searchQuery) + .then((response) => { + if (response.success) { + const data = response.result; + if (latestQueryRef.current === searchQuery) { + setSmartResult(data || null); + } + } else { + if (latestQueryRef.current === searchQuery) setSmartResult(null); + } + }) + .catch((error) => { + console.warn('Search (smart):', error); + if (latestQueryRef.current === searchQuery) setSmartResult(null); + }) + .finally(() => { + if (latestQueryRef.current === searchQuery) setIsSmartLoading(false); + }); + }; + + return ( + +
+ + + {t('search.title', 'Hva leter du etter eller lurer på?')} + + + + + + +
+ +
+ {query && ( +
+ {/* Smart answer section (only for 2+ words) */} + {query.trim().split(/\s+/).length > 1 && ( +
+ {isSmartLoading ? ( +
+ + + + + + + + + + + + + + + + + + + + + +
+ ) : smartResult?.content ? ( + <> + + KI-Oversikt{' '} + + + + + + +
+ +
+ + + {smartExpanded && smartResult.sources?.length > 0 && ( + + )} + + ) : null} +
+ )} + + {/* Quick results */} + {isQuickLoading && quickResults.length === 0 && ( +
+
+ + +
+
+ + +
+
+ )} + {quickResults.length > 0 && ( +
+ + Søkeresultater + + {quickResults + .slice(0, visibleQuickCount) + .map((result, index) => ( +
+
+

+ + {result.title} + +

+ + {result.type} + +
+ + {result.content} + + + {result.url} + +
+ ))} + {quickResults.length > visibleQuickCount && ( +
+ +
+ )} +
+ )} +
+ )} + + {!query && ( +
+ + {t('search.suggestions-title', 'Prøv å søke etter...')} + +
+ { + setQuery('Button component'); + performSearch('Button component'); + }} + > + Button component + + + { + setQuery('Design tokens'); + performSearch('Design tokens'); + }} + > + Design tokens + + + { + setQuery('Accessibility guidelines'); + performSearch('Accessibility guidelines'); + }} + > + Accessibility guidelines + + { + setQuery('Hvordan legger jeg til scroll i dropdown?'); + performSearch('Hvordan legger jeg til scroll i dropdown?'); + }} + > + Hvordan legger jeg til scroll i dropdown? + +
+
+ )} + + {query && + !isTyping && + !isQuickLoading && + !isSmartLoading && + quickResults.length === 0 && + !smartResult && ( +
+ {t('search.no-results', 'Ingen resultat for')} "{query}" +
+ )} +
+
+ ); +}; diff --git a/internal/components/src/types.ts b/internal/components/src/types.ts new file mode 100644 index 0000000000..862cea9af1 --- /dev/null +++ b/internal/components/src/types.ts @@ -0,0 +1,26 @@ +export type QuickResult = { + title: string; + content: string; + url: string; + type: 'component' | 'guide' | 'pattern' | 'blog'; + sources?: { title: string; url: string }[]; +}; + +export type SmartResult = { + content: string; + sources: { title: string; url: string }[]; +}; + +export type OnSearch = (query: string) => Promise<{ + success: boolean; + results: QuickResult[]; + query: string; + error?: string; +}>; + +export type OnAiSearch = (query: string) => Promise<{ + success: boolean; + result: SmartResult; + query: string; + error?: string; +}>; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c1eb558fa..fc444fe9e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,7 +52,7 @@ importers: version: 5.9.3 typescript-plugin-css-modules: specifier: ^5.2.0 - version: 5.2.0(typescript@5.9.3) + version: 5.2.0(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.18.8)(typescript@5.9.3))(typescript@5.9.3) vite: specifier: ^7.1.9 version: 7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) @@ -60,6 +60,52 @@ importers: specifier: 3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.8)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + apps/ai-api: + dependencies: + cors: + specifier: ^2.8.5 + version: 2.8.5 + dotenv: + specifier: ^17.2.1 + version: 17.2.3 + express: + specifier: ^5.1.0 + version: 5.1.0 + ioredis: + specifier: ^5.6.1 + version: 5.8.0 + meilisearch: + specifier: ^0.51.0 + version: 0.51.0 + node-cache: + specifier: ^5.1.2 + version: 5.1.2 + node-fetch: + specifier: ^3.3.2 + version: 3.3.2 + devDependencies: + '@types/cors': + specifier: ^2.8.19 + version: 2.8.19 + '@types/express': + specifier: ^5.0.3 + version: 5.0.3 + '@types/node': + specifier: ^24.1.0 + version: 24.6.1 + '@types/node-cache': + specifier: ^4.1.3 + version: 4.2.5 + ts-node-dev: + specifier: ^2.0.0 + version: 2.0.0(@swc/core@1.13.5)(@types/node@24.6.1)(typescript@5.9.3) + tsx: + specifier: ^4.20.3 + version: 4.20.6 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + apps/storybook: dependencies: clsx: @@ -89,22 +135,22 @@ importers: version: 7.31.0(react@19.2.0) '@storybook/addon-a11y': specifier: ^9.1.10 - version: 9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) + version: 9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) '@storybook/addon-docs': specifier: ^9.1.10 - version: 9.1.10(@types/react@19.2.2)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) + version: 9.1.10(@types/react@19.2.2)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) '@storybook/addon-links': specifier: ^9.1.10 - version: 9.1.10(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) + version: 9.1.10(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) '@storybook/addon-themes': specifier: ^9.1.10 - version: 9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) + version: 9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) '@storybook/addon-vitest': specifier: ^9.1.10 - version: 9.1.10(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(vitest@3.2.4) + version: 9.1.10(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(vitest@3.2.4) '@storybook/react-vite': specifier: ^9.1.10 - version: 9.1.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.52.4)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(typescript@5.9.3)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + version: 9.1.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.52.4)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(typescript@5.9.3)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) '@types/ramda': specifier: ^0.31.1 version: 0.31.1 @@ -116,7 +162,7 @@ importers: version: 19.2.1(@types/react@19.2.2) '@vitest/browser': specifier: 3.2.4 - version: 3.2.4(playwright@1.55.1)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))(vitest@3.2.4) + version: 3.2.4(playwright@1.55.1)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))(vitest@3.2.4) '@vitest/coverage-v8': specifier: 3.2.4 version: 3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4) @@ -140,13 +186,13 @@ importers: version: 4.0.1 storybook: specifier: ^9.1.10 - version: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + version: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) storybook-addon-pseudo-states: specifier: ^9.1.10 - version: 9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) + version: 9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) vitest: specifier: 3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.8)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.6.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) apps/themebuilder: dependencies: @@ -365,6 +411,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + mdx-bundler: + specifier: ^10.1.1 + version: 10.1.1(acorn@8.15.0)(esbuild@0.25.9) react: specifier: ^19.2.0 version: 19.2.0 @@ -578,13 +627,13 @@ importers: version: 16.0.2(rollup@4.52.4) '@storybook/addon-docs': specifier: ^9.1.10 - version: 9.1.10(@types/react@19.2.2)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) + version: 9.1.10(@types/react@19.2.2)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) '@storybook/addon-vitest': specifier: ^9.1.10 - version: 9.1.10(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(vitest@3.2.4) + version: 9.1.10(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(vitest@3.2.4) '@storybook/react-vite': specifier: ^9.1.10 - version: 9.1.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.52.4)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(typescript@5.9.3)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + version: 9.1.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.52.4)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(typescript@5.9.3)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 @@ -620,7 +669,7 @@ importers: version: 3.5.0 storybook: specifier: ^9.1.10 - version: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + version: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) tsx: specifier: 4.20.6 version: 4.20.6 @@ -681,7 +730,7 @@ importers: version: 19.2.1(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^5.0.4 - version: 5.0.4(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + version: 5.0.4(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) postcss: specifier: ^8.5.6 version: 8.5.6 @@ -693,13 +742,13 @@ importers: version: 5.9.3 vite: specifier: ^7.1.9 - version: 7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + version: 7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) vite-plugin-react-rich-svg: specifier: ^1.3.0 - version: 1.3.0(typescript@5.9.3)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + version: 1.3.0(typescript@5.9.3)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) vite-plugin-singlefile: specifier: ^2.3.0 - version: 2.3.0(rollup@4.52.4)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + version: 2.3.0(rollup@4.52.4)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) zustand: specifier: ^5.0.8 version: 5.0.8(@types/react@19.2.2)(react@19.2.0) @@ -1059,6 +1108,10 @@ packages: peerDependencies: commander: ~14.0.0 + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@csstools/color-helpers@5.0.2': resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} engines: {node: '>=18'} @@ -1446,6 +1499,9 @@ packages: '@types/node': optional: true + '@ioredis/commands@1.4.0': + resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==} + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -1504,6 +1560,9 @@ packages: '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@jsonjoy.com/base64@1.1.2': resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} engines: {node: '>=10.0'} @@ -2437,6 +2496,18 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/apca-w3@0.1.3': resolution: {integrity: sha512-1C7e66xWCy1UOXHk/VXs4tjrY9hW3q3sykjJJxSC5TIIFt0lt9mh0ysdQxY0rVejTv4SW92G19j98PjqjCSz7Q==} @@ -2455,12 +2526,21 @@ packages: '@types/babel__traverse@7.20.7': resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} '@types/chroma-js@3.1.1': resolution: {integrity: sha512-SFCr4edNkZ1bGaLzGz7rgR1bRzVX4MmMxwsIa3/Bh6ose8v+hRpneoizHv0KChdjxaXyjRtaMq7sCuZSzPomQA==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -2479,6 +2559,12 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@5.0.7': + resolution: {integrity: sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==} + + '@types/express@5.0.3': + resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} + '@types/fs-extra@11.0.4': resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} @@ -2491,6 +2577,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/jsonfile@6.1.4': resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} @@ -2503,18 +2592,28 @@ packages: '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/minimatch@5.1.2': resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node-cache@4.2.5': + resolution: {integrity: sha512-faK2Owokboz53g8ooq2dw3iDJ6/HMTCIa2RvMte5WMTiABy+wA558K+iuyRtlR67Un5q9gEKysSDtqZYbSa0Pg==} + deprecated: This is a stub types definition. node-cache provides its own type definitions, so you do not need this installed. + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} '@types/node@22.18.8': resolution: {integrity: sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==} + '@types/node@24.6.1': + resolution: {integrity: sha512-ljvjjs3DNXummeIaooB4cLBKg2U6SPI6Hjra/9rRIy7CpM0HpLtG9HptkMKAb4HYWy5S7HUvJEuWgr/y0U8SHw==} + '@types/object-hash@3.0.6': resolution: {integrity: sha512-fOBV8C1FIu2ELinoILQ+ApxcUKz4ngq+IWUYrxSGjXzzjUALijilampwkMgEtJ+h2njAW3pi853QpzNVCHB73w==} @@ -2530,9 +2629,15 @@ packages: '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + '@types/ramda@0.31.1': resolution: {integrity: sha512-Vt6sFXnuRpzaEj+yeutA0q3bcAsK7wdPuASIzR9LXqL4gJPyFw8im9qchlbp4ltuf3kDEIRmPJTD/Fkg60dn7g==} + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-color@3.0.13': resolution: {integrity: sha512-2c/9FZ4ixC5T3JzN0LP5Cke2Mf0MKOP2Eh0NPDPWmuVH3NjPyhEjqNMQpN1Phr5m74egAy+p2lYNAFrX1z9Yrg==} peerDependencies: @@ -2565,6 +2670,18 @@ packages: '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} + '@types/send@0.17.5': + resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} + + '@types/serve-static@1.15.8': + resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} + + '@types/strip-bom@3.0.0': + resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} + + '@types/strip-json-comments@0.0.30': + resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -2668,15 +2785,18 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} - hasBin: true acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} @@ -2721,6 +2841,9 @@ packages: apca-w3@0.1.9: resolution: {integrity: sha512-Zrf6AeBeQjNe/fxK7U1jCo5zfdjDl6T4/kdw5Xlky3G7u+EJTZkyItjMYQGtwf9pkftsINxcYyOpuLkzKf1ITQ==} + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -2820,6 +2943,10 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -2960,10 +3087,18 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} @@ -3044,6 +3179,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -3054,6 +3193,10 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.7.1: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} @@ -3065,6 +3208,10 @@ packages: copy-anything@2.0.6: resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + cosmiconfig@8.3.6: resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} @@ -3074,6 +3221,9 @@ packages: typescript: optional: true + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -3140,6 +3290,10 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} @@ -3207,6 +3361,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -3239,6 +3397,10 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -3273,6 +3435,10 @@ packages: resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} engines: {node: '>=12'} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + dotenv@8.6.0: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} engines: {node: '>=10'} @@ -3281,6 +3447,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + dynamic-dedupe@0.3.0: + resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -3439,6 +3608,10 @@ packages: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -3476,6 +3649,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -3487,6 +3664,10 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -3513,6 +3694,10 @@ packages: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -3524,6 +3709,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-extra@11.3.2: resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} engines: {node: '>=14.14'} @@ -3719,6 +3908,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + icss-utils@5.1.0: resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} @@ -3761,6 +3954,10 @@ packages: inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + ioredis@5.8.0: + resolution: {integrity: sha512-AUXbKn9gvo9hHKvk6LbZJQSKn/qIfkWXrnsyL9Yrf+oeXmla9Nmf6XEumOddyhM8neynpK5oAV6r9r99KBuwzA==} + engines: {node: '>=12.22.0'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -3846,6 +4043,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -4078,6 +4278,12 @@ packages: lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} @@ -4145,6 +4351,9 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + markdown-extensions@2.0.0: resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} engines: {node: '>=16'} @@ -4226,6 +4435,13 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + meilisearch@0.51.0: + resolution: {integrity: sha512-IuNsYyT8r/QLhU33XDZdXWUT6cA/nACCHHZc+NHkNuaU55LELRxff1uBJhpJcwjYaAPEEmeWrzBhYl2XlhJdAg==} + memfs@4.17.1: resolution: {integrity: sha512-thuTRd7F4m4dReCIy7vv4eNYnU6XI/tHMLSMMHLiortw/Y0QxqKtinG523U2aerzwYWGi606oBP4oMPy4+edag==} engines: {node: '>= 4.0.0'} @@ -4233,6 +4449,10 @@ packages: merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -4365,6 +4585,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -4396,6 +4620,11 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} @@ -4442,12 +4671,25 @@ packages: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-cache@5.1.2: + resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==} + engines: {node: '>= 8.0.0'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -4457,6 +4699,10 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} @@ -4626,6 +4872,9 @@ packages: path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -5050,6 +5299,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@3.0.1: + resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} + engines: {node: '>= 0.10'} + react-code-block@1.1.3: resolution: {integrity: sha512-XfSb9BkJWt4j7FvcVMmgQxIs61g81R0y0bxoemrVPo6/v0kS61PL3S1MWWtHl2g67aRPJeoTqPa4D91v/DzMdQ==} peerDependencies: @@ -5176,6 +5429,14 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + rehype-autolink-headings@7.1.0: resolution: {integrity: sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==} @@ -5246,6 +5507,11 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + rimraf@6.0.1: resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} engines: {node: 20 || >=22} @@ -5270,6 +5536,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} @@ -5331,10 +5601,18 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + serve-static@1.16.2: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + set-cookie-parser@2.7.1: resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} @@ -5438,6 +5716,9 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -5500,6 +5781,10 @@ packages: resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} engines: {node: '>=12'} + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + strip-literal@3.0.0: resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} @@ -5680,6 +5965,31 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-node-dev@2.0.0: + resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==} + engines: {node: '>=0.8.0'} + hasBin: true + peerDependencies: + node-notifier: '*' + typescript: '*' + peerDependenciesMeta: + node-notifier: + optional: true + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + ts-toolbelt@9.6.0: resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} @@ -5707,6 +6017,9 @@ packages: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} + tsconfig@7.0.0: + resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -5741,6 +6054,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + types-ramda@0.31.0: resolution: {integrity: sha512-vaoC35CRC3xvL8Z6HkshDbi6KWM1ezK0LHN0YyxXWUn9HKzBNg/T3xSGlJZjCYspnOD3jE7bcizsp0bUXZDxnQ==} @@ -5760,6 +6077,9 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.13.0: + resolution: {integrity: sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==} + unicorn-magic@0.1.0: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} @@ -5833,6 +6153,9 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + valibot@0.41.0: resolution: {integrity: sha512-igDBb8CTYr8YTQlOKgaN9nSS0Be7z+WRuaeYqGf3Cjz3aKmSnqEmYnkfVjzIuumGqfHpa3fLIvMEAfhrpqN8ng==} peerDependencies: @@ -5972,6 +6295,10 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -6056,6 +6383,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -6080,6 +6411,10 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + yocto-queue@1.2.1: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} @@ -6654,6 +6989,10 @@ snapshots: dependencies: commander: 14.0.1 + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + '@csstools/color-helpers@5.0.2': optional: true @@ -6900,6 +7239,8 @@ snapshots: optionalDependencies: '@types/node': 22.18.8 + '@ioredis/commands@1.4.0': {} + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -6917,12 +7258,12 @@ snapshots: '@istanbuljs/schema@0.1.3': {} - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.9.3)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.9.3)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))': dependencies: glob: 10.4.5 magic-string: 0.30.17 react-docgen-typescript: 2.4.0(typescript@5.9.3) - vite: 7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + vite: 7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) optionalDependencies: typescript: 5.9.3 @@ -6966,6 +7307,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': dependencies: tslib: 2.8.1 @@ -7501,62 +7847,62 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.52.4': optional: true - '@storybook/addon-a11y@9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))': + '@storybook/addon-a11y@9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.10.3 - storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) - '@storybook/addon-docs@9.1.10(@types/react@19.2.2)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))': + '@storybook/addon-docs@9.1.10(@types/react@19.2.2)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))': dependencies: '@mdx-js/react': 3.1.0(@types/react@19.2.2)(react@19.2.0) - '@storybook/csf-plugin': 9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) + '@storybook/csf-plugin': 9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) '@storybook/icons': 1.4.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@storybook/react-dom-shim': 9.1.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) + '@storybook/react-dom-shim': 9.1.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-links@9.1.10(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))': + '@storybook/addon-links@9.1.10(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))': dependencies: '@storybook/global': 5.0.0 - storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) optionalDependencies: react: 19.2.0 - '@storybook/addon-themes@9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))': + '@storybook/addon-themes@9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))': dependencies: - storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) ts-dedent: 2.2.0 - '@storybook/addon-vitest@9.1.10(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(vitest@3.2.4)': + '@storybook/addon-vitest@9.1.10(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(vitest@3.2.4)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 1.4.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) prompts: 2.4.2 - storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) ts-dedent: 2.2.0 optionalDependencies: - '@vitest/browser': 3.2.4(playwright@1.55.1)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.55.1)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))(vitest@3.2.4) '@vitest/runner': 3.2.4 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.8)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.6.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) transitivePeerDependencies: - react - react-dom - '@storybook/builder-vite@9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))': + '@storybook/builder-vite@9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))': dependencies: - '@storybook/csf-plugin': 9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) - storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + '@storybook/csf-plugin': 9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) + storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) ts-dedent: 2.2.0 - vite: 7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + vite: 7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) - '@storybook/csf-plugin@9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))': + '@storybook/csf-plugin@9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))': dependencies: - storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) unplugin: 1.16.1 '@storybook/global@5.0.0': {} @@ -7566,39 +7912,39 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - '@storybook/react-dom-shim@9.1.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))': + '@storybook/react-dom-shim@9.1.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))': dependencies: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) - '@storybook/react-vite@9.1.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.52.4)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(typescript@5.9.3)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))': + '@storybook/react-vite@9.1.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.52.4)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(typescript@5.9.3)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.9.3)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.9.3)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) '@rollup/pluginutils': 5.1.4(rollup@4.52.4) - '@storybook/builder-vite': 9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) - '@storybook/react': 9.1.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(typescript@5.9.3) + '@storybook/builder-vite': 9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + '@storybook/react': 9.1.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(typescript@5.9.3) find-up: 7.0.0 magic-string: 0.30.17 react: 19.2.0 react-docgen: 8.0.0 react-dom: 19.2.0(react@19.2.0) resolve: 1.22.10 - storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) tsconfig-paths: 4.2.0 - vite: 7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + vite: 7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) transitivePeerDependencies: - rollup - supports-color - typescript - '@storybook/react@9.1.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(typescript@5.9.3)': + '@storybook/react@9.1.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)))(typescript@5.9.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 9.1.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) + '@storybook/react-dom-shim': 9.1.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) optionalDependencies: typescript: 5.9.3 @@ -7763,7 +8109,7 @@ snapshots: '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.27.6 '@types/aria-query': 5.0.4 aria-query: 5.3.0 chalk: 4.1.2 @@ -7833,6 +8179,14 @@ snapshots: '@trysound/sax@0.2.0': {} + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + '@types/apca-w3@0.1.3': {} '@types/aria-query@5.0.4': {} @@ -7858,12 +8212,25 @@ snapshots: dependencies: '@babel/types': 7.28.4 + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 24.6.1 + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 '@types/chroma-js@3.1.1': {} + '@types/connect@3.4.38': + dependencies: + '@types/node': 24.6.1 + + '@types/cors@2.8.19': + dependencies: + '@types/node': 24.6.1 + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -7880,6 +8247,19 @@ snapshots: '@types/estree@1.0.8': {} + '@types/express-serve-static-core@5.0.7': + dependencies: + '@types/node': 24.6.1 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.5 + + '@types/express@5.0.3': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.0.7 + '@types/serve-static': 1.15.8 + '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 @@ -7898,6 +8278,8 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/http-errors@2.0.5': {} + '@types/jsonfile@6.1.4': dependencies: '@types/node': 22.18.8 @@ -7910,16 +8292,26 @@ snapshots: '@types/mdx@2.0.13': {} + '@types/mime@1.3.5': {} + '@types/minimatch@5.1.2': {} '@types/ms@2.1.0': {} + '@types/node-cache@4.2.5': + dependencies: + node-cache: 5.1.2 + '@types/node@12.20.55': {} '@types/node@22.18.8': dependencies: undici-types: 6.21.0 + '@types/node@24.6.1': + dependencies: + undici-types: 7.13.0 + '@types/object-hash@3.0.6': {} '@types/postcss-modules-local-by-default@4.0.2': @@ -7934,10 +8326,14 @@ snapshots: '@types/prop-types@15.7.15': {} + '@types/qs@6.14.0': {} + '@types/ramda@0.31.1': dependencies: types-ramda: 0.31.0 + '@types/range-parser@1.2.7': {} + '@types/react-color@3.0.13(@types/react@19.2.2)': dependencies: '@types/react': 19.2.2 @@ -7968,6 +8364,21 @@ snapshots: '@types/resolve@1.20.6': {} + '@types/send@0.17.5': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 24.6.1 + + '@types/serve-static@1.15.8': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 24.6.1 + '@types/send': 0.17.5 + + '@types/strip-bom@3.0.0': {} + + '@types/strip-json-comments@0.0.30': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -7988,7 +8399,7 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@5.0.4(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))': + '@vitejs/plugin-react@5.0.4(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) @@ -7996,7 +8407,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.38 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + vite: 7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) transitivePeerDependencies: - supports-color @@ -8032,6 +8443,26 @@ snapshots: - msw - utf-8-validate - vite + optional: true + + '@vitest/browser@3.2.4(playwright@1.55.1)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))(vitest@3.2.4)': + dependencies: + '@testing-library/dom': 10.4.0 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) + '@vitest/mocker': 3.2.4(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + '@vitest/utils': 3.2.4 + magic-string: 0.30.17 + sirv: 3.0.1 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.6.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + ws: 8.18.2 + optionalDependencies: + playwright: 1.55.1 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite '@vitest/coverage-v8@3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4)': dependencies: @@ -8070,6 +8501,14 @@ snapshots: optionalDependencies: vite: 7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + '@vitest/mocker@3.2.4(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 @@ -8114,11 +8553,18 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 - acorn@8.14.1: {} + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 acorn@8.15.0: {} @@ -8150,6 +8596,8 @@ snapshots: dependencies: colorparsley: 0.1.8 + arg@4.1.3: {} + arg@5.0.2: {} argparse@1.0.10: @@ -8266,6 +8714,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.1 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.1 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + boolbase@1.0.0: {} brace-expansion@1.1.11: @@ -8402,8 +8864,12 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + clone@2.1.2: {} + clsx@2.1.1: {} + cluster-key-slot@1.1.2: {} + collapse-white-space@2.1.0: {} color-convert@2.0.1: @@ -8467,12 +8933,18 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + content-type@1.0.5: {} convert-source-map@2.0.0: {} cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} + cookie@0.7.1: {} cookie@1.0.2: {} @@ -8481,6 +8953,11 @@ snapshots: dependencies: is-what: 3.14.1 + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 @@ -8490,6 +8967,8 @@ snapshots: optionalDependencies: typescript: 5.9.3 + create-require@1.1.1: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -8585,6 +9064,8 @@ snapshots: csstype@3.1.3: {} + data-uri-to-buffer@4.0.1: {} + data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 @@ -8632,6 +9113,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + denque@2.1.0: {} + depd@2.0.0: {} dependency-graph@1.0.0: {} @@ -8652,6 +9135,8 @@ snapshots: dependencies: dequal: 2.0.3 + diff@4.0.2: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -8689,6 +9174,8 @@ snapshots: dotenv@16.5.0: {} + dotenv@17.2.3: {} + dotenv@8.6.0: {} dunder-proto@1.0.1: @@ -8697,6 +9184,10 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + dynamic-dedupe@0.3.0: + dependencies: + xtend: 4.0.2 + eastasianwidth@0.2.0: {} ee-first@1.1.1: {} @@ -8919,6 +9410,38 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.2.2 + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extend-shallow@2.0.1: dependencies: is-extendable: 0.1.1 @@ -8951,6 +9474,11 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + fflate@0.8.2: {} fill-range@7.1.1: @@ -8969,6 +9497,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.0: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -8999,12 +9538,18 @@ snapshots: format@0.2.2: {} + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + forwarded@0.2.0: {} fraction.js@4.3.7: {} fresh@0.5.2: {} + fresh@2.0.0: {} + fs-extra@11.3.2: dependencies: graceful-fs: 4.2.11 @@ -9272,6 +9817,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + icss-utils@5.1.0(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -9303,6 +9852,20 @@ snapshots: inline-style-parser@0.2.4: {} + ioredis@5.8.0: + dependencies: + '@ioredis/commands': 1.4.0 + cluster-key-slot: 1.1.2 + debug: 4.4.1 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ipaddr.js@1.9.1: {} is-alphabetical@2.0.1: {} @@ -9370,6 +9933,8 @@ snapshots: is-potential-custom-element-name@1.0.1: optional: true + is-promise@4.0.0: {} + is-reference@1.2.1: dependencies: '@types/estree': 1.0.7 @@ -9595,6 +10160,10 @@ snapshots: lodash.camelcase@4.3.0: {} + lodash.defaults@4.2.0: {} + + lodash.isarguments@3.1.0: {} + lodash.memoize@4.1.2: {} lodash.sortby@4.7.0: {} @@ -9655,6 +10224,8 @@ snapshots: dependencies: semver: 7.7.1 + make-error@1.3.6: {} + markdown-extensions@2.0.0: {} markdown-table@3.0.4: {} @@ -9843,7 +10414,7 @@ snapshots: mdx-bundler@10.1.1(acorn@8.15.0)(esbuild@0.25.9): dependencies: - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.27.6 '@esbuild-plugins/node-resolve': 0.2.2(esbuild@0.25.9) '@fal-works/esbuild-plugin-global-externals': 2.1.2 '@mdx-js/esbuild': 3.1.0(acorn@8.15.0)(esbuild@0.25.9) @@ -9859,6 +10430,10 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + + meilisearch@0.51.0: {} + memfs@4.17.1: dependencies: '@jsonjoy.com/json-pack': 1.2.0(tslib@2.8.1) @@ -9868,6 +10443,8 @@ snapshots: merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} methods@1.1.2: {} @@ -10156,6 +10733,10 @@ snapshots: dependencies: mime-db: 1.52.0 + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} min-indent@1.0.1: {} @@ -10180,9 +10761,11 @@ snapshots: minipass@7.1.2: {} + mkdirp@1.0.4: {} + mlly@1.7.4: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 pathe: 2.0.3 pkg-types: 1.3.1 ufo: 1.6.1 @@ -10225,6 +10808,8 @@ snapshots: negotiator@0.6.4: {} + negotiator@1.0.0: {} + no-case@3.0.4: dependencies: lower-case: 2.0.2 @@ -10233,10 +10818,22 @@ snapshots: node-addon-api@7.1.1: optional: true + node-cache@5.1.2: + dependencies: + clone: 2.1.2 + + node-domexception@1.0.0: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-releases@2.0.19: {} normalize-package-data@5.0.0: @@ -10403,6 +11000,8 @@ snapshots: path-to-regexp@0.1.12: {} + path-to-regexp@8.3.0: {} + path-type@4.0.0: {} path-unified@0.2.0: {} @@ -10521,12 +11120,13 @@ snapshots: read-cache: 1.0.0 resolve: 1.22.10 - postcss-load-config@3.1.4(postcss@8.5.6): + postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.18.8)(typescript@5.9.3)): dependencies: lilconfig: 2.1.0 yaml: 1.10.2 optionalDependencies: postcss: 8.5.6 + ts-node: 10.9.2(@swc/core@1.13.5)(@types/node@22.18.8)(typescript@5.9.3) postcss-load-config@5.1.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.6): dependencies: @@ -10773,6 +11373,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.1: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.7.0 + unpipe: 1.0.0 + react-code-block@1.1.3(prism-react-renderer@2.4.1(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: prism-react-renderer: 2.4.1(react@19.2.0) @@ -10917,6 +11524,12 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + rehype-autolink-headings@7.1.0: dependencies: '@types/hast': 3.0.4 @@ -11028,6 +11641,10 @@ snapshots: reusify@1.1.0: {} + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + rimraf@6.0.1: dependencies: glob: 11.0.2 @@ -11123,6 +11740,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.52.4 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.1 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + rrweb-cssom@0.8.0: optional: true @@ -11196,6 +11823,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.0: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + serve-static@1.16.2: dependencies: encodeurl: 2.0.0 @@ -11205,6 +11848,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + set-cookie-parser@2.7.1: {} set-function-length@1.2.2: @@ -11313,21 +11965,23 @@ snapshots: stackback@0.0.2: {} + standard-as-callback@2.1.0: {} + statuses@2.0.1: {} std-env@3.9.0: {} - storybook-addon-pseudo-states@9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))): + storybook-addon-pseudo-states@9.1.10(storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))): dependencies: - storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + storybook: 9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) - storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)): + storybook@9.1.10(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)): dependencies: '@storybook/global': 5.0.0 '@testing-library/jest-dom': 6.9.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + '@vitest/mocker': 3.2.4(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) '@vitest/spy': 3.2.4 better-opn: 3.0.2 esbuild: 0.25.9 @@ -11390,6 +12044,8 @@ snapshots: dependencies: min-indent: 1.0.1 + strip-json-comments@2.0.1: {} + strip-literal@3.0.0: dependencies: js-tokens: 9.0.1 @@ -11584,6 +12240,65 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-node-dev@2.0.0(@swc/core@1.13.5)(@types/node@24.6.1)(typescript@5.9.3): + dependencies: + chokidar: 3.6.0 + dynamic-dedupe: 0.3.0 + minimist: 1.2.8 + mkdirp: 1.0.4 + resolve: 1.22.10 + rimraf: 2.7.1 + source-map-support: 0.5.21 + tree-kill: 1.2.2 + ts-node: 10.9.2(@swc/core@1.13.5)(@types/node@24.6.1)(typescript@5.9.3) + tsconfig: 7.0.0 + typescript: 5.9.3 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - '@types/node' + + ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.18.8)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.18.8 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.13.5 + optional: true + + ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.6.1)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 24.6.1 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.13.5 + ts-toolbelt@9.6.0: {} tsconfck@3.1.5(typescript@5.9.3): @@ -11600,6 +12315,13 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tsconfig@7.0.0: + dependencies: + '@types/strip-bom': 3.0.0 + '@types/strip-json-comments': 0.0.30 + strip-bom: 3.0.0 + strip-json-comments: 2.0.1 + tslib@2.8.1: {} tsup@8.5.0(@swc/core@1.13.5)(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.7.1): @@ -11646,11 +12368,17 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + types-ramda@0.31.0: dependencies: ts-toolbelt: 9.6.0 - typescript-plugin-css-modules@5.2.0(typescript@5.9.3): + typescript-plugin-css-modules@5.2.0(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.18.8)(typescript@5.9.3))(typescript@5.9.3): dependencies: '@types/postcss-modules-local-by-default': 4.0.2 '@types/postcss-modules-scope': 3.0.4 @@ -11659,7 +12387,7 @@ snapshots: less: 4.3.0 lodash.camelcase: 4.3.0 postcss: 8.5.6 - postcss-load-config: 3.1.4(postcss@8.5.6) + postcss-load-config: 3.1.4(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.18.8)(typescript@5.9.3)) postcss-modules-extract-imports: 3.1.0(postcss@8.5.6) postcss-modules-local-by-default: 4.2.0(postcss@8.5.6) postcss-modules-scope: 3.2.1(postcss@8.5.6) @@ -11680,6 +12408,8 @@ snapshots: undici-types@6.21.0: {} + undici-types@7.13.0: {} + unicorn-magic@0.1.0: {} unified@11.0.5: @@ -11769,6 +12499,8 @@ snapshots: uuid@9.0.1: {} + v8-compile-cache-lib@3.0.1: {} + valibot@0.41.0(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 @@ -11826,23 +12558,44 @@ snapshots: - tsx - yaml - vite-plugin-react-rich-svg@1.3.0(typescript@5.9.3)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)): + vite-node@3.2.4(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-plugin-react-rich-svg@1.3.0(typescript@5.9.3)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)): dependencies: '@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) '@svgr/plugin-prettier': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3) svgo: 4.0.0 - vite: 7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + vite: 7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) transitivePeerDependencies: - supports-color - typescript - vite-plugin-singlefile@2.3.0(rollup@4.52.4)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)): + vite-plugin-singlefile@2.3.0(rollup@4.52.4)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)): dependencies: micromatch: 4.0.8 rollup: 4.52.4 - vite: 7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + vite: 7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)): dependencies: @@ -11875,6 +12628,26 @@ snapshots: tsx: 4.20.6 yaml: 2.7.1 + vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1): + dependencies: + esbuild: 0.25.9 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.6.1 + fsevents: 2.3.3 + jiti: 2.4.2 + less: 4.3.0 + lightningcss: 1.30.1 + sass: 1.87.0 + stylus: 0.62.0 + terser: 5.39.0 + tsx: 4.20.6 + yaml: 2.7.1 + vitefu@1.1.1(vite@7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)): optionalDependencies: vite: 7.1.9(@types/node@22.18.8)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) @@ -11925,6 +12698,51 @@ snapshots: - tsx - yaml + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.6.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.0 + debug: 4.4.1 + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + vite-node: 3.2.4(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 24.6.1 + '@vitest/browser': 3.2.4(playwright@1.55.1)(vite@7.1.9(@types/node@24.6.1)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.87.0)(stylus@0.62.0)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.1))(vitest@3.2.4) + '@vitest/ui': 3.2.4(vitest@3.2.4) + jsdom: 26.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + void-elements@3.1.0: {} w3c-xmlserializer@5.0.0: @@ -11932,6 +12750,8 @@ snapshots: xml-name-validator: 5.0.0 optional: true + web-streams-polyfill@3.3.3: {} + webidl-conversions@3.0.1: {} webidl-conversions@4.0.2: {} @@ -12013,6 +12833,8 @@ snapshots: xmlchars@2.2.0: optional: true + xtend@4.0.2: {} + y18n@5.0.8: {} yallist@3.1.1: {} @@ -12033,6 +12855,8 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yn@3.1.1: {} + yocto-queue@1.2.1: {} zimmerframe@1.1.2: