From 7097fa6b44c8e4482bcf65d0c7393d9361271a67 Mon Sep 17 00:00:00 2001 From: xuyong Date: Wed, 15 Apr 2026 21:35:26 +0800 Subject: [PATCH] first commit --- .env.example | 24 + .gitignore | 20 + CLAUDE.md | 57 + Dockerfile | 24 + app/__init__.py | 0 app/api/__init__.py | 0 app/api/v1/__init__.py | 0 app/api/v1/auth.py | 54 + app/api/v1/example.py | 20 + app/api/v1/health.py | 8 + app/api/v1/keys.py | 39 + app/api/v1/models.py | 16 + app/api/v1/usage.py | 64 + app/api/v1/wallet.py | 42 + app/config/__init__.py | 0 app/config/settings.py | 45 + app/core/__init__.py | 0 app/core/database.py | 21 + app/core/dependencies.py | 25 + app/core/exceptions.py | 21 + app/core/security.py | 40 + app/datamodels/__init__.py | 0 app/datamodels/schemas.py | 179 ++ app/main.py | 63 + app/models/__init__.py | 87 + app/services/__init__.py | 0 app/services/auth_service.py | 91 + app/services/example_service.py | 24 + app/services/key_service.py | 84 + app/services/model_service.py | 16 + app/services/usage_service.py | 139 ++ app/services/wallet_service.py | 60 + docker-compose.yml | 13 + frontend/index.html | 56 + frontend/package-lock.json | 2285 +++++++++++++++++++++ frontend/package.json | 24 + frontend/src/App.tsx | 44 + frontend/src/components/AuthGuard.tsx | 9 + frontend/src/components/Header.tsx | 89 + frontend/src/components/StatusBar.tsx | 34 + frontend/src/hooks/useFetch.ts | 27 + frontend/src/index.tsx | 12 + frontend/src/pages/Docs.tsx | 160 ++ frontend/src/pages/ForgotPassword.tsx | 86 + frontend/src/pages/Home.tsx | 108 + frontend/src/pages/Login.tsx | 86 + frontend/src/pages/Pricing.tsx | 98 + frontend/src/pages/Register.tsx | 111 + frontend/src/pages/ResetPassword.tsx | 111 + frontend/src/pages/dashboard/Keys.tsx | 191 ++ frontend/src/pages/dashboard/Layout.tsx | 37 + frontend/src/pages/dashboard/Logs.tsx | 136 ++ frontend/src/pages/dashboard/Overview.tsx | 42 + frontend/src/pages/dashboard/Usage.tsx | 107 + frontend/src/pages/dashboard/Wallet.tsx | 139 ++ frontend/src/services/authService.ts | 34 + frontend/src/services/exampleService.ts | 9 + frontend/src/services/httpClient.ts | 36 + frontend/src/services/keyService.ts | 25 + frontend/src/services/usageService.ts | 80 + frontend/src/services/walletService.ts | 21 + frontend/src/stores/authStore.ts | 51 + frontend/src/stores/themeStore.ts | 39 + frontend/src/types.ts | 11 + frontend/tsconfig.json | 23 + frontend/tsconfig.tsbuildinfo | 1 + frontend/vite.config.ts | 23 + requirements.txt | 11 + run.py | 10 + 69 files changed, 5642 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 Dockerfile create mode 100644 app/__init__.py create mode 100644 app/api/__init__.py create mode 100644 app/api/v1/__init__.py create mode 100644 app/api/v1/auth.py create mode 100644 app/api/v1/example.py create mode 100644 app/api/v1/health.py create mode 100644 app/api/v1/keys.py create mode 100644 app/api/v1/models.py create mode 100644 app/api/v1/usage.py create mode 100644 app/api/v1/wallet.py create mode 100644 app/config/__init__.py create mode 100644 app/config/settings.py create mode 100644 app/core/__init__.py create mode 100644 app/core/database.py create mode 100644 app/core/dependencies.py create mode 100644 app/core/exceptions.py create mode 100644 app/core/security.py create mode 100644 app/datamodels/__init__.py create mode 100644 app/datamodels/schemas.py create mode 100644 app/main.py create mode 100644 app/models/__init__.py create mode 100644 app/services/__init__.py create mode 100644 app/services/auth_service.py create mode 100644 app/services/example_service.py create mode 100644 app/services/key_service.py create mode 100644 app/services/model_service.py create mode 100644 app/services/usage_service.py create mode 100644 app/services/wallet_service.py create mode 100644 docker-compose.yml create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/components/AuthGuard.tsx create mode 100644 frontend/src/components/Header.tsx create mode 100644 frontend/src/components/StatusBar.tsx create mode 100644 frontend/src/hooks/useFetch.ts create mode 100644 frontend/src/index.tsx create mode 100644 frontend/src/pages/Docs.tsx create mode 100644 frontend/src/pages/ForgotPassword.tsx create mode 100644 frontend/src/pages/Home.tsx create mode 100644 frontend/src/pages/Login.tsx create mode 100644 frontend/src/pages/Pricing.tsx create mode 100644 frontend/src/pages/Register.tsx create mode 100644 frontend/src/pages/ResetPassword.tsx create mode 100644 frontend/src/pages/dashboard/Keys.tsx create mode 100644 frontend/src/pages/dashboard/Layout.tsx create mode 100644 frontend/src/pages/dashboard/Logs.tsx create mode 100644 frontend/src/pages/dashboard/Overview.tsx create mode 100644 frontend/src/pages/dashboard/Usage.tsx create mode 100644 frontend/src/pages/dashboard/Wallet.tsx create mode 100644 frontend/src/services/authService.ts create mode 100644 frontend/src/services/exampleService.ts create mode 100644 frontend/src/services/httpClient.ts create mode 100644 frontend/src/services/keyService.ts create mode 100644 frontend/src/services/usageService.ts create mode 100644 frontend/src/services/walletService.ts create mode 100644 frontend/src/stores/authStore.ts create mode 100644 frontend/src/stores/themeStore.ts create mode 100644 frontend/src/types.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.tsbuildinfo create mode 100644 frontend/vite.config.ts create mode 100644 requirements.txt create mode 100644 run.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4788ee4 --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +# SuperDream Configuration + +# App +SD_APP_NAME=SuperDream +SD_DEBUG=false +SD_HOST=0.0.0.0 +SD_PORT=18000 + +# Database (MySQL) +SD_DB_TYPE=mysql +SD_DB_HOST=localhost +SD_DB_PORT=3306 +SD_DB_USER=root +SD_DB_PASSWORD= +SD_DB_NAME=superdream + +# JWT +SD_JWT_SECRET=superdream-secret-change-me +SD_JWT_ALGORITHM=HS256 +SD_JWT_ACCESS_EXPIRE_MINUTES=30 +SD_JWT_REFRESH_EXPIRE_DAYS=7 + +# Storage +SD_DATA_DIR=./data diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96c6b68 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Byte-compiled +__pycache__/ +*.py[cod] + +# Env +.env + +# Data +data/ + +# Frontend +frontend/node_modules/ +frontend/dist/ + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b38d1da --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,57 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +SuperDream is a full-stack web application with a Python/FastAPI backend and a React/TypeScript/Vite frontend. The backend serves a REST API and also serves the built frontend as a SPA in production. + +## Development Commands + +### Backend +```bash +pip install -r requirements.txt # Install Python dependencies +python run.py # Start backend dev server (port 18000, auto-reload) +``` + +### Frontend +```bash +cd frontend +npm install # Install frontend dependencies +npm run dev # Start Vite dev server (port 3000, proxies /api to backend) +npm run build # TypeScript check + production build to frontend/dist/ +``` + +### Docker +```bash +docker-compose up # Build and run full stack (maps port 18000:8000) +``` + +## Architecture + +### Backend (`/app`) + +- **Entry point:** `run.py` → starts uvicorn with `app.main:app` +- **App factory:** `app/main.py` → `create_app()` builds the FastAPI instance +- **Config:** `app/config/settings.py` — Pydantic BaseSettings with `SD_` env prefix, loads from `.env` +- **API routes:** `app/api/v1/` — versioned REST endpoints (health, example CRUD) +- **Services:** `app/services/` — business logic layer (currently in-memory storage) +- **Data models:** `app/datamodels/schemas.py` — Pydantic request/response schemas +- **Models:** `app/models/` — placeholder for ORM models + +### Frontend (`/frontend/src`) + +- **React 19 + TypeScript (strict mode) + Vite** +- **Path alias:** `@` → `frontend/src/` +- **Components:** `components/` — Header, StatusBar +- **Services:** `services/` — HTTP client wrapper and API service modules +- **Hooks:** `hooks/` — `useFetch` for data fetching with loading/error states +- **Styling:** Tailwind CSS via CDN, dark theme by default with custom SuperDream color palette + +### Request Flow + +In development, the frontend Vite dev server (port 3000) proxies `/api` and `/data/files` to the backend (port 18000). In production, FastAPI serves the built frontend from `frontend/dist/`, mounting `/assets` and falling back to `index.html` for SPA routing. + +## Configuration + +All backend settings use the `SD_` environment variable prefix (e.g., `SD_PORT`, `SD_DB_TYPE`). Copy `.env.example` to `.env` to configure. Database mode is either `"file"` or `"mysql"`. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8f111b3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +# Stage 1: Build frontend +FROM node:20-bookworm AS frontend-build +WORKDIR /frontend +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci +COPY frontend/ ./ +RUN npm run build + +# Stage 2: Python runtime +FROM python:3.12-slim +WORKDIR /backend + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app/ ./app/ +COPY run.py . + +# Copy frontend build output +COPY --from=frontend-build /frontend/dist ./frontend/dist + +EXPOSE 18000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "18000"] diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/v1/auth.py b/app/api/v1/auth.py new file mode 100644 index 0000000..ccb46cb --- /dev/null +++ b/app/api/v1/auth.py @@ -0,0 +1,54 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_db +from app.core.dependencies import get_current_user +from app.models import User +from app.services.auth_service import AuthService +from app.datamodels.schemas import ( + RegisterRequest, LoginRequest, TokenResponse, RefreshRequest, + ForgotPasswordRequest, ResetPasswordRequest, UserResponse, MessageResponse, +) + +router = APIRouter(prefix="/auth", tags=["auth"]) + + +@router.post("/register", response_model=UserResponse) +async def register(body: RegisterRequest, db: AsyncSession = Depends(get_db)): + user = await AuthService.register(db, body.email, body.password) + return user + + +@router.post("/login", response_model=TokenResponse) +async def login(body: LoginRequest, db: AsyncSession = Depends(get_db)): + return await AuthService.login(db, body.email, body.password) + + +@router.post("/refresh", response_model=TokenResponse) +async def refresh(body: RefreshRequest, db: AsyncSession = Depends(get_db)): + return await AuthService.refresh(db, body.refresh_token) + + +@router.post("/logout", response_model=MessageResponse) +async def logout(): + return {"message": "Logged out successfully"} + + +@router.post("/forgot-password", response_model=MessageResponse) +async def forgot_password(body: ForgotPasswordRequest, db: AsyncSession = Depends(get_db)): + token = await AuthService.forgot_password(db, body.email) + if token: + # MVP: print token to console; production: send email + print(f"[Password Reset] email={body.email} token={token}") + return {"message": "If the email exists, a reset link has been sent"} + + +@router.post("/reset-password", response_model=MessageResponse) +async def reset_password(body: ResetPasswordRequest, db: AsyncSession = Depends(get_db)): + await AuthService.reset_password(db, body.token, body.new_password) + return {"message": "Password reset successfully"} + + +@router.get("/me", response_model=UserResponse) +async def me(user: User = Depends(get_current_user)): + return user diff --git a/app/api/v1/example.py b/app/api/v1/example.py new file mode 100644 index 0000000..480fb62 --- /dev/null +++ b/app/api/v1/example.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter +from app.services.example_service import ExampleService + +router = APIRouter() +service = ExampleService() + + +@router.get("/examples") +async def list_examples(): + return await service.list_all() + + +@router.get("/examples/{example_id}") +async def get_example(example_id: str): + return await service.get_by_id(example_id) + + +@router.post("/examples") +async def create_example(data: dict): + return await service.create(data) \ No newline at end of file diff --git a/app/api/v1/health.py b/app/api/v1/health.py new file mode 100644 index 0000000..0b46958 --- /dev/null +++ b/app/api/v1/health.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.get("/health") +async def health_check(): + return {"status": "ok", "service": "SuperDream"} diff --git a/app/api/v1/keys.py b/app/api/v1/keys.py new file mode 100644 index 0000000..abd519d --- /dev/null +++ b/app/api/v1/keys.py @@ -0,0 +1,39 @@ +from typing import List + +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_db +from app.core.dependencies import get_current_user +from app.models import User +from app.services.key_service import KeyService +from app.datamodels.schemas import CreateKeyRequest, ApiKeyResponse, ApiKeyCreatedResponse, MessageResponse + +router = APIRouter(prefix="/keys", tags=["keys"]) + + +@router.get("", response_model=List[ApiKeyResponse]) +async def list_keys( + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + return await KeyService.list_keys(db, user.id) + + +@router.post("", response_model=ApiKeyCreatedResponse) +async def create_key( + body: CreateKeyRequest, + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + return await KeyService.create_key(db, user.id, body.name) + + +@router.delete("/{key_id}", response_model=MessageResponse) +async def delete_key( + key_id: str, + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + await KeyService.delete_key(db, user.id, key_id) + return {"message": "Key deleted"} diff --git a/app/api/v1/models.py b/app/api/v1/models.py new file mode 100644 index 0000000..e4c402e --- /dev/null +++ b/app/api/v1/models.py @@ -0,0 +1,16 @@ +from typing import List + +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_db +from app.services.model_service import ModelService +from app.datamodels.schemas import ModelPricingResponse + +router = APIRouter(prefix="/models", tags=["models"]) + + +@router.get("", response_model=List[ModelPricingResponse]) +async def list_models(db: AsyncSession = Depends(get_db)): + """Public endpoint: list available models and pricing.""" + return await ModelService.list_models(db) diff --git a/app/api/v1/usage.py b/app/api/v1/usage.py new file mode 100644 index 0000000..1fd2e78 --- /dev/null +++ b/app/api/v1/usage.py @@ -0,0 +1,64 @@ +from datetime import date +from typing import List, Optional + +from fastapi import APIRouter, Depends, Query +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_db +from app.core.dependencies import get_current_user +from app.models import User +from app.services.usage_service import UsageService +from app.datamodels.schemas import ( + UsageSummaryResponse, DailyUsageResponse, + ModelUsageResponse, KeyUsageResponse, UsageLogResponse, +) + +router = APIRouter(prefix="/usage", tags=["usage"]) + + +@router.get("/summary", response_model=UsageSummaryResponse) +async def usage_summary( + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + return await UsageService.summary(db, user.id) + + +@router.get("/daily", response_model=List[DailyUsageResponse]) +async def usage_daily( + start: Optional[date] = Query(None), + end: Optional[date] = Query(None), + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + return await UsageService.daily(db, user.id, start, end) + + +@router.get("/by-model", response_model=List[ModelUsageResponse]) +async def usage_by_model( + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + return await UsageService.by_model(db, user.id) + + +@router.get("/by-key", response_model=List[KeyUsageResponse]) +async def usage_by_key( + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + return await UsageService.by_key(db, user.id) + + +@router.get("/logs", response_model=List[UsageLogResponse]) +async def usage_logs( + page: int = Query(1, ge=1), + size: int = Query(20, ge=1, le=100), + model: Optional[str] = Query(None), + key_id: Optional[str] = Query(None), + start: Optional[date] = Query(None), + end: Optional[date] = Query(None), + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + return await UsageService.logs(db, user.id, page, size, model, key_id, start, end) diff --git a/app/api/v1/wallet.py b/app/api/v1/wallet.py new file mode 100644 index 0000000..58d1bd3 --- /dev/null +++ b/app/api/v1/wallet.py @@ -0,0 +1,42 @@ +from typing import List + +from fastapi import APIRouter, Depends, Query +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_db +from app.core.dependencies import get_current_user +from app.models import User +from app.services.wallet_service import WalletService +from app.datamodels.schemas import ( + BalanceResponse, RedeemCodeRequest, TransactionResponse, +) + +router = APIRouter(prefix="/wallet", tags=["wallet"]) + + +@router.get("/balance", response_model=BalanceResponse) +async def get_balance( + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + balance = await WalletService.get_balance(db, user.id) + return {"balance": balance} + + +@router.post("/redeem", response_model=TransactionResponse) +async def redeem_code( + body: RedeemCodeRequest, + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + return await WalletService.redeem_code(db, user.id, body.code) + + +@router.get("/transactions", response_model=List[TransactionResponse]) +async def list_transactions( + page: int = Query(1, ge=1), + size: int = Query(20, ge=1, le=100), + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + return await WalletService.list_transactions(db, user.id, page, size) diff --git a/app/config/__init__.py b/app/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/config/settings.py b/app/config/settings.py new file mode 100644 index 0000000..48d629e --- /dev/null +++ b/app/config/settings.py @@ -0,0 +1,45 @@ +from pydantic_settings import BaseSettings +from functools import lru_cache + + +class Settings(BaseSettings): + app_name: str = "SuperDream" + debug: bool = False + host: str = "0.0.0.0" + port: int = 18000 + + # Database + db_type: str = "mysql" + db_host: str = "10.11.0.43" + db_port: int = 3306 + db_user: str = "root" + db_password: str = "for_develop_only" + db_name: str = "superdream" + + # Storage + data_dir: str = "./data" + + # JWT + jwt_secret: str = "superdream-secret-change-me" + jwt_algorithm: str = "HS256" + jwt_access_expire_minutes: int = 30 + jwt_refresh_expire_days: int = 7 + + @property + def database_url(self) -> str: + return ( + f"mysql+aiomysql://{self.db_user}:{self.db_password}" + f"@{self.db_host}:{self.db_port}/{self.db_name}" + ) + + class Config: + env_file = ".env" + env_prefix = "SD_" + + +@lru_cache +def get_settings() -> Settings: + return Settings() + + +settings = get_settings() \ No newline at end of file diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/database.py b/app/core/database.py new file mode 100644 index 0000000..e5482b6 --- /dev/null +++ b/app/core/database.py @@ -0,0 +1,21 @@ +from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession +from sqlalchemy.orm import DeclarativeBase +from app.config.settings import settings + +engine = create_async_engine(settings.database_url, echo=settings.debug) +async_session = async_sessionmaker(engine, expire_on_commit=False) + + +class Base(DeclarativeBase): + pass + + +async def get_db() -> AsyncSession: + async with async_session() as session: + yield session + + +async def init_db(): + import app.models # noqa: F401 — ensure all models are registered + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) diff --git a/app/core/dependencies.py b/app/core/dependencies.py new file mode 100644 index 0000000..096487f --- /dev/null +++ b/app/core/dependencies.py @@ -0,0 +1,25 @@ +from fastapi import Depends +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_db # noqa: F401 +from app.core.security import decode_token +from app.core.exceptions import UnauthorizedError +from app.models import User + +security_scheme = HTTPBearer() + + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security_scheme), + db: AsyncSession = Depends(get_db), +) -> User: + payload = decode_token(credentials.credentials) + if not payload or payload.get("type") != "access": + raise UnauthorizedError("Invalid or expired token") + + user_id = payload.get("sub") + user = await db.get(User, user_id) + if not user or user.status != "active": + raise UnauthorizedError("User not found or disabled") + return user diff --git a/app/core/exceptions.py b/app/core/exceptions.py new file mode 100644 index 0000000..ee6163d --- /dev/null +++ b/app/core/exceptions.py @@ -0,0 +1,21 @@ +from fastapi import HTTPException + + +class NotFoundError(HTTPException): + def __init__(self, detail: str = "Resource not found"): + super().__init__(status_code=404, detail=detail) + + +class BadRequestError(HTTPException): + def __init__(self, detail: str = "Bad request"): + super().__init__(status_code=400, detail=detail) + + +class UnauthorizedError(HTTPException): + def __init__(self, detail: str = "Not authenticated"): + super().__init__(status_code=401, detail=detail, headers={"WWW-Authenticate": "Bearer"}) + + +class ForbiddenError(HTTPException): + def __init__(self, detail: str = "Forbidden"): + super().__init__(status_code=403, detail=detail) \ No newline at end of file diff --git a/app/core/security.py b/app/core/security.py new file mode 100644 index 0000000..54c8bdc --- /dev/null +++ b/app/core/security.py @@ -0,0 +1,40 @@ +from datetime import datetime, timedelta +from typing import Optional + +import jwt +import bcrypt + +from app.config.settings import settings + + +def hash_password(password: str) -> str: + return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() + + +def verify_password(password: str, hashed: str) -> bool: + return bcrypt.checkpw(password.encode(), hashed.encode()) + + +def create_access_token(user_id: str) -> str: + expire = datetime.utcnow() + timedelta(minutes=settings.jwt_access_expire_minutes) + payload = {"sub": user_id, "exp": expire, "type": "access"} + return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm) + + +def create_refresh_token(user_id: str) -> str: + expire = datetime.utcnow() + timedelta(days=settings.jwt_refresh_expire_days) + payload = {"sub": user_id, "exp": expire, "type": "refresh"} + return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm) + + +def create_reset_token(user_id: str) -> str: + expire = datetime.utcnow() + timedelta(hours=1) + payload = {"sub": user_id, "exp": expire, "type": "reset"} + return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm) + + +def decode_token(token: str) -> Optional[dict]: + try: + return jwt.decode(token, settings.jwt_secret, algorithms=[settings.jwt_algorithm]) + except jwt.PyJWTError: + return None diff --git a/app/datamodels/__init__.py b/app/datamodels/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/datamodels/schemas.py b/app/datamodels/schemas.py new file mode 100644 index 0000000..db8081d --- /dev/null +++ b/app/datamodels/schemas.py @@ -0,0 +1,179 @@ +from decimal import Decimal +from pydantic import BaseModel, EmailStr +from datetime import datetime, date +from typing import Optional, List + + +# ── Auth ── + +class RegisterRequest(BaseModel): + email: str + password: str + + +class LoginRequest(BaseModel): + email: str + password: str + + +class TokenResponse(BaseModel): + access_token: str + refresh_token: Optional[str] = None + token_type: str = "bearer" + + +class RefreshRequest(BaseModel): + refresh_token: str + + +class ForgotPasswordRequest(BaseModel): + email: str + + +class ResetPasswordRequest(BaseModel): + token: str + new_password: str + + +class UserResponse(BaseModel): + id: str + email: str + balance: Decimal + status: str + created_at: datetime + + class Config: + from_attributes = True + + +# ── API Key ── + +class CreateKeyRequest(BaseModel): + name: str = "" + + +class ApiKeyResponse(BaseModel): + id: str + name: str + key_prefix: str + key_suffix: str + status: str + created_at: datetime + + class Config: + from_attributes = True + + +class ApiKeyCreatedResponse(BaseModel): + id: str + name: str + key: str + key_prefix: str + key_suffix: str + created_at: datetime + + +# ── Wallet ── + +class RedeemCodeRequest(BaseModel): + code: str + + +class TransactionResponse(BaseModel): + id: str + type: str + amount: Decimal + balance_after: Decimal + reference_id: str + created_at: datetime + + class Config: + from_attributes = True + + +class BalanceResponse(BaseModel): + balance: Decimal + + +# ── Models ── + +class ModelPricingResponse(BaseModel): + id: int + model_name: str + provider: str + input_price_per_1k: Decimal + output_price_per_1k: Decimal + status: str + updated_at: datetime + + class Config: + from_attributes = True + + +# ── Example (legacy) ── + +class ExampleCreate(BaseModel): + name: str + description: str = "" + + +class ExampleResponse(BaseModel): + id: str + name: str + description: str + created_at: datetime + + +# ── Common ── + +class MessageResponse(BaseModel): + message: str + + +# ── Usage ── + +class UsageSummaryResponse(BaseModel): + today_tokens: int + today_cost: Decimal + month_tokens: int + month_cost: Decimal + total_requests: int + + +class DailyUsageResponse(BaseModel): + date: date + total_tokens: int + cost: Decimal + requests: int + + +class ModelUsageResponse(BaseModel): + model: str + total_tokens: int + cost: Decimal + requests: int + + +class KeyUsageResponse(BaseModel): + key_id: str + key_name: str + key_prefix: str + key_suffix: str + total_tokens: int + cost: Decimal + requests: int + + +class UsageLogResponse(BaseModel): + id: int + key_id: str + model: str + prompt_tokens: int + completion_tokens: int + total_tokens: int + cost: Decimal + request_time: datetime + status: str + + class Config: + from_attributes = True diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..7dea562 --- /dev/null +++ b/app/main.py @@ -0,0 +1,63 @@ +from contextlib import asynccontextmanager +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse +from app.api.v1 import health, example, auth, keys, models as models_api, wallet, usage +from app.config.settings import settings +from app.core.database import init_db +import os + + +@asynccontextmanager +async def lifespan(app: FastAPI): + await init_db() + yield + + +def create_app() -> FastAPI: + app = FastAPI( + title="SuperDream", + description="SuperDream API", + version="0.1.0", + lifespan=lifespan, + ) + + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + # Register API routers + app.include_router(health.router, prefix="/api/v1", tags=["health"]) + app.include_router(auth.router, prefix="/api/v1") + app.include_router(keys.router, prefix="/api/v1") + app.include_router(models_api.router, prefix="/api/v1") + app.include_router(wallet.router, prefix="/api/v1") + app.include_router(usage.router, prefix="/api/v1") + app.include_router(example.router, prefix="/api/v1", tags=["example"]) + + # Serve static files + static_dir = os.path.join(os.path.dirname(__file__), "static") + if os.path.exists(static_dir): + app.mount("/static", StaticFiles(directory=static_dir), name="static") + + # Serve frontend dist in production + frontend_dist = os.path.join(os.path.dirname(__file__), "..", "frontend", "dist") + if os.path.exists(frontend_dist): + app.mount("/assets", StaticFiles(directory=os.path.join(frontend_dist, "assets")), name="frontend-assets") + + @app.get("/{full_path:path}") + async def serve_frontend(full_path: str): + file_path = os.path.join(frontend_dist, full_path) + if os.path.isfile(file_path): + return FileResponse(file_path) + return FileResponse(os.path.join(frontend_dist, "index.html")) + + return app + + +app = create_app() \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..594e11b --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import uuid +from datetime import datetime +from decimal import Decimal +from typing import List + +from sqlalchemy import String, Text, Integer, BigInteger, DateTime, Numeric, Enum, ForeignKey, Index, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.database import Base + + +class User(Base): + __tablename__ = "users" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True) + password_hash: Mapped[str] = mapped_column(String(255), nullable=False) + balance: Mapped[Decimal] = mapped_column(Numeric(16, 6), default=Decimal("0")) + status: Mapped[str] = mapped_column(String(20), default="active") # active / disabled + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now()) + + api_keys: Mapped[List[ApiKey]] = relationship(back_populates="user") + transactions: Mapped[List[Transaction]] = relationship(back_populates="user") + + +class ApiKey(Base): + __tablename__ = "api_keys" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + user_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False, index=True) + name: Mapped[str] = mapped_column(String(100), default="") + key_hash: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) # SHA256 + key_prefix: Mapped[str] = mapped_column(String(10), nullable=False) + key_suffix: Mapped[str] = mapped_column(String(10), nullable=False) + status: Mapped[str] = mapped_column(String(20), default="active") # active / revoked + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) + + user: Mapped["User"] = relationship(back_populates="api_keys") + + +class UsageLog(Base): + __tablename__ = "usage_logs" + __table_args__ = ( + Index("ix_usage_user_time", "user_id", "request_time"), + Index("ix_usage_model", "user_id", "model"), + ) + + id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) + user_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False) + key_id: Mapped[str] = mapped_column(String(36), ForeignKey("api_keys.id"), nullable=False) + model: Mapped[str] = mapped_column(String(100), nullable=False) + prompt_tokens: Mapped[int] = mapped_column(Integer, default=0) + completion_tokens: Mapped[int] = mapped_column(Integer, default=0) + total_tokens: Mapped[int] = mapped_column(Integer, default=0) + cost: Mapped[Decimal] = mapped_column(Numeric(16, 6), default=Decimal("0")) + request_time: Mapped[datetime] = mapped_column(DateTime, nullable=False) + response_time: Mapped[datetime] = mapped_column(DateTime, nullable=True) + status: Mapped[str] = mapped_column(String(20), default="success") # success / error + + +class Transaction(Base): + __tablename__ = "transactions" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + user_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False, index=True) + type: Mapped[str] = mapped_column(String(20), nullable=False) # topup / consume / refund + amount: Mapped[Decimal] = mapped_column(Numeric(16, 6), nullable=False) + balance_after: Mapped[Decimal] = mapped_column(Numeric(16, 6), nullable=False) + reference_id: Mapped[str] = mapped_column(String(100), default="") + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) + + user: Mapped["User"] = relationship(back_populates="transactions") + + +class ModelPricing(Base): + __tablename__ = "models_pricing" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + model_name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False) + provider: Mapped[str] = mapped_column(String(50), nullable=False) + input_price_per_1k: Mapped[Decimal] = mapped_column(Numeric(16, 6), nullable=False) + output_price_per_1k: Mapped[Decimal] = mapped_column(Numeric(16, 6), nullable=False) + status: Mapped[str] = mapped_column(String(20), default="available") # available / offline + updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now()) diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/auth_service.py b/app/services/auth_service.py new file mode 100644 index 0000000..f7b8e6c --- /dev/null +++ b/app/services/auth_service.py @@ -0,0 +1,91 @@ +import re + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models import User +from app.core.security import hash_password, verify_password, create_access_token, create_refresh_token, decode_token +from app.core.exceptions import BadRequestError, UnauthorizedError + + +def _validate_password(password: str) -> None: + if len(password) < 6: + raise BadRequestError("密码至少 6 位") + has_letter = bool(re.search(r"[a-zA-Z]", password)) + has_digit = bool(re.search(r"[0-9]", password)) + if not (has_letter and has_digit): + raise BadRequestError("密码需同时包含字母和数字") + + +class AuthService: + + @staticmethod + async def register(db: AsyncSession, email: str, password: str) -> User: + _validate_password(password) + + existing = await db.execute(select(User).where(User.email == email)) + if existing.scalar_one_or_none(): + raise BadRequestError("Email already registered") + + user = User(email=email, password_hash=hash_password(password)) + db.add(user) + await db.commit() + await db.refresh(user) + return user + + @staticmethod + async def login(db: AsyncSession, email: str, password: str) -> dict: + result = await db.execute(select(User).where(User.email == email)) + user = result.scalar_one_or_none() + if not user or not verify_password(password, user.password_hash): + raise UnauthorizedError("Invalid email or password") + if user.status != "active": + raise UnauthorizedError("Account is disabled") + + return { + "access_token": create_access_token(user.id), + "refresh_token": create_refresh_token(user.id), + "token_type": "bearer", + } + + @staticmethod + async def refresh(db: AsyncSession, refresh_token: str) -> dict: + payload = decode_token(refresh_token) + if not payload or payload.get("type") != "refresh": + raise UnauthorizedError("Invalid refresh token") + + user = await db.get(User, payload["sub"]) + if not user or user.status != "active": + raise UnauthorizedError("User not found or disabled") + + return { + "access_token": create_access_token(user.id), + "token_type": "bearer", + } + + @staticmethod + async def forgot_password(db: AsyncSession, email: str) -> str: + """Generate a password reset token. MVP: returns the token directly (production: send via email).""" + result = await db.execute(select(User).where(User.email == email)) + user = result.scalar_one_or_none() + if not user: + # Don't reveal whether email exists + return "" + + from app.core.security import create_reset_token + return create_reset_token(user.id) + + @staticmethod + async def reset_password(db: AsyncSession, token: str, new_password: str) -> None: + _validate_password(new_password) + + payload = decode_token(token) + if not payload or payload.get("type") != "reset": + raise BadRequestError("Invalid or expired reset token") + + user = await db.get(User, payload["sub"]) + if not user: + raise BadRequestError("User not found") + + user.password_hash = hash_password(new_password) + await db.commit() diff --git a/app/services/example_service.py b/app/services/example_service.py new file mode 100644 index 0000000..e9a1888 --- /dev/null +++ b/app/services/example_service.py @@ -0,0 +1,24 @@ +import uuid +from datetime import datetime +from typing import Dict, List, Optional + + +class ExampleService: + def __init__(self): + self._store: Dict[str, dict] = {} + + async def list_all(self) -> List[dict]: + return list(self._store.values()) + + async def get_by_id(self, example_id: str) -> Optional[dict]: + return self._store.get(example_id) + + async def create(self, data: dict) -> dict: + example_id = str(uuid.uuid4()) + item = { + "id": example_id, + "created_at": datetime.utcnow().isoformat(), + **data, + } + self._store[example_id] = item + return item \ No newline at end of file diff --git a/app/services/key_service.py b/app/services/key_service.py new file mode 100644 index 0000000..15b1faf --- /dev/null +++ b/app/services/key_service.py @@ -0,0 +1,84 @@ +import hashlib +import secrets +import uuid + +from sqlalchemy import select, func +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models import ApiKey +from app.core.exceptions import BadRequestError, NotFoundError + +MAX_KEYS_PER_USER = 5 +KEY_PREFIX = "sk-sd-" + + +class KeyService: + + @staticmethod + def _generate_key() -> str: + return KEY_PREFIX + secrets.token_urlsafe(36) + + @staticmethod + def _hash_key(raw_key: str) -> str: + return hashlib.sha256(raw_key.encode()).hexdigest() + + @staticmethod + async def list_keys(db: AsyncSession, user_id: str) -> list: + result = await db.execute( + select(ApiKey) + .where(ApiKey.user_id == user_id, ApiKey.status == "active") + .order_by(ApiKey.created_at.desc()) + ) + return result.scalars().all() + + @staticmethod + async def create_key(db: AsyncSession, user_id: str, name: str = "") -> dict: + # Check limit + count_result = await db.execute( + select(func.count()).select_from(ApiKey) + .where(ApiKey.user_id == user_id, ApiKey.status == "active") + ) + count = count_result.scalar() + if count >= MAX_KEYS_PER_USER: + raise BadRequestError(f"最多创建 {MAX_KEYS_PER_USER} 个 Key") + + raw_key = KeyService._generate_key() + key_hash = KeyService._hash_key(raw_key) + + # prefix/suffix for masked display (after "sk-sd-") + body = raw_key[len(KEY_PREFIX):] + key_prefix = body[:4] + key_suffix = body[-4:] + + api_key = ApiKey( + id=str(uuid.uuid4()), + user_id=user_id, + name=name, + key_hash=key_hash, + key_prefix=key_prefix, + key_suffix=key_suffix, + ) + db.add(api_key) + await db.commit() + await db.refresh(api_key) + + return { + "id": api_key.id, + "name": api_key.name, + "key": raw_key, # only returned once + "key_prefix": key_prefix, + "key_suffix": key_suffix, + "created_at": api_key.created_at, + } + + @staticmethod + async def delete_key(db: AsyncSession, user_id: str, key_id: str) -> None: + result = await db.execute( + select(ApiKey).where(ApiKey.id == key_id, ApiKey.user_id == user_id) + ) + api_key = result.scalar_one_or_none() + if not api_key: + raise NotFoundError("Key not found") + + api_key.status = "revoked" + await db.commit() diff --git a/app/services/model_service.py b/app/services/model_service.py new file mode 100644 index 0000000..e8ad59d --- /dev/null +++ b/app/services/model_service.py @@ -0,0 +1,16 @@ +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models import ModelPricing + + +class ModelService: + + @staticmethod + async def list_models(db: AsyncSession) -> list: + result = await db.execute( + select(ModelPricing) + .where(ModelPricing.status == "available") + .order_by(ModelPricing.provider, ModelPricing.model_name) + ) + return result.scalars().all() diff --git a/app/services/usage_service.py b/app/services/usage_service.py new file mode 100644 index 0000000..f1dd7f9 --- /dev/null +++ b/app/services/usage_service.py @@ -0,0 +1,139 @@ +from datetime import date, datetime, timedelta +from decimal import Decimal +from typing import Optional + +from sqlalchemy import select, func, cast, Date +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models import UsageLog, ApiKey + + +class UsageService: + + @staticmethod + async def summary(db: AsyncSession, user_id: str) -> dict: + today_start = datetime.combine(date.today(), datetime.min.time()) + month_start = today_start.replace(day=1) + + # Today + today_row = (await db.execute( + select( + func.coalesce(func.sum(UsageLog.total_tokens), 0), + func.coalesce(func.sum(UsageLog.cost), Decimal("0")), + ).where(UsageLog.user_id == user_id, UsageLog.request_time >= today_start) + )).one() + + # This month + month_row = (await db.execute( + select( + func.coalesce(func.sum(UsageLog.total_tokens), 0), + func.coalesce(func.sum(UsageLog.cost), Decimal("0")), + func.count(), + ).where(UsageLog.user_id == user_id, UsageLog.request_time >= month_start) + )).one() + + return { + "today_tokens": int(today_row[0]), + "today_cost": today_row[1], + "month_tokens": int(month_row[0]), + "month_cost": month_row[1], + "total_requests": int(month_row[2]), + } + + @staticmethod + async def daily( + db: AsyncSession, user_id: str, + start: Optional[date] = None, end: Optional[date] = None, + ) -> list: + if not start: + start = date.today() - timedelta(days=29) + if not end: + end = date.today() + + day_col = cast(UsageLog.request_time, Date).label("day") + result = await db.execute( + select( + day_col, + func.coalesce(func.sum(UsageLog.total_tokens), 0), + func.coalesce(func.sum(UsageLog.cost), Decimal("0")), + func.count(), + ) + .where( + UsageLog.user_id == user_id, + cast(UsageLog.request_time, Date) >= start, + cast(UsageLog.request_time, Date) <= end, + ) + .group_by(day_col) + .order_by(day_col) + ) + return [ + {"date": row[0], "total_tokens": int(row[1]), "cost": row[2], "requests": int(row[3])} + for row in result.all() + ] + + @staticmethod + async def by_model(db: AsyncSession, user_id: str) -> list: + result = await db.execute( + select( + UsageLog.model, + func.coalesce(func.sum(UsageLog.total_tokens), 0), + func.coalesce(func.sum(UsageLog.cost), Decimal("0")), + func.count(), + ) + .where(UsageLog.user_id == user_id) + .group_by(UsageLog.model) + .order_by(func.sum(UsageLog.cost).desc()) + ) + return [ + {"model": row[0], "total_tokens": int(row[1]), "cost": row[2], "requests": int(row[3])} + for row in result.all() + ] + + @staticmethod + async def by_key(db: AsyncSession, user_id: str) -> list: + result = await db.execute( + select( + UsageLog.key_id, + ApiKey.name, + ApiKey.key_prefix, + ApiKey.key_suffix, + func.coalesce(func.sum(UsageLog.total_tokens), 0), + func.coalesce(func.sum(UsageLog.cost), Decimal("0")), + func.count(), + ) + .join(ApiKey, UsageLog.key_id == ApiKey.id) + .where(UsageLog.user_id == user_id) + .group_by(UsageLog.key_id, ApiKey.name, ApiKey.key_prefix, ApiKey.key_suffix) + .order_by(func.sum(UsageLog.cost).desc()) + ) + return [ + { + "key_id": row[0], "key_name": row[1] or "", + "key_prefix": row[2], "key_suffix": row[3], + "total_tokens": int(row[4]), "cost": row[5], "requests": int(row[6]), + } + for row in result.all() + ] + + @staticmethod + async def logs( + db: AsyncSession, user_id: str, + page: int = 1, size: int = 20, + model: Optional[str] = None, + key_id: Optional[str] = None, + start: Optional[date] = None, + end: Optional[date] = None, + ) -> list: + q = select(UsageLog).where(UsageLog.user_id == user_id) + if model: + q = q.where(UsageLog.model == model) + if key_id: + q = q.where(UsageLog.key_id == key_id) + if start: + q = q.where(UsageLog.request_time >= datetime.combine(start, datetime.min.time())) + if end: + q = q.where(UsageLog.request_time < datetime.combine(end + timedelta(days=1), datetime.min.time())) + + q = q.order_by(UsageLog.request_time.desc()).offset((page - 1) * size).limit(size) + result = await db.execute(q) + return result.scalars().all() diff --git a/app/services/wallet_service.py b/app/services/wallet_service.py new file mode 100644 index 0000000..9600b3d --- /dev/null +++ b/app/services/wallet_service.py @@ -0,0 +1,60 @@ +import uuid +from decimal import Decimal + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models import User, Transaction +from app.core.exceptions import BadRequestError + +# MVP: hardcoded redeem codes → amount mapping +REDEEM_CODES = { + "SUPERDREAM10": Decimal("10"), + "SUPERDREAM50": Decimal("50"), + "SUPERDREAM100": Decimal("100"), +} + + +class WalletService: + + @staticmethod + async def get_balance(db: AsyncSession, user_id: str) -> Decimal: + user = await db.get(User, user_id) + return user.balance + + @staticmethod + async def redeem_code(db: AsyncSession, user_id: str, code: str) -> Transaction: + amount = REDEEM_CODES.get(code.upper()) + if not amount: + raise BadRequestError("无效的兑换码") + + user = await db.get(User, user_id) + user.balance += amount + new_balance = user.balance + + txn = Transaction( + id=str(uuid.uuid4()), + user_id=user_id, + type="topup", + amount=amount, + balance_after=new_balance, + reference_id=f"redeem:{code.upper()}", + ) + db.add(txn) + await db.commit() + await db.refresh(txn) + return txn + + @staticmethod + async def list_transactions( + db: AsyncSession, user_id: str, page: int = 1, size: int = 20 + ) -> list: + offset = (page - 1) * size + result = await db.execute( + select(Transaction) + .where(Transaction.user_id == user_id) + .order_by(Transaction.created_at.desc()) + .offset(offset) + .limit(size) + ) + return result.scalars().all() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..208dc5b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +services: + backend: + container_name: superdream + build: + context: . + dockerfile: Dockerfile + ports: + - "18000:18000" + volumes: + - ./data:/backend/data + env_file: + - .env + restart: unless-stopped diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..7cf8c19 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,56 @@ + + + + + + SuperDream + + + + + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..376d23b --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2285 @@ +{ + "name": "superdream-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "superdream-frontend", + "version": "0.1.0", + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.14.1", + "recharts": "^3.8.1" + }, + "devDependencies": { + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz", + "integrity": "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.336", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.336.tgz", + "integrity": "sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/react-is": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz", + "integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.1.tgz", + "integrity": "sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.1.tgz", + "integrity": "sha512-ZkrQuwwhGibjQLqH1eCdyiZyLWglPxzxdl5tgwgKEyCSGC76vmAjleGocRe3J/MLfzMUIKwaFJWpFVJhK3d2xA==", + "license": "MIT", + "dependencies": { + "react-router": "7.14.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..d226721 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "superdream-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.14.1", + "recharts": "^3.8.1" + }, + "devDependencies": { + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..cce9caf --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,44 @@ +import { Routes, Route } from "react-router-dom"; +import Header from "./components/Header"; +import StatusBar from "./components/StatusBar"; +import AuthGuard from "./components/AuthGuard"; +import Home from "./pages/Home"; +import Pricing from "./pages/Pricing"; +import Docs from "./pages/Docs"; +import Login from "./pages/Login"; +import Register from "./pages/Register"; +import ForgotPassword from "./pages/ForgotPassword"; +import ResetPassword from "./pages/ResetPassword"; +import DashboardLayout from "./pages/dashboard/Layout"; +import Overview from "./pages/dashboard/Overview"; +import Keys from "./pages/dashboard/Keys"; +import Wallet from "./pages/dashboard/Wallet"; +import Usage from "./pages/dashboard/Usage"; +import Logs from "./pages/dashboard/Logs"; + +export default function App() { + return ( +
+
+ + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + }> + } /> + } /> + } /> + } /> + } /> + + + + +
+ ); +} diff --git a/frontend/src/components/AuthGuard.tsx b/frontend/src/components/AuthGuard.tsx new file mode 100644 index 0000000..9cb7810 --- /dev/null +++ b/frontend/src/components/AuthGuard.tsx @@ -0,0 +1,9 @@ +import { Navigate } from "react-router-dom"; +import { authStore } from "../stores/authStore"; + +export default function AuthGuard({ children }: { children: React.ReactNode }) { + if (!authStore.isLoggedIn()) { + return ; + } + return <>{children}; +} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx new file mode 100644 index 0000000..cb0a862 --- /dev/null +++ b/frontend/src/components/Header.tsx @@ -0,0 +1,89 @@ +import { Link, useNavigate } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { authStore } from "../stores/authStore"; +import { themeStore } from "../stores/themeStore"; + +interface HeaderProps { + title: string; +} + +export default function Header({ title }: HeaderProps) { + const navigate = useNavigate(); + const [loggedIn, setLoggedIn] = useState(authStore.isLoggedIn()); + const [user, setUser] = useState(authStore.getUser()); + const [dark, setDark] = useState(themeStore.isDark()); + + useEffect(() => { + if (authStore.isLoggedIn() && !authStore.getUser()) { + authStore.fetchUser(); + } + const unsub1 = authStore.subscribe(() => { + setLoggedIn(authStore.isLoggedIn()); + setUser(authStore.getUser()); + }); + const unsub2 = themeStore.subscribe(() => setDark(themeStore.isDark())); + return () => { unsub1(); unsub2(); }; + }, []); + + const handleLogout = () => { + authStore.clearTokens(); + navigate("/login"); + }; + + return ( +
+ + {title} + + +
+ ); +} diff --git a/frontend/src/components/StatusBar.tsx b/frontend/src/components/StatusBar.tsx new file mode 100644 index 0000000..4046196 --- /dev/null +++ b/frontend/src/components/StatusBar.tsx @@ -0,0 +1,34 @@ +import { useFetch } from "../hooks/useFetch"; +import { httpClient } from "../services/httpClient"; + +interface HealthStatus { + status: string; + service: string; +} + +export default function StatusBar() { + const { data, loading, error } = useFetch(() => + httpClient.get("/health") + ); + + return ( +
+ + + {loading + ? "Connecting..." + : error + ? `Error: ${error}` + : `${data?.service} - ${data?.status}`} + +
+ ); +} diff --git a/frontend/src/hooks/useFetch.ts b/frontend/src/hooks/useFetch.ts new file mode 100644 index 0000000..5872e18 --- /dev/null +++ b/frontend/src/hooks/useFetch.ts @@ -0,0 +1,27 @@ +import { useState, useEffect } from "react"; + +export function useFetch(fetcher: () => Promise) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + setLoading(true); + fetcher() + .then((result) => { + if (!cancelled) setData(result); + }) + .catch((err) => { + if (!cancelled) setError(err.message); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; + }, []); + + return { data, loading, error }; +} diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx new file mode 100644 index 0000000..a9247ae --- /dev/null +++ b/frontend/src/index.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import App from "./App"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + +); diff --git a/frontend/src/pages/Docs.tsx b/frontend/src/pages/Docs.tsx new file mode 100644 index 0000000..7983eb8 --- /dev/null +++ b/frontend/src/pages/Docs.tsx @@ -0,0 +1,160 @@ +import { Link } from "react-router-dom"; + +const BASE_URL_EXAMPLE = "https://api.superdream.example.com"; + +export default function Docs() { + return ( +
+
+

API 文档

+

+ SuperDream 兼容 OpenAI API 格式,你可以使用任何支持 OpenAI 的 SDK 直接接入。 +

+ + {/* Quick start */} +
+
    +
  1. + + 注册账号 + + ,在控制台获取 API Key +
  2. +
  3. 在钱包页面充值余额
  4. +
  5. 将下方示例中的 API Key 替换为你自己的 Key 即可调用
  6. +
+
+ + {/* Base URL */} +
+ {`${BASE_URL_EXAMPLE}/v1`} +

+ 将你的 OpenAI SDK 的 base_url 指向此地址即可。 +

+
+ + {/* Auth */} +
+

+ 在请求 Header 中携带 API Key: +

+ Authorization: Bearer sk-sd-your-api-key +
+ + {/* Python example */} +
+ {`from openai import OpenAI + +client = OpenAI( + api_key="sk-sd-your-api-key", + base_url="${BASE_URL_EXAMPLE}/v1", +) + +response = client.chat.completions.create( + model="gpt-4o", + messages=[ + {"role": "user", "content": "Hello!"} + ], +) + +print(response.choices[0].message.content)`} +
+ + {/* curl example */} +
+ {`curl ${BASE_URL_EXAMPLE}/v1/chat/completions \\ + -H "Content-Type: application/json" \\ + -H "Authorization: Bearer sk-sd-your-api-key" \\ + -d '{ + "model": "gpt-4o", + "messages": [ + {"role": "user", "content": "Hello!"} + ] + }'`} +
+ + {/* Node.js example */} +
+ {`import OpenAI from "openai"; + +const client = new OpenAI({ + apiKey: "sk-sd-your-api-key", + baseURL: "${BASE_URL_EXAMPLE}/v1", +}); + +const response = await client.chat.completions.create({ + model: "gpt-4o", + messages: [ + { role: "user", content: "Hello!" } + ], +}); + +console.log(response.choices[0].message.content);`} +
+ + {/* Supported models */} +
+

+ 在请求的 model 字段中指定模型名称即可切换模型。 +

+

+ 查看完整的模型列表和定价: + + 定价页面 + +

+
+ + {/* FAQ */} +
+ + SuperDream 聚合了多家模型供应商,你可以通过一个统一接口调用不同厂商的模型,无需分别申请账号和管理密钥。 + + + 按实际消耗的 token 数量计费,输入和输出分别计价。费用在每次调用完成后实时从余额扣除。 + + + Key 创建后仅显示一次,丢失后无法找回。你可以在控制台删除旧 Key 并创建新的。 + + + 支持。在请求中设置 stream: true 即可使用 SSE 流式输出。 + +
+
+
+ ); +} + +function Section({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+

{title}

+ {children} +
+ ); +} + +function Code({ children }: { children: string }) { + return ( + + {children} + + ); +} + +function CodeBlock({ children }: { children: string }) { + return ( +
+      {children}
+    
+ ); +} + +function Faq({ q, children }: { q: string; children: React.ReactNode }) { + return ( +
+

{q}

+

{children}

+
+ ); +} diff --git a/frontend/src/pages/ForgotPassword.tsx b/frontend/src/pages/ForgotPassword.tsx new file mode 100644 index 0000000..e9912fc --- /dev/null +++ b/frontend/src/pages/ForgotPassword.tsx @@ -0,0 +1,86 @@ +import { useState, FormEvent } from "react"; +import { Link } from "react-router-dom"; +import { authService } from "../services/authService"; + +export default function ForgotPassword() { + const [email, setEmail] = useState(""); + const [sent, setSent] = useState(false); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setError(""); + setLoading(true); + try { + await authService.forgotPassword(email); + setSent(true); + } catch (err: any) { + setError(err.message || "请求失败"); + } finally { + setLoading(false); + } + }; + + if (sent) { + return ( +
+
+

邮件已发送

+

+ 如果该邮箱已注册,你将收到一封密码重置邮件。请检查收件箱。 +

+ + 返回登录 + +
+
+ ); + } + + return ( +
+
+

忘记密码

+

+ 输入注册邮箱,我们将发送密码重置链接 +

+ + {error && ( +
+ {error} +
+ )} + +
+ + setEmail(e.target.value)} + className="w-full px-4 py-2 rounded-lg bg-superdream-bg border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white focus:border-superdream-accent focus:outline-none" + placeholder="you@example.com" + /> +
+ + + +
+ + 返回登录 + +
+
+
+ ); +} diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx new file mode 100644 index 0000000..c1c03af --- /dev/null +++ b/frontend/src/pages/Home.tsx @@ -0,0 +1,108 @@ +import { Link } from "react-router-dom"; + +const features = [ + { + title: "统一接口", + desc: "兼容 OpenAI API 格式,一个接口调用所有主流大模型,无需逐家对接。", + }, + { + title: "按量计费", + desc: "按实际 token 用量付费,无月费无绑定,用多少付多少。", + }, + { + title: "多模型聚合", + desc: "GPT-4o、Claude、Gemini、DeepSeek 等主流模型一站接入,自由切换。", + }, + { + title: "实时监控", + desc: "每笔调用实时记录,用量、费用一目了然,支持多维度统计分析。", + }, +]; + +const models = [ + { name: "GPT-4o", provider: "OpenAI" }, + { name: "GPT-4o-mini", provider: "OpenAI" }, + { name: "Claude Sonnet 4", provider: "Anthropic" }, + { name: "Claude Haiku 3.5", provider: "Anthropic" }, + { name: "Gemini 2.5 Pro", provider: "Google" }, + { name: "DeepSeek V3", provider: "DeepSeek" }, +]; + +export default function Home() { + return ( +
+ {/* Hero */} +
+

+ 大模型 API,触手可及 +

+

+ SuperDream 聚合主流大模型 API,提供统一的 OpenAI 兼容接口。按量计费,即充即用,几行代码即可接入。 +

+
+ + 免费注册 + + + 查看价格 + +
+
+ + {/* Features */} +
+

为什么选择 SuperDream

+
+ {features.map((f) => ( +
+

{f.title}

+

{f.desc}

+
+ ))} +
+
+ + {/* Supported models */} +
+

支持的模型

+
+ {models.map((m) => ( +
+ {m.name} + {m.provider} +
+ ))} +
+

+ 更多模型持续接入中...  + + 查看完整定价 + +

+
+ + {/* CTA */} +
+

立即开始

+

注册账号,获取 API Key,几分钟完成接入

+ + 免费注册 + +
+ + {/* Footer */} +
+ SuperDream © {new Date().getFullYear()} +
+
+ ); +} diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx new file mode 100644 index 0000000..9ec2d64 --- /dev/null +++ b/frontend/src/pages/Login.tsx @@ -0,0 +1,86 @@ +import { useState, FormEvent } from "react"; +import { useNavigate, Link } from "react-router-dom"; +import { authService } from "../services/authService"; +import { authStore } from "../stores/authStore"; + +export default function Login() { + const navigate = useNavigate(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setError(""); + setLoading(true); + try { + const res = await authService.login(email, password); + authStore.setTokens(res.access_token, res.refresh_token); + await authStore.fetchUser(); + navigate("/dashboard"); + } catch (err: any) { + setError(err.message || "登录失败"); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

登录

+ + {error && ( +
+ {error} +
+ )} + +
+ + setEmail(e.target.value)} + className="w-full px-4 py-2 rounded-lg bg-superdream-bg border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white focus:border-superdream-accent focus:outline-none" + placeholder="you@example.com" + /> +
+ +
+ + setPassword(e.target.value)} + className="w-full px-4 py-2 rounded-lg bg-superdream-bg border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white focus:border-superdream-accent focus:outline-none" + placeholder="••••••••" + /> +
+ + + +
+ + 注册账号 + + + 忘记密码 + +
+
+
+ ); +} diff --git a/frontend/src/pages/Pricing.tsx b/frontend/src/pages/Pricing.tsx new file mode 100644 index 0000000..4550f1f --- /dev/null +++ b/frontend/src/pages/Pricing.tsx @@ -0,0 +1,98 @@ +import { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; +import { httpClient } from "../services/httpClient"; + +interface ModelPrice { + id: number; + model_name: string; + provider: string; + input_price_per_1k: string; + output_price_per_1k: string; + status: string; +} + +export default function Pricing() { + const [models, setModels] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + httpClient + .get("/models") + .then(setModels) + .catch(() => {}) + .finally(() => setLoading(false)); + }, []); + + // Group by provider + const grouped: Record = {}; + models.forEach((m) => { + if (!grouped[m.provider]) grouped[m.provider] = []; + grouped[m.provider].push(m); + }); + + return ( +
+
+

模型定价

+

+ 按实际 token 用量计费,价格透明,无隐藏费用 +

+ + {loading ? ( +

加载中...

+ ) : models.length === 0 ? ( +
+

暂无定价数据

+

模型定价由管理员在后台配置后展示

+
+ ) : ( + Object.entries(grouped).map(([provider, list]) => ( +
+

{provider}

+
+ + + + + + + + + + {list.map((m) => ( + + + + + + ))} + +
模型输入价格 / 1K tokens输出价格 / 1K tokens
{m.model_name}¥{m.input_price_per_1k}¥{m.output_price_per_1k}
+
+
+ )) + )} + + {/* Billing notes */} +
+

计费说明

+
    +
  • 按实际使用的 token 数量计费,输入和输出分别计价
  • +
  • 费用在每次 API 调用完成后实时从余额中扣除
  • +
  • 支持通过兑换码充值余额,后续将开放更多充值方式
  • +
  • 所有调用记录和费用明细可在控制台实时查看
  • +
+
+ +
+ + 立即注册,开始使用 + +
+
+
+ ); +} diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx new file mode 100644 index 0000000..0e402f0 --- /dev/null +++ b/frontend/src/pages/Register.tsx @@ -0,0 +1,111 @@ +import { useState, FormEvent } from "react"; +import { useNavigate, Link } from "react-router-dom"; +import { authService } from "../services/authService"; +import { authStore } from "../stores/authStore"; + +export default function Register() { + const navigate = useNavigate(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirm, setConfirm] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setError(""); + + if (password !== confirm) { + setError("两次密码输入不一致"); + return; + } + if (password.length < 6) { + setError("密码至少 6 位"); + return; + } + if (!/[a-zA-Z]/.test(password) || !/[0-9]/.test(password)) { + setError("密码需同时包含字母和数字"); + return; + } + try { + await authService.register(email, password); + // Auto-login after registration + const res = await authService.login(email, password); + authStore.setTokens(res.access_token, res.refresh_token); + await authStore.fetchUser(); + navigate("/dashboard"); + } catch (err: any) { + setError(err.message || "注册失败"); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

注册

+ + {error && ( +
+ {error} +
+ )} + +
+ + setEmail(e.target.value)} + className="w-full px-4 py-2 rounded-lg bg-superdream-bg border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white focus:border-superdream-accent focus:outline-none" + placeholder="you@example.com" + /> +
+ +
+ + setPassword(e.target.value)} + className="w-full px-4 py-2 rounded-lg bg-superdream-bg border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white focus:border-superdream-accent focus:outline-none" + placeholder="至少 6 位" + /> +
+ +
+ + setConfirm(e.target.value)} + className="w-full px-4 py-2 rounded-lg bg-superdream-bg border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white focus:border-superdream-accent focus:outline-none" + placeholder="再次输入密码" + /> +
+ + + +
+ 已有账号? + + 去登录 + +
+
+
+ ); +} diff --git a/frontend/src/pages/ResetPassword.tsx b/frontend/src/pages/ResetPassword.tsx new file mode 100644 index 0000000..29f4d9f --- /dev/null +++ b/frontend/src/pages/ResetPassword.tsx @@ -0,0 +1,111 @@ +import { useState, FormEvent } from "react"; +import { Link, useSearchParams } from "react-router-dom"; +import { authService } from "../services/authService"; + +export default function ResetPassword() { + const [searchParams] = useSearchParams(); + const token = searchParams.get("token") || ""; + const [password, setPassword] = useState(""); + const [confirm, setConfirm] = useState(""); + const [done, setDone] = useState(false); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setError(""); + + if (password !== confirm) { + setError("两次密码输入不一致"); + return; + } + if (password.length < 6) { + setError("密码至少 6 位"); + return; + } + if (!/[a-zA-Z]/.test(password) || !/[0-9]/.test(password)) { + setError("密码需同时包含字母和数字"); + return; + } + if (!token) { + setError("缺少重置 token,请通过邮件链接访问"); + return; + } + + setLoading(true); + try { + await authService.resetPassword(token, password); + setDone(true); + } catch (err: any) { + setError(err.message || "重置失败"); + } finally { + setLoading(false); + } + }; + + if (done) { + return ( +
+
+

密码已重置

+

请使用新密码登录。

+ + 去登录 + +
+
+ ); + } + + return ( +
+
+

重置密码

+ + {error && ( +
+ {error} +
+ )} + +
+ + setPassword(e.target.value)} + className="w-full px-4 py-2 rounded-lg bg-superdream-bg border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white focus:border-superdream-accent focus:outline-none" + placeholder="至少 6 位" + /> +
+ +
+ + setConfirm(e.target.value)} + className="w-full px-4 py-2 rounded-lg bg-superdream-bg border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white focus:border-superdream-accent focus:outline-none" + placeholder="再次输入新密码" + /> +
+ + +
+
+ ); +} diff --git a/frontend/src/pages/dashboard/Keys.tsx b/frontend/src/pages/dashboard/Keys.tsx new file mode 100644 index 0000000..6931355 --- /dev/null +++ b/frontend/src/pages/dashboard/Keys.tsx @@ -0,0 +1,191 @@ +import { useState, useEffect } from "react"; +import { keyService, ApiKeyInfo } from "../../services/keyService"; + +export default function Keys() { + const [keys, setKeys] = useState([]); + const [name, setName] = useState(""); + const [newKey, setNewKey] = useState(""); + const [copied, setCopied] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [deleteTarget, setDeleteTarget] = useState(null); + const [deleting, setDeleting] = useState(false); + + const fetchKeys = async () => { + try { + const data = await keyService.list(); + setKeys(data); + } catch (err: any) { + setError(err.message); + } + }; + + useEffect(() => { + fetchKeys(); + }, []); + + const handleCreate = async () => { + setError(""); + setNewKey(""); + setLoading(true); + try { + const res = await keyService.create(name); + setNewKey(res.key); + setName(""); + await fetchKeys(); + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + const confirmDelete = async () => { + if (!deleteTarget) return; + setDeleting(true); + try { + await keyService.remove(deleteTarget); + setDeleteTarget(null); + await fetchKeys(); + } catch (err: any) { + setError(err.message); + setDeleteTarget(null); + } finally { + setDeleting(false); + } + }; + + const handleCopy = async (text: string) => { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+

API Key 管理

+ + {error && ( +
+ {error} +
+ )} + + {/* Create Key */} +
+
+
+ + setName(e.target.value)} + placeholder="例如:测试环境" + className="w-full px-3 py-2 rounded-lg bg-superdream-bg border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white text-sm focus:border-superdream-accent focus:outline-none" + /> +
+ +
+
+ + {/* Newly created key */} + {newKey && ( +
+

Key 创建成功!请立即复制保存,此 Key 仅显示一次。

+
+ + {newKey} + + +
+
+ )} + + {/* Key list */} +
+ + + + + + + + + + + {keys.length === 0 ? ( + + + + ) : ( + keys.map((k) => ( + + + + + + + )) + )} + +
名称Key创建时间操作
+ 暂无 Key,点击上方按钮创建 +
{k.name || "-"} + sk-sd-{k.key_prefix}...{k.key_suffix} + + {new Date(k.created_at).toLocaleString()} + + +
+
+ + {/* Delete confirm modal */} + {deleteTarget && ( +
+
!deleting && setDeleteTarget(null)} + /> +
+

确认删除

+

+ 删除后该 Key 将立即失效且无法恢复,确定要删除吗? +

+
+ + +
+
+
+ )} +
+ ); +} diff --git a/frontend/src/pages/dashboard/Layout.tsx b/frontend/src/pages/dashboard/Layout.tsx new file mode 100644 index 0000000..416c98b --- /dev/null +++ b/frontend/src/pages/dashboard/Layout.tsx @@ -0,0 +1,37 @@ +import { Outlet, NavLink } from "react-router-dom"; + +const navItems = [ + { to: "/dashboard", label: "概览", end: true }, + { to: "/dashboard/keys", label: "API Key" }, + { to: "/dashboard/wallet", label: "钱包" }, + { to: "/dashboard/usage", label: "用量" }, + { to: "/dashboard/logs", label: "调用日志" }, +]; + +export default function DashboardLayout() { + return ( +
+ +
+ +
+
+ ); +} diff --git a/frontend/src/pages/dashboard/Logs.tsx b/frontend/src/pages/dashboard/Logs.tsx new file mode 100644 index 0000000..6d12aae --- /dev/null +++ b/frontend/src/pages/dashboard/Logs.tsx @@ -0,0 +1,136 @@ +import { useState, useEffect } from "react"; +import { usageService, UsageLogItem } from "../../services/usageService"; + +export default function Logs() { + const [logs, setLogs] = useState([]); + const [page, setPage] = useState(1); + const [model, setModel] = useState(""); + const [loading, setLoading] = useState(false); + const pageSize = 20; + + const fetchLogs = async () => { + setLoading(true); + try { + const data = await usageService.logs({ + page, + size: pageSize, + model: model || undefined, + }); + setLogs(data); + } catch { + // silently fail + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchLogs(); + }, [page]); + + const handleFilter = () => { + setPage(1); + fetchLogs(); + }; + + return ( +
+

调用日志

+ + {/* Filters */} +
+
+
+ + setModel(e.target.value)} + placeholder="如 gpt-4o" + className="px-3 py-2 rounded-lg bg-superdream-bg border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white text-sm focus:border-superdream-accent focus:outline-none w-48" + /> +
+ +
+
+ + {/* Table */} +
+ + + + + + + + + + + + + + {loading ? ( + + + + ) : logs.length === 0 ? ( + + + + ) : ( + logs.map((log) => ( + + + + + + + + + + )) + )} + +
时间模型PromptCompletionTotal费用状态
+ 加载中... +
+ 暂无调用记录 +
+ {new Date(log.request_time).toLocaleString()} + {log.model}{log.prompt_tokens.toLocaleString()}{log.completion_tokens.toLocaleString()}{log.total_tokens.toLocaleString()}¥{log.cost} + + {log.status} + +
+
+ + {/* Pagination */} +
+ + 第 {page} 页 + +
+
+ ); +} diff --git a/frontend/src/pages/dashboard/Overview.tsx b/frontend/src/pages/dashboard/Overview.tsx new file mode 100644 index 0000000..b400391 --- /dev/null +++ b/frontend/src/pages/dashboard/Overview.tsx @@ -0,0 +1,42 @@ +import { useState, useEffect } from "react"; +import { walletService } from "../../services/walletService"; +import { keyService } from "../../services/keyService"; +import { usageService, UsageSummary } from "../../services/usageService"; + +export default function Overview() { + const [balance, setBalance] = useState("0"); + const [keyCount, setKeyCount] = useState(0); + const [summary, setSummary] = useState(null); + + useEffect(() => { + walletService.getBalance().then((b) => setBalance(b.balance)).catch(() => {}); + keyService.list().then((keys) => setKeyCount(keys.length)).catch(() => {}); + usageService.summary().then(setSummary).catch(() => {}); + }, []); + + return ( +
+

控制台

+
+
+

余额

+

¥{balance}

+
+
+

活跃 Key

+

{keyCount}

+
+
+

今日调用

+

+ {summary ? `${summary.today_tokens.toLocaleString()} tokens` : "0"} +

+
+
+

今日消耗

+

¥{summary?.today_cost ?? "0"}

+
+
+
+ ); +} diff --git a/frontend/src/pages/dashboard/Usage.tsx b/frontend/src/pages/dashboard/Usage.tsx new file mode 100644 index 0000000..1a45be3 --- /dev/null +++ b/frontend/src/pages/dashboard/Usage.tsx @@ -0,0 +1,107 @@ +import { useState, useEffect } from "react"; +import { + ResponsiveContainer, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, + BarChart, Bar, Cell, +} from "recharts"; +import { + usageService, UsageSummary, DailyUsage, ModelUsage, +} from "../../services/usageService"; + +const COLORS = ["#8B5CF6", "#a78bfa", "#6366f1", "#818cf8", "#c084fc", "#7c3aed"]; + +export default function Usage() { + const [summary, setSummary] = useState(null); + const [daily, setDaily] = useState([]); + const [byModel, setByModel] = useState([]); + + useEffect(() => { + usageService.summary().then(setSummary).catch(() => {}); + usageService.daily().then(setDaily).catch(() => {}); + usageService.byModel().then(setByModel).catch(() => {}); + }, []); + + return ( +
+

用量统计

+ + {/* Summary cards */} +
+ + + + +
+ + {/* Daily chart */} +
+

每日用量(最近 30 天)

+ {daily.length === 0 ? ( +

暂无数据

+ ) : ( + + + + v.slice(5)} + /> + + + + + + )} +
+ + {/* By model chart */} +
+

按模型统计

+ {byModel.length === 0 ? ( +

暂无数据

+ ) : ( + + + + + + + + {byModel.map((_, i) => ( + + ))} + + + + )} +
+
+ ); +} + +function Card({ label, value }: { label: string; value: string }) { + return ( +
+

{label}

+

{value}

+
+ ); +} diff --git a/frontend/src/pages/dashboard/Wallet.tsx b/frontend/src/pages/dashboard/Wallet.tsx new file mode 100644 index 0000000..70aceb2 --- /dev/null +++ b/frontend/src/pages/dashboard/Wallet.tsx @@ -0,0 +1,139 @@ +import { useState, useEffect } from "react"; +import { walletService, TransactionInfo } from "../../services/walletService"; + +const TYPE_LABELS: Record = { + topup: "充值", + consume: "消费", + refund: "退款", +}; + +export default function Wallet() { + const [balance, setBalance] = useState("0"); + const [code, setCode] = useState(""); + const [transactions, setTransactions] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + + const fetchData = async () => { + try { + const [b, txns] = await Promise.all([ + walletService.getBalance(), + walletService.transactions(), + ]); + setBalance(b.balance); + setTransactions(txns); + } catch (err: any) { + setError(err.message); + } + }; + + useEffect(() => { + fetchData(); + }, []); + + const handleRedeem = async () => { + setError(""); + setSuccess(""); + if (!code.trim()) return; + setLoading(true); + try { + const txn = await walletService.redeem(code.trim()); + setSuccess(`充值成功!+¥${txn.amount}`); + setCode(""); + await fetchData(); + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + return ( +
+

钱包

+ + {/* Balance */} +
+

当前余额

+

¥{balance}

+
+ + {/* Redeem */} +
+ + + {error && ( +
+ {error} +
+ )} + {success && ( +
+ {success} +
+ )} + +
+ setCode(e.target.value)} + placeholder="输入兑换码" + className="flex-1 px-3 py-2 rounded-lg bg-superdream-bg border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white text-sm focus:border-superdream-accent focus:outline-none" + /> + +
+
+ + {/* Transactions */} +
+

+ 交易记录 +

+ + + + + + + + + + + + {transactions.length === 0 ? ( + + + + ) : ( + transactions.map((txn) => ( + + + + + + + + )) + )} + +
类型金额余额备注时间
+ 暂无交易记录 +
+ {TYPE_LABELS[txn.type] || txn.type} + + {txn.type === "consume" ? "-" : "+"}¥{txn.amount} + ¥{txn.balance_after}{txn.reference_id || "-"} + {new Date(txn.created_at).toLocaleString()} +
+
+
+ ); +} diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts new file mode 100644 index 0000000..630f15e --- /dev/null +++ b/frontend/src/services/authService.ts @@ -0,0 +1,34 @@ +import { httpClient } from "./httpClient"; + +export interface TokenResponse { + access_token: string; + refresh_token?: string; + token_type: string; +} + +export interface UserInfo { + id: string; + email: string; + balance: string; + status: string; + created_at: string; +} + +export const authService = { + register: (email: string, password: string) => + httpClient.post("/auth/register", { email, password }), + + login: (email: string, password: string) => + httpClient.post("/auth/login", { email, password }), + + refresh: (refresh_token: string) => + httpClient.post("/auth/refresh", { refresh_token }), + + me: () => httpClient.get("/auth/me"), + + forgotPassword: (email: string) => + httpClient.post<{ message: string }>("/auth/forgot-password", { email }), + + resetPassword: (token: string, new_password: string) => + httpClient.post<{ message: string }>("/auth/reset-password", { token, new_password }), +}; diff --git a/frontend/src/services/exampleService.ts b/frontend/src/services/exampleService.ts new file mode 100644 index 0000000..8fe3119 --- /dev/null +++ b/frontend/src/services/exampleService.ts @@ -0,0 +1,9 @@ +import { httpClient } from "./httpClient"; +import { Example } from "../types"; + +export const exampleService = { + list: () => httpClient.get("/examples"), + get: (id: string) => httpClient.get(`/examples/${id}`), + create: (data: { name: string; description?: string }) => + httpClient.post("/examples", data), +}; diff --git a/frontend/src/services/httpClient.ts b/frontend/src/services/httpClient.ts new file mode 100644 index 0000000..5d228fc --- /dev/null +++ b/frontend/src/services/httpClient.ts @@ -0,0 +1,36 @@ +const BASE_URL = "/api/v1"; + +function getAuthHeaders(): Record { + const headers: Record = { "Content-Type": "application/json" }; + const token = localStorage.getItem("sd_access_token"); + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } + return headers; +} + +async function request( + endpoint: string, + options?: RequestInit +): Promise { + const res = await fetch(`${BASE_URL}${endpoint}`, { + headers: getAuthHeaders(), + ...options, + }); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.detail || `HTTP ${res.status}: ${res.statusText}`); + } + return res.json(); +} + +export const httpClient = { + get: (endpoint: string) => request(endpoint), + post: (endpoint: string, body: unknown) => + request(endpoint, { + method: "POST", + body: JSON.stringify(body), + }), + delete: (endpoint: string) => + request(endpoint, { method: "DELETE" }), +}; diff --git a/frontend/src/services/keyService.ts b/frontend/src/services/keyService.ts new file mode 100644 index 0000000..ed04295 --- /dev/null +++ b/frontend/src/services/keyService.ts @@ -0,0 +1,25 @@ +import { httpClient } from "./httpClient"; + +export interface ApiKeyInfo { + id: string; + name: string; + key_prefix: string; + key_suffix: string; + status: string; + created_at: string; +} + +export interface ApiKeyCreated { + id: string; + name: string; + key: string; + key_prefix: string; + key_suffix: string; + created_at: string; +} + +export const keyService = { + list: () => httpClient.get("/keys"), + create: (name: string) => httpClient.post("/keys", { name }), + remove: (keyId: string) => httpClient.delete<{ message: string }>(`/keys/${keyId}`), +}; diff --git a/frontend/src/services/usageService.ts b/frontend/src/services/usageService.ts new file mode 100644 index 0000000..2a0b5bf --- /dev/null +++ b/frontend/src/services/usageService.ts @@ -0,0 +1,80 @@ +import { httpClient } from "./httpClient"; + +export interface UsageSummary { + today_tokens: number; + today_cost: string; + month_tokens: number; + month_cost: string; + total_requests: number; +} + +export interface DailyUsage { + date: string; + total_tokens: number; + cost: string; + requests: number; +} + +export interface ModelUsage { + model: string; + total_tokens: number; + cost: string; + requests: number; +} + +export interface KeyUsage { + key_id: string; + key_name: string; + key_prefix: string; + key_suffix: string; + total_tokens: number; + cost: string; + requests: number; +} + +export interface UsageLogItem { + id: number; + key_id: string; + model: string; + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + cost: string; + request_time: string; + status: string; +} + +export const usageService = { + summary: () => httpClient.get("/usage/summary"), + + daily: (start?: string, end?: string) => { + const params = new URLSearchParams(); + if (start) params.set("start", start); + if (end) params.set("end", end); + const qs = params.toString(); + return httpClient.get(`/usage/daily${qs ? `?${qs}` : ""}`); + }, + + byModel: () => httpClient.get("/usage/by-model"), + + byKey: () => httpClient.get("/usage/by-key"), + + logs: (params: { + page?: number; + size?: number; + model?: string; + key_id?: string; + start?: string; + end?: string; + } = {}) => { + const sp = new URLSearchParams(); + if (params.page) sp.set("page", String(params.page)); + if (params.size) sp.set("size", String(params.size)); + if (params.model) sp.set("model", params.model); + if (params.key_id) sp.set("key_id", params.key_id); + if (params.start) sp.set("start", params.start); + if (params.end) sp.set("end", params.end); + const qs = sp.toString(); + return httpClient.get(`/usage/logs${qs ? `?${qs}` : ""}`); + }, +}; diff --git a/frontend/src/services/walletService.ts b/frontend/src/services/walletService.ts new file mode 100644 index 0000000..a71755a --- /dev/null +++ b/frontend/src/services/walletService.ts @@ -0,0 +1,21 @@ +import { httpClient } from "./httpClient"; + +export interface BalanceInfo { + balance: string; +} + +export interface TransactionInfo { + id: string; + type: string; + amount: string; + balance_after: string; + reference_id: string; + created_at: string; +} + +export const walletService = { + getBalance: () => httpClient.get("/wallet/balance"), + redeem: (code: string) => httpClient.post("/wallet/redeem", { code }), + transactions: (page = 1, size = 20) => + httpClient.get(`/wallet/transactions?page=${page}&size=${size}`), +}; diff --git a/frontend/src/stores/authStore.ts b/frontend/src/stores/authStore.ts new file mode 100644 index 0000000..e1a55ca --- /dev/null +++ b/frontend/src/stores/authStore.ts @@ -0,0 +1,51 @@ +import { authService, UserInfo } from "../services/authService"; + +const TOKEN_KEY = "sd_access_token"; +const REFRESH_KEY = "sd_refresh_token"; + +let currentUser: UserInfo | null = null; +let listeners: Array<() => void> = []; + +function notify() { + listeners.forEach((fn) => fn()); +} + +export const authStore = { + getToken: (): string | null => localStorage.getItem(TOKEN_KEY), + + setTokens: (access: string, refresh?: string) => { + localStorage.setItem(TOKEN_KEY, access); + if (refresh) localStorage.setItem(REFRESH_KEY, refresh); + notify(); + }, + + clearTokens: () => { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(REFRESH_KEY); + currentUser = null; + notify(); + }, + + isLoggedIn: (): boolean => !!localStorage.getItem(TOKEN_KEY), + + getUser: (): UserInfo | null => currentUser, + + fetchUser: async (): Promise => { + if (!authStore.isLoggedIn()) return null; + try { + currentUser = await authService.me(); + notify(); + return currentUser; + } catch { + authStore.clearTokens(); + return null; + } + }, + + subscribe: (fn: () => void) => { + listeners.push(fn); + return () => { + listeners = listeners.filter((l) => l !== fn); + }; + }, +}; diff --git a/frontend/src/stores/themeStore.ts b/frontend/src/stores/themeStore.ts new file mode 100644 index 0000000..d938a11 --- /dev/null +++ b/frontend/src/stores/themeStore.ts @@ -0,0 +1,39 @@ +const THEME_KEY = "sd_theme"; + +type Theme = "light" | "dark"; + +let listeners: Array<() => void> = []; + +function notify() { + listeners.forEach((fn) => fn()); +} + +function applyTheme(theme: Theme) { + if (theme === "dark") { + document.documentElement.classList.add("dark"); + } else { + document.documentElement.classList.remove("dark"); + } +} + +export const themeStore = { + get: (): Theme => { + return (localStorage.getItem(THEME_KEY) as Theme) || "dark"; + }, + + toggle: () => { + const next: Theme = themeStore.get() === "dark" ? "light" : "dark"; + localStorage.setItem(THEME_KEY, next); + applyTheme(next); + notify(); + }, + + isDark: (): boolean => themeStore.get() === "dark", + + subscribe: (fn: () => void) => { + listeners.push(fn); + return () => { + listeners = listeners.filter((l) => l !== fn); + }; + }, +}; diff --git a/frontend/src/types.ts b/frontend/src/types.ts new file mode 100644 index 0000000..9da5c6c --- /dev/null +++ b/frontend/src/types.ts @@ -0,0 +1,11 @@ +export interface Example { + id: string; + name: string; + description: string; + created_at: string; +} + +export interface ApiResponse { + data: T; + message?: string; +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..a636857 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo new file mode 100644 index 0000000..5c90324 --- /dev/null +++ b/frontend/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/app.tsx","./src/index.tsx","./src/types.ts","./src/components/authguard.tsx","./src/components/header.tsx","./src/components/statusbar.tsx","./src/hooks/usefetch.ts","./src/pages/docs.tsx","./src/pages/forgotpassword.tsx","./src/pages/home.tsx","./src/pages/login.tsx","./src/pages/pricing.tsx","./src/pages/register.tsx","./src/pages/resetpassword.tsx","./src/pages/dashboard/keys.tsx","./src/pages/dashboard/layout.tsx","./src/pages/dashboard/logs.tsx","./src/pages/dashboard/overview.tsx","./src/pages/dashboard/usage.tsx","./src/pages/dashboard/wallet.tsx","./src/services/authservice.ts","./src/services/exampleservice.ts","./src/services/httpclient.ts","./src/services/keyservice.ts","./src/services/usageservice.ts","./src/services/walletservice.ts","./src/stores/authstore.ts","./src/stores/themestore.ts"],"version":"5.8.3"} \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..3f56b9e --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import path from "path"; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "src"), + }, + }, + server: { + port: 3000, + host: "0.0.0.0", + proxy: { + "/api": "http://127.0.0.1:18000", + "/data/files": "http://127.0.0.1:18000", + }, + }, + build: { + outDir: "dist", + }, +}); diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d178126 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +fastapi>=0.104.1 +uvicorn[standard]>=0.24.0 +python-dotenv>=1.0.0 +pydantic>=2.5.0 +pydantic-settings>=2.1.0 +aiofiles>=23.2.0 +httpx>=0.25.0 +sqlalchemy[asyncio]>=2.0.23 +aiomysql>=0.2.0 +PyJWT>=2.8.0 +bcrypt>=4.1.0 \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000..30ec019 --- /dev/null +++ b/run.py @@ -0,0 +1,10 @@ +import uvicorn +from app.config.settings import settings + +if __name__ == "__main__": + uvicorn.run( + "app.main:app", + host=settings.host, + port=settings.port, + reload=True, + )