diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..98f3cd4
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,49 @@
+name: CI
+
+on:
+ push:
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ checks:
+ runs-on: ubuntu-latest
+ services:
+ postgres:
+ image: postgres:16-alpine
+ env:
+ POSTGRES_USER: ai_platform_test
+ POSTGRES_PASSWORD: ci-only-password
+ POSTGRES_DB: ai_platform_test
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd "pg_isready -U ai_platform_test -d ai_platform_test"
+ --health-interval 5s
+ --health-timeout 5s
+ --health-retries 12
+ env:
+ AI_PLATFORM_DATABASE_URL: postgresql+asyncpg://ai_platform_test:ci-only-password@localhost:5432/ai_platform_test
+ AI_PLATFORM_ENVIRONMENT: test
+ AI_PLATFORM_TENANT_PROVISIONING_SECRET: ci-only-provisioner-value
+ BACKEND_INTERNAL_URL: http://127.0.0.1:8000
+ WEB_ORIGIN: http://localhost:3000
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: astral-sh/setup-uv@v6
+ with:
+ enable-cache: true
+ cache-dependency-glob: uv.lock
+ - uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: 1.4.0
+ - run: uv sync --locked --all-groups
+ - working-directory: apps/web
+ run: bun install --frozen-lockfile
+ - run: make migrate
+ - run: make check
+ - run: docker build -f Dockerfile.backend -t ai-platform-backend:ci .
+ - run: docker build -f apps/web/Dockerfile -t ai-platform-web:ci .
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..6f5de7a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,49 @@
+SHELL := /bin/sh
+.DEFAULT_GOAL := help
+
+.PHONY: help install db-up db-down migrate backend-dev web-dev contract check-fast check test build
+
+help: ## List repository commands
+ @grep -E '^[a-zA-Z_-]+:.*## ' $(MAKEFILE_LIST) | sort | awk -F ':.*## ' '{printf "%-16s %s\n", $$1, $$2}'
+
+install: ## Sync project-local dependencies from both lockfiles
+ uv sync --locked --all-groups
+ cd apps/web && bun install --frozen-lockfile
+
+db-up: ## Start only the new local PostgreSQL volume
+ docker compose up -d postgres
+
+db-down: ## Stop local services without deleting the database
+ docker compose down
+
+migrate: ## Apply migrations to the configured database
+ uv run --no-sync alembic upgrade head
+
+backend-dev: ## Run a single backend process bound to localhost
+ uv run --no-sync uvicorn ai_platform.main:app --reload --host 127.0.0.1 --port 8000
+
+web-dev: ## Run SvelteKit bound to localhost
+ cd apps/web && bun run dev -- --host 127.0.0.1
+
+contract: ## Generate the backend OpenAPI consumed by Admin Web
+ uv run --no-sync ai-platform export-openapi contracts/lead-scoring/v2/openapi.json
+
+check-fast: ## Check staged/all tracked source with opt-in pre-commit rules
+ uv run --no-sync pre-commit run --all-files
+
+test: ## Run backend tests only on the isolated test database
+ @set -a; if [ -f .env.test ] && [ -z "$$CI" ]; then . ./.env.test; fi; set +a; \
+ python -c 'from urllib.parse import urlsplit; import os,sys; url=os.environ.get("AI_PLATFORM_DATABASE_URL", ""); sys.exit(urlsplit(url).path.rstrip("/").split("/")[-1] != "ai_platform_test")' || { echo "Test URL must end in /ai_platform_test." >&2; exit 2; }; \
+ uv run --no-sync pytest
+
+check: check-fast contract ## Run backend and web checks used by CI
+ uv run --no-sync ruff check src tests
+ uv run --no-sync ruff format --check src tests
+ uv run --no-sync pyright
+ uv run --no-sync lint-imports
+ $(MAKE) test
+ cd apps/web && bun run format:check && bun run check && bun run test && bun run build
+
+build: ## Build both images; does not deploy them
+ docker build -f Dockerfile.backend -t ai-platform-backend:local .
+ docker build -f apps/web/Dockerfile -t ai-platform-web:local .
diff --git a/apps/web/.env.example b/apps/web/.env.example
new file mode 100644
index 0000000..c9f6a90
--- /dev/null
+++ b/apps/web/.env.example
@@ -0,0 +1,4 @@
+# Admin Web server only. SvelteKit reads apps/web/.env when run from apps/web/.
+# Docker Compose passes BACKEND_INTERNAL_URL directly to the web container instead.
+BACKEND_INTERNAL_URL=http://127.0.0.1:8000
+# Never put credentials or backend URLs in PUBLIC_* variables.
diff --git a/apps/web/.prettierrc b/apps/web/.prettierrc
new file mode 100644
index 0000000..35e3418
--- /dev/null
+++ b/apps/web/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "plugins": ["prettier-plugin-svelte"],
+ "singleQuote": true,
+ "printWidth": 100
+}
diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile
new file mode 100644
index 0000000..e112990
--- /dev/null
+++ b/apps/web/Dockerfile
@@ -0,0 +1,18 @@
+# syntax=docker/dockerfile:1
+FROM oven/bun:1.4.0 AS build
+WORKDIR /app
+COPY apps/web/package.json apps/web/bun.lock ./
+RUN bun install --frozen-lockfile
+COPY apps/web/ ./
+COPY contracts/lead-scoring/v2/openapi.json /contracts/lead-scoring/v2/openapi.json
+RUN bun run build
+
+FROM oven/bun:1.4.0 AS runtime
+ENV NODE_ENV=production HOST=0.0.0.0 PORT=3000
+WORKDIR /app
+COPY apps/web/package.json apps/web/bun.lock ./
+RUN bun install --production --frozen-lockfile
+COPY --from=build --chown=bun:bun /app/build ./build
+USER bun
+EXPOSE 3000
+CMD ["bun", "build/index.js"]
diff --git a/apps/web/bun.lock b/apps/web/bun.lock
new file mode 100644
index 0000000..ce427b2
--- /dev/null
+++ b/apps/web/bun.lock
@@ -0,0 +1,522 @@
+{
+ "lockfileVersion": 2,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "ai-platform-admin",
+ "dependencies": {
+ "@fontsource-variable/inter": "^5.3.0",
+ "@sveltejs/kit": "2.70.3",
+ "cn": "^0.4.0",
+ "openapi-fetch": "0.17.0",
+ },
+ "devDependencies": {
+ "@internationalized/date": "^3.12.0",
+ "@lucide/svelte": "^1.48.0",
+ "@sveltejs/adapter-node": "5.5.7",
+ "@sveltejs/vite-plugin-svelte": "7.3.1",
+ "@tailwindcss/vite": "^4.3.3",
+ "@types/node": "24.6.1",
+ "bits-ui": "^2.18.0",
+ "openapi-typescript": "7.13.0",
+ "paneforge": "^1.0.2",
+ "prettier": "3.9.9",
+ "prettier-plugin-svelte": "4.1.1",
+ "svelte": "5.57.1",
+ "svelte-check": "4.7.6",
+ "tailwind-variants": "^3.3.0",
+ "tailwindcss": "^4.3.3",
+ "typescript": "5.9.3",
+ "vite": "8.3.1",
+ "vitest": "5.0.1",
+ },
+ },
+ },
+ "packages": {
+ "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
+
+ "@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="],
+
+ "@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="],
+
+ "@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
+
+ "@fontsource-variable/inter": ["@fontsource-variable/inter@5.3.0", "", {}, "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA=="],
+
+ "@internationalized/date": ["@internationalized/date@3.12.4", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+
+ "@lucide/svelte": ["@lucide/svelte@1.48.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-bLimVDjehxu3fpu9XEUpqEkBk2sJAHAT2sVcLHhlM70EdNsXuj9ZnYENWv5xgw3+05h4i5TirAxTiTgnZZf8gw=="],
+
+ "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
+
+ "@oxc-project/types": ["@oxc-project/types@0.151.0", "", {}, "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA=="],
+
+ "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
+
+ "@redocly/ajv": ["@redocly/ajv@8.11.2", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js-replace": "^1.0.1" } }, "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg=="],
+
+ "@redocly/config": ["@redocly/config@0.22.0", "", {}, "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ=="],
+
+ "@redocly/openapi-core": ["@redocly/openapi-core@1.34.20", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.3.2", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-ypeBZ/6BKXR9+7/TtbKhbl4UgD7raHhPS12oknlKno2A8+lnFkxIwiE/Aklu6L2cd/ioH+fCWuMxi9/p3EyAPw=="],
+
+ "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.11", "", { "os": "android", "cpu": "arm" }, "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw=="],
+
+ "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.11", "", { "os": "android", "cpu": "arm64" }, "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA=="],
+
+ "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ=="],
+
+ "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w=="],
+
+ "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow=="],
+
+ "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.11", "", { "os": "linux", "cpu": "arm" }, "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ=="],
+
+ "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw=="],
+
+ "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg=="],
+
+ "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw=="],
+
+ "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ=="],
+
+ "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.11", "", { "os": "linux", "cpu": "x64" }, "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A=="],
+
+ "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.11", "", { "os": "linux", "cpu": "x64" }, "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg=="],
+
+ "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.11", "", { "os": "none", "cpu": "arm64" }, "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA=="],
+
+ "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw=="],
+
+ "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.11", "", { "os": "win32", "cpu": "x64" }, "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA=="],
+
+ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
+
+ "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@29.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-ZaOxZceP7SOUW7Lqw5IRVweSQYWaeIPnXIGLiB690EBA3FGJTO40EEr2L5yZplJWsgTCogILRSpcAe7+U0Otdg=="],
+
+ "@rollup/plugin-json": ["@rollup/plugin-json@6.1.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA=="],
+
+ "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg=="],
+
+ "@rollup/plugin-replace": ["@rollup/plugin-replace@6.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA=="],
+
+ "@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="],
+
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.63.5", "", { "os": "android", "cpu": "arm" }, "sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg=="],
+
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.63.5", "", { "os": "android", "cpu": "arm64" }, "sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg=="],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.63.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg=="],
+
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.63.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ=="],
+
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.63.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg=="],
+
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.63.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ=="],
+
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.63.5", "", { "os": "linux", "cpu": "arm" }, "sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ=="],
+
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.63.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw=="],
+
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.63.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw=="],
+
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.63.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ=="],
+
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.63.5", "", { "os": "linux", "cpu": "none" }, "sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ=="],
+
+ "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.63.5", "", { "os": "linux", "cpu": "none" }, "sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA=="],
+
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.63.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA=="],
+
+ "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.63.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg=="],
+
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.63.5", "", { "os": "linux", "cpu": "none" }, "sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg=="],
+
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.63.5", "", { "os": "linux", "cpu": "none" }, "sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ=="],
+
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.63.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w=="],
+
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.63.5", "", { "os": "linux", "cpu": "x64" }, "sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA=="],
+
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.63.5", "", { "os": "linux", "cpu": "x64" }, "sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA=="],
+
+ "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.63.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig=="],
+
+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.63.5", "", { "os": "none", "cpu": "arm64" }, "sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g=="],
+
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.63.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw=="],
+
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.63.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw=="],
+
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.63.5", "", { "os": "win32", "cpu": "x64" }, "sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw=="],
+
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.63.5", "", { "os": "win32", "cpu": "x64" }, "sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw=="],
+
+ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+
+ "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.13", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ=="],
+
+ "@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.5.7", "", { "dependencies": { "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-replace": "^6.0.3", "rollup": "^4.59.0" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-uOfc9eVlI3A37RRSaKcgrheBYPrfJwC9VMqDp8x/O6tlKdcLLvHThSWD0KNIbjQ/d+7bwLGx3vx6aowAcRfd2g=="],
+
+ "@sveltejs/kit": ["@sveltejs/kit@2.70.3", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg=="],
+
+ "@sveltejs/load-config": ["@sveltejs/load-config@0.2.3", "", {}, "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ=="],
+
+ "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@7.3.1", "", { "dependencies": { "deepmerge": "^4.3.1", "magic-string": "^1.0.0", "obug": "^2.1.0", "vitefu": "^1.1.2" }, "peerDependencies": { "svelte": "^5.46.4", "vite": "^8.0.0-beta.7 || ^8.0.0" } }, "sha512-ZPsLN8B1e/En+Ak5s4V7srFDT532oS0qieLsQwu63NGKsS+iAjoO2Js1BochlHlglcU+Pt7WAO3C5Ee+4f6gVA=="],
+
+ "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="],
+
+ "@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
+
+ "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="],
+
+ "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="],
+
+ "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="],
+
+ "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="],
+
+ "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="],
+
+ "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="],
+
+ "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="],
+
+ "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="],
+
+ "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="],
+
+ "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="],
+
+ "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="],
+
+ "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="],
+
+ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
+
+ "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="],
+
+ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
+
+ "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
+
+ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+
+ "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
+
+ "@types/node": ["@types/node@24.6.1", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-ljvjjs3DNXummeIaooB4cLBKg2U6SPI6Hjra/9rRIy7CpM0HpLtG9HptkMKAb4HYWy5S7HUvJEuWgr/y0U8SHw=="],
+
+ "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
+
+ "@vitest/mocker": ["@vitest/mocker@5.0.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.31", "@vitest/spy": "5.0.1", "estree-walker": "^3.0.3", "magic-string": "^1.2.3" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q=="],
+
+ "@vitest/spy": ["@vitest/spy@5.0.1", "", {}, "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q=="],
+
+ "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
+
+ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+
+ "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
+
+ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+
+ "aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
+
+ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
+
+ "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
+
+ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+
+ "bits-ui": ["bits-ui@2.19.3", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-04COIqqht1h0YRnVTJVHy3+F8NhzS5x0eSM8eqvavaWmK/GLDTgYJW2h1CA//8hh9vSk4rG7rOvJ3QAIpCuYIw=="],
+
+ "brace-expansion": ["brace-expansion@2.1.7", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g=="],
+
+ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
+
+ "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="],
+
+ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
+
+ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
+
+ "cn": ["cn@0.4.0", "", { "bin": { "cn": "bin/cn.mjs" } }, "sha512-qOAeUhPwPzBxx6jaeQdo646XIKliKHc2j1ZF7Ngb1o0pJZKH5gq8FkA0aVdUdPFUDXNXyUCXpCtoJISo6dhizg=="],
+
+ "colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="],
+
+ "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
+
+ "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
+
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
+ "devalue": ["devalue@5.9.4", "", {}, "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg=="],
+
+ "enhanced-resolve": ["enhanced-resolve@5.25.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w=="],
+
+ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+
+ "es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="],
+
+ "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
+
+ "esrap": ["esrap@2.3.11", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-1kmQFiDHcf1T69K9qKtn2MasiKlaq6xNw2ZEgHD8jNyyoNEfx9eCOiJpYDmc0Yk0FKT0bzx5dyBocMDAitWpaQ=="],
+
+ "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
+
+ "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="],
+
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
+ "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
+
+ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
+ "index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="],
+
+ "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
+
+ "is-core-module": ["is-core-module@2.17.0", "", { "dependencies": { "hasown": "^2.0.4" } }, "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA=="],
+
+ "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
+
+ "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
+
+ "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
+
+ "js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="],
+
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="],
+
+ "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
+
+ "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
+
+ "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
+
+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
+
+ "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
+
+ "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
+
+ "magic-string": ["magic-string@1.4.2", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g=="],
+
+ "minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
+
+ "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
+
+ "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="],
+
+ "obug": ["obug@2.2.1", "", {}, "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q=="],
+
+ "openapi-fetch": ["openapi-fetch@0.17.0", "", { "dependencies": { "openapi-typescript-helpers": "^0.1.0" } }, "sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig=="],
+
+ "openapi-typescript": ["openapi-typescript@7.13.0", "", { "dependencies": { "@redocly/openapi-core": "^1.34.6", "ansi-colors": "^4.1.3", "change-case": "^5.4.4", "parse-json": "^8.3.0", "supports-color": "^10.2.2", "yargs-parser": "^21.1.1" }, "peerDependencies": { "typescript": "^5.x" }, "bin": { "openapi-typescript": "bin/cli.js" } }, "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ=="],
+
+ "openapi-typescript-helpers": ["openapi-typescript-helpers@0.1.0", "", {}, "sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw=="],
+
+ "paneforge": ["paneforge@1.0.2", "", { "dependencies": { "runed": "^0.23.4", "svelte-toolbelt": "^0.9.2" }, "peerDependencies": { "svelte": "^5.29.0" } }, "sha512-KzmIXQH1wCfwZ4RsMohD/IUtEjVhteR+c+ulb/CHYJHX8SuDXoJmChtsc/Xs5Wl8NHS4L5Q7cxL8MG40gSU1bA=="],
+
+ "parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="],
+
+ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
+
+ "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="],
+
+ "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="],
+
+ "prettier": ["prettier@3.9.9", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg=="],
+
+ "prettier-plugin-svelte": ["prettier-plugin-svelte@4.1.1", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^5.0.0" } }, "sha512-wXvbXMjSvb4C9ENWTHXyd+ihakKCsJ6rJhLP6/8HFNj4GkZr48jqL9PoKsl2sk7SyCZRTnJ7O2TTowUpOxP/KA=="],
+
+ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
+
+ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
+
+ "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
+
+ "rolldown": ["rolldown@1.2.11", "", { "dependencies": { "@oxc-project/types": "=0.151.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.11", "@rolldown/binding-android-arm64": "1.2.11", "@rolldown/binding-darwin-arm64": "1.2.11", "@rolldown/binding-darwin-x64": "1.2.11", "@rolldown/binding-freebsd-x64": "1.2.11", "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", "@rolldown/binding-linux-arm64-gnu": "1.2.11", "@rolldown/binding-linux-arm64-musl": "1.2.11", "@rolldown/binding-linux-ppc64-gnu": "1.2.11", "@rolldown/binding-linux-s390x-gnu": "1.2.11", "@rolldown/binding-linux-x64-gnu": "1.2.11", "@rolldown/binding-linux-x64-musl": "1.2.11", "@rolldown/binding-openharmony-arm64": "1.2.11", "@rolldown/binding-win32-arm64-msvc": "1.2.11", "@rolldown/binding-win32-x64-msvc": "1.2.11" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw=="],
+
+ "rollup": ["rollup@4.63.5", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.5", "@rollup/rollup-android-arm64": "4.63.5", "@rollup/rollup-darwin-arm64": "4.63.5", "@rollup/rollup-darwin-x64": "4.63.5", "@rollup/rollup-freebsd-arm64": "4.63.5", "@rollup/rollup-freebsd-x64": "4.63.5", "@rollup/rollup-linux-arm-gnueabihf": "4.63.5", "@rollup/rollup-linux-arm-musleabihf": "4.63.5", "@rollup/rollup-linux-arm64-gnu": "4.63.5", "@rollup/rollup-linux-arm64-musl": "4.63.5", "@rollup/rollup-linux-loong64-gnu": "4.63.5", "@rollup/rollup-linux-loong64-musl": "4.63.5", "@rollup/rollup-linux-ppc64-gnu": "4.63.5", "@rollup/rollup-linux-ppc64-musl": "4.63.5", "@rollup/rollup-linux-riscv64-gnu": "4.63.5", "@rollup/rollup-linux-riscv64-musl": "4.63.5", "@rollup/rollup-linux-s390x-gnu": "4.63.5", "@rollup/rollup-linux-x64-gnu": "4.63.5", "@rollup/rollup-linux-x64-musl": "4.63.5", "@rollup/rollup-openbsd-x64": "4.63.5", "@rollup/rollup-openharmony-arm64": "4.63.5", "@rollup/rollup-win32-arm64-msvc": "4.63.5", "@rollup/rollup-win32-ia32-msvc": "4.63.5", "@rollup/rollup-win32-x64-gnu": "4.63.5", "@rollup/rollup-win32-x64-msvc": "4.63.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw=="],
+
+ "runed": ["runed@0.35.1", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0" }, "optionalPeers": ["@sveltejs/kit"] }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="],
+
+ "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
+
+ "set-cookie-parser": ["set-cookie-parser@3.1.2", "", {}, "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw=="],
+
+ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
+
+ "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
+
+ "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
+
+ "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
+
+ "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
+
+ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
+
+ "svelte": ["svelte@5.57.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@jridgewell/sourcemap-codec": "^1.6.0", "@sveltejs/acorn-typescript": "^1.0.13", "@types/estree": "^1.0.9", "acorn": "^8.18.0", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.9.2", "esm-env": "^1.2.1", "esrap": "^2.3.6", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-Uqj49lWKB+iSSnneuwiYYJ7MZgkB+eXr0LXBhv4uDuAkXqnWmq65Sxflfvp0Lc6MdKjMUxGaeOKWJqz5SNiVIA=="],
+
+ "svelte-check": ["svelte-check@4.7.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.3", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw=="],
+
+ "svelte-toolbelt": ["svelte-toolbelt@0.10.6", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.35.1", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ=="],
+
+ "tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="],
+
+ "tailwind-variants": ["tailwind-variants@3.3.1", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge", "tailwindcss"] }, "sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw=="],
+
+ "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
+
+ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
+
+ "tinybench": ["tinybench@6.1.4", "", {}, "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ=="],
+
+ "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="],
+
+ "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
+
+ "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
+
+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="],
+
+ "uri-js-replace": ["uri-js-replace@1.0.1", "", {}, "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g=="],
+
+ "vite": ["vite@8.3.1", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.9", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA=="],
+
+ "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
+
+ "vitest": ["vitest@5.0.1", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/mocker": "5.0.1", "chai": "^6.2.2", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", "magic-string": "^1.2.3", "obug": "^2.1.4", "picomatch": "^4.0.7", "std-env": "^4.2.0", "tinybench": "6.1.4", "tinyexec": "1.3.0", "tinyglobby": "^0.2.17", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "5.0.1", "@vitest/browser-preview": "5.0.1", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", "@vitest/coverage-istanbul": "5.0.1", "@vitest/coverage-v8": "5.0.1", "@vitest/ui": "5.0.1", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg=="],
+
+ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
+
+ "yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="],
+
+ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
+
+ "zimmerframe": ["zimmerframe@1.1.5", "", {}, "sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA=="],
+
+ "@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
+
+ "@rollup/plugin-commonjs/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "@rollup/plugin-replace/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "@sveltejs/kit/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
+
+ "@tailwindcss/node/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
+ "paneforge/runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="],
+
+ "paneforge/svelte-toolbelt": ["svelte-toolbelt@0.9.3", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.29.0", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw=="],
+
+ "svelte/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
+
+ "paneforge/svelte-toolbelt/runed": ["runed@0.29.2", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA=="],
+ }
+}
diff --git a/apps/web/components.json b/apps/web/components.json
new file mode 100644
index 0000000..2e35327
--- /dev/null
+++ b/apps/web/components.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://shadcn-svelte.com/schema.json",
+ "typescript": true,
+ "tailwind": {
+ "css": "src/app.css",
+ "baseColor": "zinc"
+ },
+ "aliases": {
+ "lib": "$lib",
+ "utils": "$lib/utils",
+ "components": "$lib/components",
+ "hooks": "$lib/hooks",
+ "ui": "$lib/components/ui"
+ }
+}
diff --git a/apps/web/package.json b/apps/web/package.json
new file mode 100644
index 0000000..9705d2a
--- /dev/null
+++ b/apps/web/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "ai-platform-admin",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev",
+ "generate-api": "openapi-typescript ../../contracts/lead-scoring/v2/openapi.json -o src/lib/generated/api.d.ts",
+ "check": "bun run generate-api && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+ "test": "vitest run",
+ "format:check": "prettier --check 'src/**/*.{ts,svelte,css}' '!src/lib/components/ui/**' '!src/lib/generated/**'",
+ "build": "bun run generate-api && vite build",
+ "preview": "vite preview",
+ "start": "bun build/index.js"
+ },
+ "dependencies": {
+ "@fontsource-variable/inter": "^5.3.0",
+ "@sveltejs/kit": "2.70.3",
+ "cn": "^0.4.0",
+ "openapi-fetch": "0.17.0"
+ },
+ "devDependencies": {
+ "@internationalized/date": "^3.12.0",
+ "@lucide/svelte": "^1.48.0",
+ "@sveltejs/adapter-node": "5.5.7",
+ "@sveltejs/vite-plugin-svelte": "7.3.1",
+ "@tailwindcss/vite": "^4.3.3",
+ "@types/node": "24.6.1",
+ "bits-ui": "^2.18.0",
+ "openapi-typescript": "7.13.0",
+ "paneforge": "^1.0.2",
+ "prettier": "3.9.9",
+ "prettier-plugin-svelte": "4.1.1",
+ "svelte": "5.57.1",
+ "svelte-check": "4.7.6",
+ "tailwind-variants": "^3.3.0",
+ "tailwindcss": "^4.3.3",
+ "typescript": "5.9.3",
+ "vite": "8.3.1",
+ "vitest": "5.0.1"
+ }
+}
diff --git a/apps/web/src/app.css b/apps/web/src/app.css
new file mode 100644
index 0000000..6a58bca
--- /dev/null
+++ b/apps/web/src/app.css
@@ -0,0 +1,80 @@
+@import 'tailwindcss';
+
+:root {
+ --radius: 0.625rem;
+ --background: #f5f8fa;
+ --foreground: #032d60;
+ --card: #fff;
+ --card-foreground: #032d60;
+ --popover: #fff;
+ --popover-foreground: #032d60;
+ --primary: #2e844a;
+ --primary-foreground: #fff;
+ --secondary: #eaf2f5;
+ --secondary-foreground: #17495d;
+ --muted: #edf3f5;
+ --muted-foreground: #526474;
+ --accent: #dff3ef;
+ --accent-foreground: #124b52;
+ --destructive: #a43432;
+ --border: #d9e5e9;
+ --input: #afc3cb;
+ --ring: #1183a0;
+ --sidebar: #f9fcfc;
+ --sidebar-foreground: #032d60;
+ --sidebar-primary: #2e844a;
+ --sidebar-primary-foreground: #fff;
+ --sidebar-accent: #e5f3f1;
+ --sidebar-accent-foreground: #164a58;
+ --sidebar-border: #dbe7ea;
+ --sidebar-ring: #1183a0;
+ font-family: 'Inter Variable', system-ui, sans-serif;
+}
+
+@theme inline {
+ --font-sans: 'Inter Variable', system-ui, sans-serif;
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ min-width: 320px;
+ }
+ :focus-visible {
+ outline: 2px solid var(--ring);
+ outline-offset: 2px;
+ }
+}
diff --git a/apps/web/src/app.html b/apps/web/src/app.html
new file mode 100644
index 0000000..6ea8db4
--- /dev/null
+++ b/apps/web/src/app.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
+
diff --git a/apps/web/src/lib/components/AdminNavigation.svelte b/apps/web/src/lib/components/AdminNavigation.svelte
new file mode 100644
index 0000000..2184a74
--- /dev/null
+++ b/apps/web/src/lib/components/AdminNavigation.svelte
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+ DỊCH VỤ AI
+
+
+
+
+
+
+
+ Churn
+ Chưa có đặc tả
+
+
+
+
+
+
+ LEAD SCORING
+
+
+
+ {#snippet child({ props })}
+
+ Lần chấm điểm
+
+ {/snippet}
+
+
+
+
+ {#snippet child({ props })}
+
+ Cấu hình AI
+
+ {/snippet}
+
+
+
+
+
+
+
+
diff --git a/apps/web/src/lib/components/ui/badge/badge.svelte b/apps/web/src/lib/components/ui/badge/badge.svelte
new file mode 100644
index 0000000..bd05fc4
--- /dev/null
+++ b/apps/web/src/lib/components/ui/badge/badge.svelte
@@ -0,0 +1,49 @@
+
+
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/badge/index.ts b/apps/web/src/lib/components/ui/badge/index.ts
new file mode 100644
index 0000000..64e0aa9
--- /dev/null
+++ b/apps/web/src/lib/components/ui/badge/index.ts
@@ -0,0 +1,2 @@
+export { default as Badge } from "./badge.svelte";
+export { badgeVariants, type BadgeVariant } from "./badge.svelte";
diff --git a/apps/web/src/lib/components/ui/button/button.svelte b/apps/web/src/lib/components/ui/button/button.svelte
new file mode 100644
index 0000000..325182a
--- /dev/null
+++ b/apps/web/src/lib/components/ui/button/button.svelte
@@ -0,0 +1,82 @@
+
+
+
+
+{#if href}
+
+ {@render children?.()}
+
+{:else}
+
+ {@render children?.()}
+
+{/if}
diff --git a/apps/web/src/lib/components/ui/button/index.ts b/apps/web/src/lib/components/ui/button/index.ts
new file mode 100644
index 0000000..fb585d7
--- /dev/null
+++ b/apps/web/src/lib/components/ui/button/index.ts
@@ -0,0 +1,17 @@
+import Root, {
+ type ButtonProps,
+ type ButtonSize,
+ type ButtonVariant,
+ buttonVariants,
+} from "./button.svelte";
+
+export {
+ Root,
+ type ButtonProps as Props,
+ //
+ Root as Button,
+ buttonVariants,
+ type ButtonProps,
+ type ButtonSize,
+ type ButtonVariant,
+};
diff --git a/apps/web/src/lib/components/ui/card/card-action.svelte b/apps/web/src/lib/components/ui/card/card-action.svelte
new file mode 100644
index 0000000..53b3861
--- /dev/null
+++ b/apps/web/src/lib/components/ui/card/card-action.svelte
@@ -0,0 +1,23 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/card/card-content.svelte b/apps/web/src/lib/components/ui/card/card-content.svelte
new file mode 100644
index 0000000..bf5dac7
--- /dev/null
+++ b/apps/web/src/lib/components/ui/card/card-content.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/card/card-description.svelte b/apps/web/src/lib/components/ui/card/card-description.svelte
new file mode 100644
index 0000000..8e37a1b
--- /dev/null
+++ b/apps/web/src/lib/components/ui/card/card-description.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/card/card-footer.svelte b/apps/web/src/lib/components/ui/card/card-footer.svelte
new file mode 100644
index 0000000..db2282e
--- /dev/null
+++ b/apps/web/src/lib/components/ui/card/card-footer.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/card/card-header.svelte b/apps/web/src/lib/components/ui/card/card-header.svelte
new file mode 100644
index 0000000..416d287
--- /dev/null
+++ b/apps/web/src/lib/components/ui/card/card-header.svelte
@@ -0,0 +1,23 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/card/card-title.svelte b/apps/web/src/lib/components/ui/card/card-title.svelte
new file mode 100644
index 0000000..e1ca7a9
--- /dev/null
+++ b/apps/web/src/lib/components/ui/card/card-title.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/card/card.svelte b/apps/web/src/lib/components/ui/card/card.svelte
new file mode 100644
index 0000000..cf6c106
--- /dev/null
+++ b/apps/web/src/lib/components/ui/card/card.svelte
@@ -0,0 +1,22 @@
+
+
+img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)}
+ {...restProps}
+>
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/card/index.ts b/apps/web/src/lib/components/ui/card/index.ts
new file mode 100644
index 0000000..5705f13
--- /dev/null
+++ b/apps/web/src/lib/components/ui/card/index.ts
@@ -0,0 +1,25 @@
+import Action from "./card-action.svelte";
+import Content from "./card-content.svelte";
+import Description from "./card-description.svelte";
+import Footer from "./card-footer.svelte";
+import Header from "./card-header.svelte";
+import Title from "./card-title.svelte";
+import Root from "./card.svelte";
+
+export {
+ Root,
+ Content,
+ Description,
+ Footer,
+ Header,
+ Title,
+ Action,
+ //
+ Root as Card,
+ Content as CardContent,
+ Description as CardDescription,
+ Footer as CardFooter,
+ Header as CardHeader,
+ Title as CardTitle,
+ Action as CardAction,
+};
diff --git a/apps/web/src/lib/components/ui/input/index.ts b/apps/web/src/lib/components/ui/input/index.ts
new file mode 100644
index 0000000..f47b6d3
--- /dev/null
+++ b/apps/web/src/lib/components/ui/input/index.ts
@@ -0,0 +1,7 @@
+import Root from "./input.svelte";
+
+export {
+ Root,
+ //
+ Root as Input,
+};
diff --git a/apps/web/src/lib/components/ui/input/input.svelte b/apps/web/src/lib/components/ui/input/input.svelte
new file mode 100644
index 0000000..075abd0
--- /dev/null
+++ b/apps/web/src/lib/components/ui/input/input.svelte
@@ -0,0 +1,48 @@
+
+
+{#if type === "file"}
+
+{:else}
+
+{/if}
diff --git a/apps/web/src/lib/components/ui/label/index.ts b/apps/web/src/lib/components/ui/label/index.ts
new file mode 100644
index 0000000..8bfca0b
--- /dev/null
+++ b/apps/web/src/lib/components/ui/label/index.ts
@@ -0,0 +1,7 @@
+import Root from "./label.svelte";
+
+export {
+ Root,
+ //
+ Root as Label,
+};
diff --git a/apps/web/src/lib/components/ui/label/label.svelte b/apps/web/src/lib/components/ui/label/label.svelte
new file mode 100644
index 0000000..d5e3086
--- /dev/null
+++ b/apps/web/src/lib/components/ui/label/label.svelte
@@ -0,0 +1,20 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/resizable/index.ts b/apps/web/src/lib/components/ui/resizable/index.ts
new file mode 100644
index 0000000..2e37f11
--- /dev/null
+++ b/apps/web/src/lib/components/ui/resizable/index.ts
@@ -0,0 +1,13 @@
+import { Pane } from "paneforge";
+import Handle from "./resizable-handle.svelte";
+import PaneGroup from "./resizable-pane-group.svelte";
+
+export {
+ PaneGroup,
+ Pane,
+ Handle,
+ //
+ PaneGroup as ResizablePaneGroup,
+ Pane as ResizablePane,
+ Handle as ResizableHandle,
+};
diff --git a/apps/web/src/lib/components/ui/resizable/resizable-handle.svelte b/apps/web/src/lib/components/ui/resizable/resizable-handle.svelte
new file mode 100644
index 0000000..3b3369b
--- /dev/null
+++ b/apps/web/src/lib/components/ui/resizable/resizable-handle.svelte
@@ -0,0 +1,27 @@
+
+
+div]:rotate-90",
+ className
+ )}
+ {...restProps}
+>
+ {#if withHandle}
+
+ {/if}
+
diff --git a/apps/web/src/lib/components/ui/resizable/resizable-pane-group.svelte b/apps/web/src/lib/components/ui/resizable/resizable-pane-group.svelte
new file mode 100644
index 0000000..e554d3a
--- /dev/null
+++ b/apps/web/src/lib/components/ui/resizable/resizable-pane-group.svelte
@@ -0,0 +1,24 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/separator/index.ts b/apps/web/src/lib/components/ui/separator/index.ts
new file mode 100644
index 0000000..82442d2
--- /dev/null
+++ b/apps/web/src/lib/components/ui/separator/index.ts
@@ -0,0 +1,7 @@
+import Root from "./separator.svelte";
+
+export {
+ Root,
+ //
+ Root as Separator,
+};
diff --git a/apps/web/src/lib/components/ui/separator/separator.svelte b/apps/web/src/lib/components/ui/separator/separator.svelte
new file mode 100644
index 0000000..e91b176
--- /dev/null
+++ b/apps/web/src/lib/components/ui/separator/separator.svelte
@@ -0,0 +1,21 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/sheet/index.ts b/apps/web/src/lib/components/ui/sheet/index.ts
new file mode 100644
index 0000000..12a55b2
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sheet/index.ts
@@ -0,0 +1,34 @@
+import Close from "./sheet-close.svelte";
+import Content from "./sheet-content.svelte";
+import Description from "./sheet-description.svelte";
+import Footer from "./sheet-footer.svelte";
+import Header from "./sheet-header.svelte";
+import Overlay from "./sheet-overlay.svelte";
+import Portal from "./sheet-portal.svelte";
+import Title from "./sheet-title.svelte";
+import Trigger from "./sheet-trigger.svelte";
+import Root from "./sheet.svelte";
+
+export {
+ Root,
+ Close,
+ Trigger,
+ Portal,
+ Overlay,
+ Content,
+ Header,
+ Footer,
+ Title,
+ Description,
+ //
+ Root as Sheet,
+ Close as SheetClose,
+ Trigger as SheetTrigger,
+ Portal as SheetPortal,
+ Overlay as SheetOverlay,
+ Content as SheetContent,
+ Header as SheetHeader,
+ Footer as SheetFooter,
+ Title as SheetTitle,
+ Description as SheetDescription,
+};
diff --git a/apps/web/src/lib/components/ui/sheet/sheet-close.svelte b/apps/web/src/lib/components/ui/sheet/sheet-close.svelte
new file mode 100644
index 0000000..ae382c1
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sheet/sheet-close.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/sheet/sheet-content.svelte b/apps/web/src/lib/components/ui/sheet/sheet-content.svelte
new file mode 100644
index 0000000..5406288
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sheet/sheet-content.svelte
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+ {@render children?.()}
+ {#if showCloseButton}
+
+ {#snippet child({ props })}
+
+
+ Close
+
+ {/snippet}
+
+ {/if}
+
+
diff --git a/apps/web/src/lib/components/ui/sheet/sheet-description.svelte b/apps/web/src/lib/components/ui/sheet/sheet-description.svelte
new file mode 100644
index 0000000..333b17a
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sheet/sheet-description.svelte
@@ -0,0 +1,17 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/sheet/sheet-footer.svelte b/apps/web/src/lib/components/ui/sheet/sheet-footer.svelte
new file mode 100644
index 0000000..ad9e07b
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sheet/sheet-footer.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/sheet/sheet-header.svelte b/apps/web/src/lib/components/ui/sheet/sheet-header.svelte
new file mode 100644
index 0000000..189b151
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sheet/sheet-header.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/sheet/sheet-overlay.svelte b/apps/web/src/lib/components/ui/sheet/sheet-overlay.svelte
new file mode 100644
index 0000000..c2a186d
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sheet/sheet-overlay.svelte
@@ -0,0 +1,20 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/sheet/sheet-portal.svelte b/apps/web/src/lib/components/ui/sheet/sheet-portal.svelte
new file mode 100644
index 0000000..f3085a3
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sheet/sheet-portal.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/sheet/sheet-title.svelte b/apps/web/src/lib/components/ui/sheet/sheet-title.svelte
new file mode 100644
index 0000000..10c1717
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sheet/sheet-title.svelte
@@ -0,0 +1,17 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/sheet/sheet-trigger.svelte b/apps/web/src/lib/components/ui/sheet/sheet-trigger.svelte
new file mode 100644
index 0000000..e266975
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sheet/sheet-trigger.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/sheet/sheet.svelte b/apps/web/src/lib/components/ui/sheet/sheet.svelte
new file mode 100644
index 0000000..5bf9783
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sheet/sheet.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/sidebar/constants.ts b/apps/web/src/lib/components/ui/sidebar/constants.ts
new file mode 100644
index 0000000..1347e83
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/constants.ts
@@ -0,0 +1,6 @@
+export const SIDEBAR_COOKIE_NAME = "sidebar_state";
+export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
+export const SIDEBAR_WIDTH = "16rem";
+export const SIDEBAR_WIDTH_MOBILE = "18rem";
+export const SIDEBAR_WIDTH_ICON = "3rem";
+export const SIDEBAR_KEYBOARD_SHORTCUT = "b";
diff --git a/apps/web/src/lib/components/ui/sidebar/context.svelte.ts b/apps/web/src/lib/components/ui/sidebar/context.svelte.ts
new file mode 100644
index 0000000..d1f4562
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/context.svelte.ts
@@ -0,0 +1,79 @@
+import { getContext, setContext } from "svelte";
+import { IsMobile } from "$lib/hooks/is-mobile.svelte.js";
+import { SIDEBAR_KEYBOARD_SHORTCUT } from "./constants.js";
+
+type Getter = () => T;
+
+export type SidebarStateProps = {
+ /**
+ * A getter function that returns the current open state of the sidebar.
+ * We use a getter function here to support `bind:open` on the `Sidebar.Provider`
+ * component.
+ */
+ open: Getter;
+
+ /**
+ * A function that sets the open state of the sidebar. To support `bind:open`, we need
+ * a source of truth for changing the open state to ensure it will be synced throughout
+ * the sub-components and any `bind:` references.
+ */
+ setOpen: (open: boolean) => void;
+};
+
+class SidebarState {
+ readonly props: SidebarStateProps;
+ open = $derived.by(() => this.props.open());
+ openMobile = $state(false);
+ setOpen: SidebarStateProps["setOpen"];
+ #isMobile: IsMobile;
+ state = $derived.by(() => (this.open ? "expanded" : "collapsed"));
+
+ constructor(props: SidebarStateProps) {
+ this.setOpen = props.setOpen;
+ this.#isMobile = new IsMobile();
+ this.props = props;
+ }
+
+ // Convenience getter for checking if the sidebar is mobile
+ // without this, we would need to use `sidebar.isMobile.current` everywhere
+ get isMobile() {
+ return this.#isMobile.current;
+ }
+
+ // Event handler to apply to the ``
+ handleShortcutKeydown = (e: KeyboardEvent) => {
+ if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) {
+ e.preventDefault();
+ this.toggle();
+ }
+ };
+
+ setOpenMobile = (value: boolean) => {
+ this.openMobile = value;
+ };
+
+ toggle = () => {
+ return this.#isMobile.current ? (this.openMobile = !this.openMobile) : this.setOpen(!this.open);
+ };
+}
+
+const SYMBOL_KEY = "scn-sidebar";
+
+/**
+ * Instantiates a new `SidebarState` instance and sets it in the context.
+ *
+ * @param props The constructor props for the `SidebarState` class.
+ * @returns The `SidebarState` instance.
+ */
+export function setSidebar(props: SidebarStateProps): SidebarState {
+ return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props));
+}
+
+/**
+ * Retrieves the `SidebarState` instance from the context. This is a class instance,
+ * so you cannot destructure it.
+ * @returns The `SidebarState` instance.
+ */
+export function useSidebar(): SidebarState {
+ return getContext(Symbol.for(SYMBOL_KEY));
+}
diff --git a/apps/web/src/lib/components/ui/sidebar/index.ts b/apps/web/src/lib/components/ui/sidebar/index.ts
new file mode 100644
index 0000000..6e65d82
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/index.ts
@@ -0,0 +1,75 @@
+import Content from "./sidebar-content.svelte";
+import Footer from "./sidebar-footer.svelte";
+import GroupAction from "./sidebar-group-action.svelte";
+import GroupContent from "./sidebar-group-content.svelte";
+import GroupLabel from "./sidebar-group-label.svelte";
+import Group from "./sidebar-group.svelte";
+import Header from "./sidebar-header.svelte";
+import Input from "./sidebar-input.svelte";
+import Inset from "./sidebar-inset.svelte";
+import MenuAction from "./sidebar-menu-action.svelte";
+import MenuBadge from "./sidebar-menu-badge.svelte";
+import MenuButton from "./sidebar-menu-button.svelte";
+import MenuItem from "./sidebar-menu-item.svelte";
+import MenuSkeleton from "./sidebar-menu-skeleton.svelte";
+import MenuSubButton from "./sidebar-menu-sub-button.svelte";
+import MenuSubItem from "./sidebar-menu-sub-item.svelte";
+import MenuSub from "./sidebar-menu-sub.svelte";
+import Menu from "./sidebar-menu.svelte";
+import Provider from "./sidebar-provider.svelte";
+import Rail from "./sidebar-rail.svelte";
+import Separator from "./sidebar-separator.svelte";
+import Trigger from "./sidebar-trigger.svelte";
+import Root from "./sidebar.svelte";
+import { useSidebar } from "./context.svelte.js";
+
+export {
+ Content,
+ Footer,
+ Group,
+ GroupAction,
+ GroupContent,
+ GroupLabel,
+ Header,
+ Input,
+ Inset,
+ Menu,
+ MenuAction,
+ MenuBadge,
+ MenuButton,
+ MenuItem,
+ MenuSkeleton,
+ MenuSub,
+ MenuSubButton,
+ MenuSubItem,
+ Provider,
+ Rail,
+ Root,
+ Separator,
+ //
+ Root as Sidebar,
+ Content as SidebarContent,
+ Footer as SidebarFooter,
+ Group as SidebarGroup,
+ GroupAction as SidebarGroupAction,
+ GroupContent as SidebarGroupContent,
+ GroupLabel as SidebarGroupLabel,
+ Header as SidebarHeader,
+ Input as SidebarInput,
+ Inset as SidebarInset,
+ Menu as SidebarMenu,
+ MenuAction as SidebarMenuAction,
+ MenuBadge as SidebarMenuBadge,
+ MenuButton as SidebarMenuButton,
+ MenuItem as SidebarMenuItem,
+ MenuSkeleton as SidebarMenuSkeleton,
+ MenuSub as SidebarMenuSub,
+ MenuSubButton as SidebarMenuSubButton,
+ MenuSubItem as SidebarMenuSubItem,
+ Provider as SidebarProvider,
+ Rail as SidebarRail,
+ Separator as SidebarSeparator,
+ Trigger as SidebarTrigger,
+ Trigger,
+ useSidebar,
+};
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-content.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-content.svelte
new file mode 100644
index 0000000..4747f64
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-content.svelte
@@ -0,0 +1,24 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-footer.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-footer.svelte
new file mode 100644
index 0000000..2d2131d
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-footer.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-group-action.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-group-action.svelte
new file mode 100644
index 0000000..1d07b97
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-group-action.svelte
@@ -0,0 +1,33 @@
+
+
+{#if child}
+ {@render child({ props: mergedProps })}
+{:else}
+
+ {@render children?.()}
+
+{/if}
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-group-content.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-group-content.svelte
new file mode 100644
index 0000000..7835b0e
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-group-content.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-group-label.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-group-label.svelte
new file mode 100644
index 0000000..518216c
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-group-label.svelte
@@ -0,0 +1,33 @@
+
+
+{#if child}
+ {@render child({ props: mergedProps })}
+{:else}
+
+ {@render children?.()}
+
+{/if}
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-group.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-group.svelte
new file mode 100644
index 0000000..1d009d6
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-group.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-header.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-header.svelte
new file mode 100644
index 0000000..3797156
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-header.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-input.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-input.svelte
new file mode 100644
index 0000000..a450b95
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-input.svelte
@@ -0,0 +1,21 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-inset.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-inset.svelte
new file mode 100644
index 0000000..19a787c
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-inset.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-menu-action.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-menu-action.svelte
new file mode 100644
index 0000000..87547c0
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-menu-action.svelte
@@ -0,0 +1,37 @@
+
+
+{#if child}
+ {@render child({ props: mergedProps })}
+{:else}
+
+ {@render children?.()}
+
+{/if}
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte
new file mode 100644
index 0000000..6cecdb4
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte
@@ -0,0 +1,24 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-menu-button.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-menu-button.svelte
new file mode 100644
index 0000000..ac109a9
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-menu-button.svelte
@@ -0,0 +1,100 @@
+
+
+
+
+{#snippet Button({ props }: { props?: Record })}
+ {@const mergedProps = mergeProps(buttonProps, props)}
+ {#if child}
+ {@render child({ props: mergedProps })}
+ {:else}
+
+ {@render children?.()}
+
+ {/if}
+{/snippet}
+
+{#if !tooltipContent}
+ {@render Button({})}
+{:else}
+
+
+ {#snippet child({ props })}
+ {@render Button({ props })}
+ {/snippet}
+
+
+ {#if typeof tooltipContent === "string"}
+ {tooltipContent}
+ {:else if tooltipContent}
+ {@render tooltipContent()}
+ {/if}
+
+
+{/if}
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-menu-item.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-menu-item.svelte
new file mode 100644
index 0000000..4db4453
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-menu-item.svelte
@@ -0,0 +1,21 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte
new file mode 100644
index 0000000..cfd4433
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte
@@ -0,0 +1,36 @@
+
+
+
+ {#if showIcon}
+
+ {/if}
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte
new file mode 100644
index 0000000..09ff228
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte
@@ -0,0 +1,39 @@
+
+
+{#if child}
+ {@render child({ props: mergedProps })}
+{:else}
+
+ {@render children?.()}
+
+{/if}
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte
new file mode 100644
index 0000000..681d0f1
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte
@@ -0,0 +1,21 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte
new file mode 100644
index 0000000..29614ee
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte
@@ -0,0 +1,21 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-menu.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-menu.svelte
new file mode 100644
index 0000000..a8cdefd
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-menu.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-provider.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-provider.svelte
new file mode 100644
index 0000000..edcc2d9
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-provider.svelte
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+ {@render children?.()}
+
+
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-rail.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-rail.svelte
new file mode 100644
index 0000000..6b44746
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-rail.svelte
@@ -0,0 +1,36 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-separator.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-separator.svelte
new file mode 100644
index 0000000..18d791b
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-separator.svelte
@@ -0,0 +1,19 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar-trigger.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar-trigger.svelte
new file mode 100644
index 0000000..93fc9af
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar-trigger.svelte
@@ -0,0 +1,36 @@
+
+
+ {
+ onclick?.(e);
+ sidebar.toggle();
+ }}
+ {...restProps}
+>
+
+ Toggle Sidebar
+
diff --git a/apps/web/src/lib/components/ui/sidebar/sidebar.svelte b/apps/web/src/lib/components/ui/sidebar/sidebar.svelte
new file mode 100644
index 0000000..bd0a664
--- /dev/null
+++ b/apps/web/src/lib/components/ui/sidebar/sidebar.svelte
@@ -0,0 +1,108 @@
+
+
+{#if collapsible === 'none'}
+
+ {@render children?.()}
+
+{:else if sidebar.isMobile}
+ sidebar.openMobile, (v) => sidebar.setOpenMobile(v)}
+ {...restProps}
+ >
+ button]:hidden',
+ className,
+ )}
+ style="--sidebar-width: {SIDEBAR_WIDTH_MOBILE};"
+ {side}
+ >
+
+ Điều hướng quản trị
+ Liên kết đến các trang quản trị của AI Platform.
+
+
+ {@render children?.()}
+
+
+
+{:else}
+
+{/if}
diff --git a/apps/web/src/lib/components/ui/skeleton/index.ts b/apps/web/src/lib/components/ui/skeleton/index.ts
new file mode 100644
index 0000000..186db21
--- /dev/null
+++ b/apps/web/src/lib/components/ui/skeleton/index.ts
@@ -0,0 +1,7 @@
+import Root from "./skeleton.svelte";
+
+export {
+ Root,
+ //
+ Root as Skeleton,
+};
diff --git a/apps/web/src/lib/components/ui/skeleton/skeleton.svelte b/apps/web/src/lib/components/ui/skeleton/skeleton.svelte
new file mode 100644
index 0000000..1a940ee
--- /dev/null
+++ b/apps/web/src/lib/components/ui/skeleton/skeleton.svelte
@@ -0,0 +1,17 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/table/index.ts b/apps/web/src/lib/components/ui/table/index.ts
new file mode 100644
index 0000000..841ee58
--- /dev/null
+++ b/apps/web/src/lib/components/ui/table/index.ts
@@ -0,0 +1,28 @@
+import Body from "./table-body.svelte";
+import Caption from "./table-caption.svelte";
+import Cell from "./table-cell.svelte";
+import Footer from "./table-footer.svelte";
+import Head from "./table-head.svelte";
+import Header from "./table-header.svelte";
+import Row from "./table-row.svelte";
+import Root from "./table.svelte";
+
+export {
+ Root,
+ Body,
+ Caption,
+ Cell,
+ Footer,
+ Head,
+ Header,
+ Row,
+ //
+ Root as Table,
+ Body as TableBody,
+ Caption as TableCaption,
+ Cell as TableCell,
+ Footer as TableFooter,
+ Head as TableHead,
+ Header as TableHeader,
+ Row as TableRow,
+};
diff --git a/apps/web/src/lib/components/ui/table/table-body.svelte b/apps/web/src/lib/components/ui/table/table-body.svelte
new file mode 100644
index 0000000..935feae
--- /dev/null
+++ b/apps/web/src/lib/components/ui/table/table-body.svelte
@@ -0,0 +1,15 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/table/table-caption.svelte b/apps/web/src/lib/components/ui/table/table-caption.svelte
new file mode 100644
index 0000000..4696cff
--- /dev/null
+++ b/apps/web/src/lib/components/ui/table/table-caption.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/table/table-cell.svelte b/apps/web/src/lib/components/ui/table/table-cell.svelte
new file mode 100644
index 0000000..a998bf6
--- /dev/null
+++ b/apps/web/src/lib/components/ui/table/table-cell.svelte
@@ -0,0 +1,15 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/table/table-footer.svelte b/apps/web/src/lib/components/ui/table/table-footer.svelte
new file mode 100644
index 0000000..b9b14eb
--- /dev/null
+++ b/apps/web/src/lib/components/ui/table/table-footer.svelte
@@ -0,0 +1,20 @@
+
+
+tr]:last:border-b-0", className)}
+ {...restProps}
+>
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/table/table-head.svelte b/apps/web/src/lib/components/ui/table/table-head.svelte
new file mode 100644
index 0000000..267c4e0
--- /dev/null
+++ b/apps/web/src/lib/components/ui/table/table-head.svelte
@@ -0,0 +1,15 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/table/table-header.svelte b/apps/web/src/lib/components/ui/table/table-header.svelte
new file mode 100644
index 0000000..f47d259
--- /dev/null
+++ b/apps/web/src/lib/components/ui/table/table-header.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/table/table-row.svelte b/apps/web/src/lib/components/ui/table/table-row.svelte
new file mode 100644
index 0000000..c7b05c8
--- /dev/null
+++ b/apps/web/src/lib/components/ui/table/table-row.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
diff --git a/apps/web/src/lib/components/ui/table/table.svelte b/apps/web/src/lib/components/ui/table/table.svelte
new file mode 100644
index 0000000..8207a8a
--- /dev/null
+++ b/apps/web/src/lib/components/ui/table/table.svelte
@@ -0,0 +1,17 @@
+
+
+
+
+ {@render children?.()}
+
+
diff --git a/apps/web/src/lib/components/ui/textarea/index.ts b/apps/web/src/lib/components/ui/textarea/index.ts
new file mode 100644
index 0000000..ace797a
--- /dev/null
+++ b/apps/web/src/lib/components/ui/textarea/index.ts
@@ -0,0 +1,7 @@
+import Root from "./textarea.svelte";
+
+export {
+ Root,
+ //
+ Root as Textarea,
+};
diff --git a/apps/web/src/lib/components/ui/textarea/textarea.svelte b/apps/web/src/lib/components/ui/textarea/textarea.svelte
new file mode 100644
index 0000000..5d54a04
--- /dev/null
+++ b/apps/web/src/lib/components/ui/textarea/textarea.svelte
@@ -0,0 +1,22 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/tooltip/index.ts b/apps/web/src/lib/components/ui/tooltip/index.ts
new file mode 100644
index 0000000..d7d2f13
--- /dev/null
+++ b/apps/web/src/lib/components/ui/tooltip/index.ts
@@ -0,0 +1,19 @@
+import Content from "./tooltip-content.svelte";
+import Portal from "./tooltip-portal.svelte";
+import Provider from "./tooltip-provider.svelte";
+import Trigger from "./tooltip-trigger.svelte";
+import Root from "./tooltip.svelte";
+
+export {
+ Root,
+ Trigger,
+ Content,
+ Provider,
+ Portal,
+ //
+ Root as Tooltip,
+ Content as TooltipContent,
+ Trigger as TooltipTrigger,
+ Provider as TooltipProvider,
+ Portal as TooltipPortal,
+};
diff --git a/apps/web/src/lib/components/ui/tooltip/tooltip-content.svelte b/apps/web/src/lib/components/ui/tooltip/tooltip-content.svelte
new file mode 100644
index 0000000..d997e22
--- /dev/null
+++ b/apps/web/src/lib/components/ui/tooltip/tooltip-content.svelte
@@ -0,0 +1,48 @@
+
+
+
+
+ {@render children?.()}
+
+ {#snippet child({ props })}
+
+ {/snippet}
+
+
+
diff --git a/apps/web/src/lib/components/ui/tooltip/tooltip-portal.svelte b/apps/web/src/lib/components/ui/tooltip/tooltip-portal.svelte
new file mode 100644
index 0000000..d234f7d
--- /dev/null
+++ b/apps/web/src/lib/components/ui/tooltip/tooltip-portal.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/tooltip/tooltip-provider.svelte b/apps/web/src/lib/components/ui/tooltip/tooltip-provider.svelte
new file mode 100644
index 0000000..6dba9a6
--- /dev/null
+++ b/apps/web/src/lib/components/ui/tooltip/tooltip-provider.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/apps/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte
new file mode 100644
index 0000000..c4b21fc
--- /dev/null
+++ b/apps/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/apps/web/src/lib/components/ui/tooltip/tooltip.svelte b/apps/web/src/lib/components/ui/tooltip/tooltip.svelte
new file mode 100644
index 0000000..03c9a3d
--- /dev/null
+++ b/apps/web/src/lib/components/ui/tooltip/tooltip.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/apps/web/src/lib/hooks/is-mobile.svelte.ts b/apps/web/src/lib/hooks/is-mobile.svelte.ts
new file mode 100644
index 0000000..2dfb0eb
--- /dev/null
+++ b/apps/web/src/lib/hooks/is-mobile.svelte.ts
@@ -0,0 +1,9 @@
+import { MediaQuery } from 'svelte/reactivity';
+
+const DEFAULT_MOBILE_BREAKPOINT = 768;
+
+export class IsMobile extends MediaQuery {
+ constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) {
+ super(`max-width: ${breakpoint - 1}px`);
+ }
+}
diff --git a/apps/web/src/lib/server/backend.test.ts b/apps/web/src/lib/server/backend.test.ts
new file mode 100644
index 0000000..699c406
--- /dev/null
+++ b/apps/web/src/lib/server/backend.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, it } from 'vitest';
+import { sessionFromSetCookie } from './backend';
+
+describe('server-only admin session relay', () => {
+ it('extracts only the backend session cookie', () => {
+ expect(sessionFromSetCookie('ai_platform_session=opaque-123; HttpOnly; Path=/')).toBe(
+ 'opaque-123',
+ );
+ expect(sessionFromSetCookie('unrelated=value; Path=/')).toBeNull();
+ expect(sessionFromSetCookie(null)).toBeNull();
+ });
+});
diff --git a/apps/web/src/lib/server/backend.ts b/apps/web/src/lib/server/backend.ts
new file mode 100644
index 0000000..aa8b6fd
--- /dev/null
+++ b/apps/web/src/lib/server/backend.ts
@@ -0,0 +1,57 @@
+import { env } from '$env/dynamic/private';
+import type { RequestEvent } from '@sveltejs/kit';
+
+const sessionCookie = 'ap_session';
+const csrfCookie = 'ap_csrf';
+
+export function sessionFromSetCookie(header: string | null): string | null {
+ const match = header?.match(/(?:^|,\s*)ai_platform_session=([^;\s,]+)/);
+ return match ? match[1] : null;
+}
+
+export function setAdminSession(event: RequestEvent, value: string, csrf: string): void {
+ const options = {
+ path: '/',
+ httpOnly: true,
+ sameSite: 'strict' as const,
+ secure: event.url.protocol === 'https:',
+ maxAge: 8 * 60 * 60,
+ };
+ event.cookies.set(sessionCookie, value, options);
+ event.cookies.set(csrfCookie, csrf, options);
+}
+
+export function clearAdminSession(event: RequestEvent): void {
+ event.cookies.delete(sessionCookie, { path: '/' });
+ event.cookies.delete(csrfCookie, { path: '/' });
+}
+
+export async function backend(
+ event: RequestEvent,
+ path: `/v1/${string}`,
+ init: RequestInit = {},
+): Promise {
+ const base = env.BACKEND_INTERNAL_URL;
+ if (!base || !/^https?:\/\//.test(base)) {
+ throw new Error('BACKEND_INTERNAL_URL must be configured on the server');
+ }
+ const headers = new Headers(init.headers);
+ const token = event.cookies.get(sessionCookie);
+ if (token) headers.set('cookie', `ai_platform_session=${token}`);
+ const csrf = event.cookies.get(csrfCookie);
+ if (csrf && init.method && !['GET', 'HEAD'].includes(init.method.toUpperCase())) {
+ headers.set('x-csrf-token', csrf);
+ }
+ headers.set('accept', 'application/json');
+ return event.fetch(`${base.replace(/\/$/, '')}${path}`, { ...init, headers });
+}
+
+export async function backendJson(
+ event: RequestEvent,
+ path: `/v1/${string}`,
+ init: RequestInit = {},
+): Promise<{ response: Response; data: T | null }> {
+ const response = await backend(event, path, init);
+ if (!response.ok) return { response, data: null };
+ return { response, data: (await response.json()) as T };
+}
diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts
new file mode 100644
index 0000000..6d36933
--- /dev/null
+++ b/apps/web/src/lib/utils.ts
@@ -0,0 +1,5 @@
+export { cn } from 'cn';
+
+export type WithElementRef = T & { ref?: E | null };
+export type WithoutChildren = Omit;
+export type WithoutChildrenOrChild = Omit;
diff --git a/apps/web/src/routes/(admin)/+layout.server.ts b/apps/web/src/routes/(admin)/+layout.server.ts
new file mode 100644
index 0000000..460e991
--- /dev/null
+++ b/apps/web/src/routes/(admin)/+layout.server.ts
@@ -0,0 +1,13 @@
+import { redirect } from '@sveltejs/kit';
+import { backendJson, clearAdminSession } from '$lib/server/backend';
+import type { LayoutServerLoad } from './$types';
+
+export const load: LayoutServerLoad = async (event) => {
+ if (!event.cookies.get('ap_session')) redirect(303, '/login');
+ const { response, data } = await backendJson<{ email: string }>(event, '/v1/admin/me');
+ if (!response.ok || !data) {
+ clearAdminSession(event);
+ redirect(303, '/login');
+ }
+ return { admin: data };
+};
diff --git a/apps/web/src/routes/(admin)/+layout.svelte b/apps/web/src/routes/(admin)/+layout.svelte
new file mode 100644
index 0000000..8e92442
--- /dev/null
+++ b/apps/web/src/routes/(admin)/+layout.svelte
@@ -0,0 +1,28 @@
+
+
+AI Platform · Quản trị
+
+
+
+
+
+
+ DotB AI Platform
+ Quản trị nội bộ
+
+
+ {@render children()}
+
+
+
diff --git a/apps/web/src/routes/(admin)/lead-scoring/ai-profiles/+page.server.ts b/apps/web/src/routes/(admin)/lead-scoring/ai-profiles/+page.server.ts
new file mode 100644
index 0000000..861dcee
--- /dev/null
+++ b/apps/web/src/routes/(admin)/lead-scoring/ai-profiles/+page.server.ts
@@ -0,0 +1,42 @@
+import { error, fail } from '@sveltejs/kit';
+import { backend, backendJson } from '$lib/server/backend';
+import type { Actions, PageServerLoad } from './$types';
+
+export type AIProfile = {
+ version: string;
+ model: string;
+ system_prompt: string;
+ timeout_seconds: number;
+ active: boolean;
+};
+
+export const load: PageServerLoad = async (event) => {
+ const { response, data } = await backendJson<{ profiles: AIProfile[] }>(
+ event,
+ '/v1/admin/ai-profiles',
+ );
+ if (!response.ok || !data) error(502, 'Không tải được cấu hình AI.');
+ return { profiles: data.profiles };
+};
+
+export const actions: Actions = {
+ default: async (event) => {
+ const form = await event.request.formData();
+ const version = String(form.get('version') ?? '').trim();
+ const model = String(form.get('model') ?? '').trim();
+ const system_prompt = String(form.get('system_prompt') ?? '').trim();
+ const timeout_seconds = Number(form.get('timeout_seconds'));
+ if (!version || !model || !system_prompt || !Number.isFinite(timeout_seconds)) {
+ return fail(400, { message: 'Điền đầy đủ phiên bản, model, prompt và timeout.' });
+ }
+ const response = await backend(event, '/v1/admin/ai-profiles', {
+ method: 'PUT',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ version, model, system_prompt, timeout_seconds }),
+ });
+ if (response.status === 409)
+ return fail(409, { message: 'Phiên bản đã tồn tại với nội dung khác.' });
+ if (!response.ok) return fail(response.status, { message: 'Không lưu được cấu hình AI.' });
+ return { message: 'Đã kích hoạt phiên bản mới.', success: true };
+ },
+};
diff --git a/apps/web/src/routes/(admin)/lead-scoring/ai-profiles/+page.svelte b/apps/web/src/routes/(admin)/lead-scoring/ai-profiles/+page.svelte
new file mode 100644
index 0000000..0f72cef
--- /dev/null
+++ b/apps/web/src/routes/(admin)/lead-scoring/ai-profiles/+page.svelte
@@ -0,0 +1,121 @@
+
+
+{#snippet versionList()}
+
+ Phiên bản
+
+ {#if data.profiles.length === 0}
+
+ Chưa có cấu hình AI. Các lần chấm điểm chỉ dùng quy tắc không cần AI.
+
+ {:else}
+
+ {#each data.profiles as profile (profile.version)}
+
+ {profile.version}
+ {profile.model}
+ {#if profile.active}Đang dùng {/if}
+
+ {/each}
+
+ {/if}
+
+
+{/snippet}
+
+{#snippet profileEditor(prefix: string)}
+
+
+ Tạo và kích hoạt phiên bản
+ Tạo phiên bản mới thay vì sửa lịch sử của các lần chấm điểm đã chạy.
+
+
+
+
+
+{/snippet}
+
+
+ Lead Scoring / Cấu hình
+ Cấu hình AI
+
+ Quản lý model, prompt và thời gian chờ của AI Platform. Omni sở hữu quy tắc và trọng số;
+ thông tin xác thực nhà cung cấp, gồm V-Key, do AIHub quản lý và không nhập tại đây.
+
+
+
+ {@render versionList()}
+ {@render profileEditor('mobile')}
+
+
+
+ {@render versionList()}
+
+ {@render profileEditor('desktop')}
+
+
diff --git a/apps/web/src/routes/(admin)/lead-scoring/runs/+page.server.ts b/apps/web/src/routes/(admin)/lead-scoring/runs/+page.server.ts
new file mode 100644
index 0000000..04a428d
--- /dev/null
+++ b/apps/web/src/routes/(admin)/lead-scoring/runs/+page.server.ts
@@ -0,0 +1,18 @@
+import { error } from '@sveltejs/kit';
+import { backendJson } from '$lib/server/backend';
+import type { PageServerLoad } from './$types';
+
+export type RunSummary = {
+ run_id: string;
+ tenant_id: string;
+ status: string;
+ final_score: number | null;
+ degraded: boolean;
+ created_at: string | null;
+};
+
+export const load: PageServerLoad = async (event) => {
+ const { response, data } = await backendJson<{ runs: RunSummary[] }>(event, '/v1/admin/runs');
+ if (!response.ok || !data) error(502, 'Không tải được danh sách lần chấm điểm.');
+ return { runs: data.runs };
+};
diff --git a/apps/web/src/routes/(admin)/lead-scoring/runs/+page.svelte b/apps/web/src/routes/(admin)/lead-scoring/runs/+page.svelte
new file mode 100644
index 0000000..603abdd
--- /dev/null
+++ b/apps/web/src/routes/(admin)/lead-scoring/runs/+page.svelte
@@ -0,0 +1,106 @@
+
+
+
+
+ Gần đây
+
+ {#if data.runs.length === 0}
+
+ Chưa có lần chấm điểm nào.
+
+ {:else}
+
+ {#each data.runs as run (run.run_id)}
+
+
+
+
+ {run.status}{run.degraded ? ' · dự phòng' : ''}
+ Điểm: {run.final_score ?? '—'}
+
+ {run.created_at
+ ? new Date(run.created_at).toLocaleString('vi-VN')
+ : '—'}
+
+ {run.tenant_id}
+
+ Xem chi tiết
+
+
+
+ {/each}
+
+
+
+ Thời điểm Tenant Trạng thái Điểm Chi tiết
+
+ {#each data.runs as run (run.run_id)}
+
+ {run.created_at
+ ? new Date(run.created_at).toLocaleString('vi-VN')
+ : '—'}
+ {run.tenant_id}
+ {run.status}{run.degraded ? ' · dự phòng' : ''}
+ {run.final_score ?? '—'}
+ Xem chi tiết
+
+ {/each}
+
+
+
+ {/if}
+
+
diff --git a/apps/web/src/routes/(admin)/lead-scoring/runs/[run_id]/+page.server.ts b/apps/web/src/routes/(admin)/lead-scoring/runs/[run_id]/+page.server.ts
new file mode 100644
index 0000000..58116b7
--- /dev/null
+++ b/apps/web/src/routes/(admin)/lead-scoring/runs/[run_id]/+page.server.ts
@@ -0,0 +1,16 @@
+import { error } from '@sveltejs/kit';
+import { backendJson } from '$lib/server/backend';
+import type { components } from '$lib/generated/api';
+import type { PageServerLoad } from './$types';
+
+export type RunDetail = components['schemas']['RunDetail'];
+
+export const load: PageServerLoad = async (event) => {
+ const { response, data } = await backendJson(
+ event,
+ `/v1/admin/runs/${encodeURIComponent(event.params.run_id)}`,
+ );
+ if (response.status === 404) error(404, 'Không tìm thấy lần chấm điểm.');
+ if (!response.ok || !data) error(502, 'Không tải được lần chấm điểm.');
+ return { run: data };
+};
diff --git a/apps/web/src/routes/(admin)/lead-scoring/runs/[run_id]/+page.svelte b/apps/web/src/routes/(admin)/lead-scoring/runs/[run_id]/+page.svelte
new file mode 100644
index 0000000..004a258
--- /dev/null
+++ b/apps/web/src/routes/(admin)/lead-scoring/runs/[run_id]/+page.svelte
@@ -0,0 +1,170 @@
+
+
+← Danh sách
+
+
+ Điểm quy tắc {data.run.rule_score ?? '—'}
+ Điểm AI {data.run.ai_score ?? '—'}
+ Điểm cuối {data.run.final_score ?? '—'}
+
+
+
+
+ Trạng thái
+
+
+ Tenant
+ {data.run.tenant_id}
+ Trạng thái
+
+ {data.run.status}
+
+ Chế độ dự phòng
+ {data.run.degraded ? 'Có' : 'Không'}
+ Phiên bản AI
+ {data.run.profile_version ?? '—'}
+ Mã lỗi
+ {data.run.error_code ?? '—'}
+
+
+
+
+ Quy tắc đã áp dụng
+
+ {#if data.run.breakdown?.length}
+
+ {#each data.run.breakdown as item}
+
+ {item.rule_id}
+ {item.matched ? 'Khớp' : 'Không khớp'} · {item.points} điểm
+
+ {/each}
+
+ {:else}
+
+ Chưa có chi tiết quy tắc cho lần chấm điểm này.
+
+ {/if}
+
+
+
+
+
+ Cuộc trò chuyện
+ Hội thoại được gửi trong snapshot của lần chấm điểm.
+
+
+ {#if !data.run.snapshot}
+
+ Không có snapshot trong phản hồi này. Các lần chấm cũ có thể chưa cung cấp lịch sử
+ hội thoại.
+
+ {:else if !data.run.snapshot.conversations?.length}
+ Snapshot này không có cuộc trò chuyện.
+ {:else}
+
+ {#each data.run.snapshot.conversations as conversation, conversationIndex (`${conversation.conversation_id}-${conversationIndex}`)}
+
+
+ {#if conversation.messages?.length}
+
+ {#each conversation.messages as message (message.message_id)}
+
+
+ {senderLabels[message.sender_type]}
+ {formatTimestamp(message.sent_at)}
+
+
+ {message.content}
+
+
+ {/each}
+
+ {:else}
+
+ Cuộc trò chuyện này chưa có tin nhắn trong snapshot.
+
+ {/if}
+
+ {/each}
+
+ {/if}
+
+
diff --git a/apps/web/src/routes/(admin)/lead-scoring/runs/[run_id]/RunAnalysis.svelte b/apps/web/src/routes/(admin)/lead-scoring/runs/[run_id]/RunAnalysis.svelte
new file mode 100644
index 0000000..f8074c2
--- /dev/null
+++ b/apps/web/src/routes/(admin)/lead-scoring/runs/[run_id]/RunAnalysis.svelte
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+ Độ tin cậy
+ {confidence}
+ Cách tính
+ {resultState}
+ Mã lỗi
+ {run.error_code ?? '—'}
+
+
+
Lý do chấm điểm
+ {#if run.ai_reasoning}
+
+ {run.ai_reasoning}
+
+ {:else if run.status === 'failed' || run.degraded}
+
+ AI không hoàn tất lượt chấm này. Điểm cuối dùng kết quả quy tắc nếu có.
+
+ {:else if run.result_mode === 'rules_only'}
+
+ Lần chấm này chỉ dùng quy tắc, không yêu cầu kết quả AI.
+
+ {:else}
+
+ Chưa có lý do AI trong kết quả này.
+
+ {/if}
+
+
+
+
+
+
+
+ {#if run.ai_evidence?.length}
+
+ {:else if run.status === 'failed' || run.degraded}
+
+ Không có bằng chứng được lưu vì lượt AI dự phòng hoặc thất bại.
+ {#if run.error_code}Mã lỗi: {run.error_code}.{/if}
+
+ {:else if run.result_mode === 'rules_only'}
+
+ Không có bằng chứng AI vì lần chấm này chỉ dùng quy tắc.
+
+ {:else}
+
Không có bằng chứng AI trong phản hồi.
+ {/if}
+
+
+
+
+
+
+ {#each metricRows as metric (metric.label)}
+
+
{metric.label}
+ {metric.value}
+
+ {/each}
+
+
+
diff --git a/apps/web/src/routes/(admin)/lead-scoring/runs/[run_id]/RunAnalysis.test.ts b/apps/web/src/routes/(admin)/lead-scoring/runs/[run_id]/RunAnalysis.test.ts
new file mode 100644
index 0000000..7bc6e87
--- /dev/null
+++ b/apps/web/src/routes/(admin)/lead-scoring/runs/[run_id]/RunAnalysis.test.ts
@@ -0,0 +1,113 @@
+import { render } from 'svelte/server';
+import { describe, expect, it } from 'vitest';
+import type { components } from '$lib/generated/api';
+import RunAnalysis from './RunAnalysis.svelte';
+
+const successfulRun: components['schemas']['RunDetail'] = {
+ run_id: '7d5fe45b-0816-42ca-807a-3869a0227949',
+ request_id: '8a67f3d4-f2a4-40e1-9b9b-4e2913060cf1',
+ tenant_id: 'mock-tenant',
+ status: 'completed',
+ profile_version: 'medium-v1',
+ rule_score: 40,
+ ai_score: 80,
+ final_score: 70,
+ degraded: false,
+ result_mode: 'hybrid',
+ error_code: null,
+ breakdown: [],
+ created_at: null,
+ completed_at: null,
+ snapshot: {
+ captured_at: '2026-01-01T12:00:00Z',
+ lead_profile: { ems_lead_id: 'mock-lead' },
+ conversations: [],
+ },
+ metrics: {
+ profile_field_count: 2,
+ conversation_count: 2,
+ message_count: 3,
+ contact_message_count: 1,
+ customer_message_count: 1,
+ conversation_turn_count: 2,
+ contact_started_conversation_count: 1,
+ contact_message_recency_days: 0.5,
+ contact_message_recency_hours: 12,
+ customer_initiated_return: true,
+ },
+ ai_confidence: 0.91,
+ ai_reasoning: 'Thể hiện nhu cầu rõ ràng.',
+ ai_evidence: [
+ {
+ signal: 'Ý định đăng ký',
+ detail: 'Đã hỏi về hồ sơ đăng ký.',
+ source: 'mock-message-001',
+ },
+ ],
+};
+
+function renderAnalysis(run: components['schemas']['RunDetail']): string {
+ return render(RunAnalysis, { props: { run } }).body;
+}
+
+describe('run analysis', () => {
+ it('shows extracted evidence, AI result context, and backend metrics', () => {
+ const html = renderAnalysis(successfulRun);
+
+ expect(html).toContain('Ý định đăng ký');
+ expect(html).toContain('Đã hỏi về hồ sơ đăng ký.');
+ expect(html).toContain('mock-message-001');
+ expect(html).toContain('91%');
+ expect(html).toContain('Thể hiện nhu cầu rõ ràng.');
+ expect(html).toContain('AI và quy tắc');
+ expect(html).toContain('Thông tin hồ sơ có giá trị');
+ expect(html).toContain('Số lượt hội thoại');
+ expect(html).toContain('Hội thoại bắt đầu từ khách hàng');
+ expect(html).toContain('Số ngày từ tin nhắn khách hàng gần nhất');
+ expect(html).toContain('Số giờ từ tin nhắn khách hàng gần nhất');
+ expect(html).toContain('12 giờ');
+ expect(html).toContain('Hội thoại tiếp theo bắt đầu từ khách hàng');
+ expect(html).toContain('Có');
+ });
+
+ it('shows missing recency and a false return metric when the snapshot has no contact messages', () => {
+ const html = renderAnalysis({
+ ...successfulRun,
+ snapshot: {
+ ...successfulRun.snapshot,
+ conversations: [{ conversation_id: 'empty', messages: [] }],
+ },
+ metrics: {
+ ...successfulRun.metrics,
+ contact_message_count: 0,
+ customer_message_count: 0,
+ contact_message_recency_days: null,
+ contact_message_recency_hours: null,
+ customer_initiated_return: false,
+ },
+ });
+
+ expect(html).toContain('Số giờ từ tin nhắn khách hàng gần nhất');
+ expect(html).toContain('—');
+ expect(html).toContain('Hội thoại tiếp theo bắt đầu từ khách hàng');
+ expect(html).toContain('Không');
+ });
+
+ it('explains fallback and error state when extracted evidence is cleared', () => {
+ const html = renderAnalysis({
+ ...successfulRun,
+ ai_score: null,
+ ai_confidence: null,
+ ai_reasoning: null,
+ ai_evidence: null,
+ result_mode: 'rules_only_fallback',
+ degraded: true,
+ error_code: 'aihub_timeout',
+ });
+
+ expect(html).toContain('Dự phòng bằng điểm quy tắc');
+ expect(html).toContain('aihub_timeout');
+ expect(html).toContain('Không có bằng chứng được lưu vì lượt AI dự phòng hoặc thất bại.');
+ expect(html).not.toContain('Ý định đăng ký');
+ });
+});
diff --git a/apps/web/src/routes/(admin)/logout/+page.server.ts b/apps/web/src/routes/(admin)/logout/+page.server.ts
new file mode 100644
index 0000000..7ef035f
--- /dev/null
+++ b/apps/web/src/routes/(admin)/logout/+page.server.ts
@@ -0,0 +1,11 @@
+import { redirect } from '@sveltejs/kit';
+import { backend, clearAdminSession } from '$lib/server/backend';
+import type { Actions } from './$types';
+
+export const actions: Actions = {
+ default: async (event) => {
+ await backend(event, '/v1/admin/logout', { method: 'POST' });
+ clearAdminSession(event);
+ redirect(303, '/login');
+ },
+};
diff --git a/apps/web/src/routes/(admin)/logout/+page.svelte b/apps/web/src/routes/(admin)/logout/+page.svelte
new file mode 100644
index 0000000..0cf2e30
--- /dev/null
+++ b/apps/web/src/routes/(admin)/logout/+page.svelte
@@ -0,0 +1,2 @@
+Đăng xuất
+Bạn có thể đăng nhập lại .
diff --git a/apps/web/src/routes/+layout.svelte b/apps/web/src/routes/+layout.svelte
new file mode 100644
index 0000000..703889f
--- /dev/null
+++ b/apps/web/src/routes/+layout.svelte
@@ -0,0 +1,8 @@
+
+
+{@render children()}
diff --git a/apps/web/src/routes/+page.server.ts b/apps/web/src/routes/+page.server.ts
new file mode 100644
index 0000000..fd7b332
--- /dev/null
+++ b/apps/web/src/routes/+page.server.ts
@@ -0,0 +1,4 @@
+import { redirect } from '@sveltejs/kit';
+import type { PageServerLoad } from './$types';
+
+export const load: PageServerLoad = () => redirect(303, '/lead-scoring/runs');
diff --git a/apps/web/src/routes/login/+page.server.ts b/apps/web/src/routes/login/+page.server.ts
new file mode 100644
index 0000000..d8014ad
--- /dev/null
+++ b/apps/web/src/routes/login/+page.server.ts
@@ -0,0 +1,24 @@
+import { fail, redirect } from '@sveltejs/kit';
+import { backend, sessionFromSetCookie, setAdminSession } from '$lib/server/backend';
+import type { Actions } from './$types';
+
+export const actions: Actions = {
+ default: async (event) => {
+ const data = await event.request.formData();
+ const email = String(data.get('email') ?? '').trim();
+ const password = String(data.get('password') ?? '');
+ if (!email || !password) return fail(400, { message: 'Nhập email và mật khẩu.' });
+ const response = await backend(event, '/v1/admin/login', {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ email, password }),
+ });
+ if (!response.ok) return fail(401, { message: 'Thông tin đăng nhập không hợp lệ.' });
+ const session = sessionFromSetCookie(response.headers.get('set-cookie'));
+ const body = (await response.json()) as { csrf_token?: string };
+ if (!session || !body.csrf_token)
+ return fail(502, { message: 'Máy chủ không thể tạo phiên đăng nhập.' });
+ setAdminSession(event, session, body.csrf_token);
+ redirect(303, '/lead-scoring/runs');
+ },
+};
diff --git a/apps/web/src/routes/login/+page.svelte b/apps/web/src/routes/login/+page.svelte
new file mode 100644
index 0000000..a1a4017
--- /dev/null
+++ b/apps/web/src/routes/login/+page.svelte
@@ -0,0 +1,60 @@
+
+
+Đăng nhập | AI Platform
+
+
+
+
+
+
DotB AI Platform
+
+
+
Nội bộ
+
Đăng nhập quản trị
+
+
+
+
+
+ Email
+
+
+ Mật khẩu
+
+ {#if form?.message}
+ {form.message}
+
{/if}
+ Đăng nhập
+
+
+
+
diff --git a/apps/web/static/brand/dotb-mark.svg b/apps/web/static/brand/dotb-mark.svg
new file mode 100644
index 0000000..596ed1b
--- /dev/null
+++ b/apps/web/static/brand/dotb-mark.svg
@@ -0,0 +1,61 @@
+
+image/svg+xml
diff --git a/apps/web/svelte.config.js b/apps/web/svelte.config.js
new file mode 100644
index 0000000..b233db3
--- /dev/null
+++ b/apps/web/svelte.config.js
@@ -0,0 +1,12 @@
+import adapter from '@sveltejs/adapter-node';
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ preprocess: vitePreprocess(),
+ kit: {
+ adapter: adapter()
+ }
+};
+
+export default config;
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
new file mode 100644
index 0000000..4344710
--- /dev/null
+++ b/apps/web/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ "moduleResolution": "bundler"
+ }
+}
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
new file mode 100644
index 0000000..758ebfd
--- /dev/null
+++ b/apps/web/vite.config.ts
@@ -0,0 +1,8 @@
+import { sveltekit } from '@sveltejs/kit/vite';
+import tailwindcss from '@tailwindcss/vite';
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ plugins: [tailwindcss(), sveltekit()],
+ test: { include: ['src/**/*.test.ts'] }
+});
diff --git a/compose.yaml b/compose.yaml
index f3f0153..e472b3e 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -1,14 +1,76 @@
+name: ai-platform-local
+
services:
postgres:
image: postgres:16-alpine
environment:
- POSTGRES_USER: postgres
- POSTGRES_PASSWORD: postgres
- POSTGRES_DB: lead_scoring
+ POSTGRES_USER: ${POSTGRES_USER:-ai_platform}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-local-change-this-password}
+ POSTGRES_DB: ${POSTGRES_DB:-ai_platform}
ports:
- - "${POSTGRES_PORT:-5433}:5432"
+ - "127.0.0.1:${POSTGRES_PORT:-55432}:5432"
+ volumes:
+ - ai_platform_pgdata:/var/lib/postgresql/data
healthcheck:
- test: ["CMD-SHELL", "pg_isready -U postgres -d lead_scoring"]
+ test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
- retries: 10
+ retries: 12
+
+ migrate:
+ build:
+ context: .
+ dockerfile: Dockerfile.backend
+ command: ["uv", "run", "--no-sync", "alembic", "upgrade", "head"]
+ environment:
+ AI_PLATFORM_DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-ai_platform}:${POSTGRES_PASSWORD:-local-change-this-password}@postgres:5432/${POSTGRES_DB:-ai_platform}
+ AI_PLATFORM_ENVIRONMENT: ${AI_PLATFORM_ENVIRONMENT:-development}
+ depends_on:
+ postgres:
+ condition: service_healthy
+ restart: "no"
+
+ backend:
+ build:
+ context: .
+ dockerfile: Dockerfile.backend
+ environment:
+ AI_PLATFORM_DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-ai_platform}:${POSTGRES_PASSWORD:-local-change-this-password}@postgres:5432/${POSTGRES_DB:-ai_platform}
+ AI_PLATFORM_ENVIRONMENT: ${AI_PLATFORM_ENVIRONMENT:-development}
+ AI_PLATFORM_TENANT_PROVISIONING_SECRET: ${AI_PLATFORM_TENANT_PROVISIONING_SECRET:-local-provisioner-replace-before-use}
+ AI_PLATFORM_WEBHOOK_URL: ${AI_PLATFORM_WEBHOOK_URL:-}
+ AI_PLATFORM_WEBHOOK_SIGNING_SECRET: ${AI_PLATFORM_WEBHOOK_SIGNING_SECRET:-}
+ AI_PLATFORM_AIHUB_BASE_URL: ${AI_PLATFORM_AIHUB_BASE_URL:-}
+ ports:
+ - "127.0.0.1:${BACKEND_PORT:-8000}:8000"
+ depends_on:
+ postgres:
+ condition: service_healthy
+ migrate:
+ condition: service_completed_successfully
+ healthcheck:
+ test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready', timeout=2)"]
+ interval: 10s
+ timeout: 3s
+ retries: 6
+ start_period: 10s
+
+ web:
+ build:
+ context: .
+ dockerfile: apps/web/Dockerfile
+ environment:
+ NODE_ENV: production
+ HOST: 0.0.0.0
+ PORT: 3000
+ ORIGIN: ${WEB_ORIGIN:-http://localhost:3000}
+ BACKEND_INTERNAL_URL: http://backend:8000
+ ports:
+ - "127.0.0.1:${WEB_PORT:-3000}:3000"
+ depends_on:
+ backend:
+ condition: service_healthy
+
+volumes:
+ ai_platform_pgdata:
+ name: ai_platform_pgdata