JeCabrera commited on
Commit
d4bbbd9
·
1 Parent(s): 32edabf

fix: remove binary logo asset to avoid PR binary-file error

Browse files
Dockerfile CHANGED
@@ -1,34 +1,23 @@
1
- # Dockerfile
2
-
3
- # Base image to use
4
- FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8
5
-
6
- # Set the working directory to /app
 
 
 
 
7
  WORKDIR /app
8
 
9
- # Copy the backend application files
10
- COPY ./backend /app/backend
11
-
12
- # Copy the frontend application files
13
- COPY ./frontend /app/frontend
14
-
15
- # Install dependencies for the backend
16
  RUN pip install --no-cache-dir -r /app/backend/requirements.txt
17
 
18
- # Set the environment variables for FastAPI
19
- ENV MODULE_NAME=backend.main:app
20
-
21
- # Expose port 80 for the FastAPI app
22
- EXPOSE 80
23
-
24
- # Install Node.js for frontend
25
- RUN apt-get update && apt-get install -y nodejs npm
26
-
27
- # Build the React frontend
28
- RUN cd frontend && npm install && npm run build
29
 
30
- # Serve the React app
31
- RUN cp -R frontend/build /app/frontend/build
 
32
 
33
- # Start the FastAPI app
34
- CMD uvicorn backend.main:app --host 0.0.0.0 --port 80 --reload
 
1
+ # ---------- Frontend build ----------
2
+ FROM node:20-bookworm-slim AS frontend-builder
3
+ WORKDIR /app/frontend
4
+ COPY frontend/package.json frontend/package-lock.json* ./
5
+ RUN npm install
6
+ COPY frontend .
7
+ RUN npm run build
8
+
9
+ # ---------- Backend runtime ----------
10
+ FROM python:3.11-slim
11
  WORKDIR /app
12
 
13
+ COPY backend/requirements.txt /app/backend/requirements.txt
 
 
 
 
 
 
14
  RUN pip install --no-cache-dir -r /app/backend/requirements.txt
15
 
16
+ COPY backend /app/backend
17
+ COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
 
 
 
 
 
 
 
 
 
18
 
19
+ # Hugging Face Spaces expone PORT (fallback 7860)
20
+ ENV PORT=7860
21
+ EXPOSE 7860
22
 
23
+ CMD sh -c "uvicorn backend.main:app --host 0.0.0.0 --port ${PORT}"
 
MIGRACION_REACT_HF.md ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Plan de migración: lógica Python (Streamlit) → React moderno + backend compatible con Hugging Face Spaces
2
+
3
+ ## 1) Hallazgos de la revisión actual
4
+
5
+ ### Backend Python existente
6
+ - `app.py` contiene prácticamente toda la lógica de producto: manejo de sesiones, títulos de chat, menú inicial, prompts de ejemplo y streaming de respuesta desde Gemini. Está fuertemente acoplado a Streamlit (`st.session_state`, `st.chat_message`, `st.chat_input`, `st.rerun`).
7
+ - `session_state.py` implementa persistencia local por usuario (`data/<namespace>`) y encapsula llamadas a `google-genai` con `send_message_stream`.
8
+ - `system_prompts.py` define el prompt maestro del flujo de 5 preguntas y formato final del email.
9
+ - `firebase_store.py` ya anticipa un backend con persistencia remota (Firestore) para índice/historial de chats.
10
+
11
+ ### Frontend React existente
12
+ - Ya hay estructura Vite + React + TypeScript.
13
+ - `ChatWindow.tsx` consume `POST /api/chat/send`, pero ese endpoint no existe en `backend/main.py`.
14
+ - `ChatWindow.tsx` actualmente reinicia mensajes al cambiar chat y no carga historial desde servidor.
15
+ - `ChatHistory.tsx` y `App.tsx` mantienen datos en memoria local del navegador, sin sincronización real.
16
+
17
+ ### Desalineaciones de infraestructura
18
+ - `backend/main.py` solo monta archivos estáticos y no expone API funcional.
19
+ - `backend/gemini_service.py` usa un endpoint no estándar de Gemini (`https://gemini.googleapis.com/v1/email/generate`) y no coincide con el SDK usado en `session_state.py`.
20
+ - `Dockerfile` espera `frontend/build`, pero Vite genera `dist`.
21
+
22
+ ## 2) Objetivo técnico recomendado
23
+
24
+ Separar la app en capas limpias:
25
+ 1. **Frontend React (UI + estado de vista)**
26
+ 2. **Backend FastAPI (lógica de negocio + Gemini + persistencia)**
27
+ 3. **Proveedor de almacenamiento** (local JSON/SQLite para MVP y Firebase opcional)
28
+
29
+ Esto te deja una base escalable para HF Spaces (Docker SDK) y para crecer a multiusuario real.
30
+
31
+ ## 3) Qué lógica debes mover de Python a React y cuál NO
32
+
33
+ ### Mover a React (presentación y UX)
34
+ - Render de chat (mensajes, indicador "escribiendo", input).
35
+ - Menú inicial y botones de ejemplos.
36
+ - Sidebar de sesiones y navegación entre chats.
37
+ - Estado visual (loading, error, disabled states).
38
+
39
+ ### Mantener en backend (dominio y seguridad)
40
+ - Lógica de `get_enhanced_prompt` (saludo y modo ejemplo).
41
+ - Generación de título de chat por IA.
42
+ - Integración con Gemini y streaming.
43
+ - Persistencia de historiales por usuario/chat.
44
+ - Reglas de negocio del flujo guiado.
45
+
46
+ > Regla práctica: si algo implica API keys, reglas de negocio o consistencia de datos, va en backend.
47
+
48
+ ## 4) Arquitectura de API sugerida (contratos)
49
+
50
+ ### Endpoints mínimos
51
+ - `POST /api/chats` → crea chat y devuelve `{chat_id, title}`
52
+ - `GET /api/chats` → lista chats del usuario
53
+ - `GET /api/chats/{chat_id}/messages` → historial
54
+ - `POST /api/chats/{chat_id}/messages` → agrega mensaje y devuelve respuesta IA
55
+ - `GET /api/prompts/examples` → ejemplos iniciales (opcional, para no hardcodear)
56
+
57
+ ### Streaming recomendado
58
+ Para una UX moderna:
59
+ - Preferir **SSE** (`text/event-stream`) o **chunked responses** para tokens.
60
+ - Frontend consume stream y va pintando texto incrementalmente.
61
+
62
+ ## 5) Plan de implementación por fases
63
+
64
+ ### Fase 0 — Alineación de base
65
+ 1. Consolidar `system_prompts.py` en backend y eliminar duplicados.
66
+ 2. Definir modelos Pydantic (`Chat`, `Message`, `SendMessageRequest`, `SendMessageResponse`).
67
+ 3. Corregir `backend/requirements.txt` para usar `google-genai`.
68
+
69
+ ### Fase 1 — Backend funcional
70
+ 1. Crear `backend/services/chat_service.py` con:
71
+ - `create_chat`
72
+ - `list_chats`
73
+ - `send_message`
74
+ - `load_history`
75
+ 2. Crear `backend/repositories` con interfaz de persistencia:
76
+ - `local_repo.py` (MVP)
77
+ - `firebase_repo.py` (adaptando `firebase_store.py`)
78
+ 3. Implementar rutas en `backend/main.py` para `/api/*`.
79
+
80
+ ### Fase 2 — Frontend robusto
81
+ 1. Crear capa de cliente API en `frontend/src/lib/api.ts`.
82
+ 2. Introducir estado global con **TanStack Query** + store liviano (Zustand o Context).
83
+ 3. Refactor de componentes:
84
+ - `ChatShell` (layout)
85
+ - `ChatSidebar`
86
+ - `MessageList`
87
+ - `Composer`
88
+ 4. Manejar errores con toasts y estados vacíos.
89
+
90
+ ### Fase 3 — Hugging Face Spaces
91
+ 1. Ajustar `Dockerfile` multi-stage:
92
+ - Stage Node: build de Vite (`dist`)
93
+ - Stage Python: FastAPI + copiar `dist` a carpeta estática
94
+ 2. FastAPI debe servir estáticos y fallback SPA (`index.html`).
95
+ 3. Variables HF:
96
+ - `GOOGLE_API_KEY`
97
+ - opcional: `FIREBASE_SERVICE_ACCOUNT_JSON`
98
+
99
+ ### Fase 4 — Escalabilidad
100
+ 1. Añadir auth (al menos token por usuario o Firebase Auth).
101
+ 2. Trazas/observabilidad (logs estructurados + request_id).
102
+ 3. Tests:
103
+ - unit tests de servicio
104
+ - contract tests de API
105
+ - e2e básico de chat
106
+
107
+ ## 6) Decisiones técnicas concretas recomendadas
108
+
109
+ - **Gemini**: usar solo `google-genai` (evitar `requests` custom mientras no sea imprescindible).
110
+ - **Persistencia**: iniciar con SQLite o JSON por simplicidad; dejar interfaz para Firebase.
111
+ - **Estado frontend**:
112
+ - Server state: TanStack Query
113
+ - UI state: Zustand/Context
114
+ - **Tipado**: compartir esquemas con OpenAPI generado por FastAPI + tipos TS (openapi-typescript).
115
+
116
+ ## 7) Riesgos actuales que debes corregir primero
117
+
118
+ 1. Endpoint `/api/chat/send` inexistente (rompe el frontend actual).
119
+ 2. `Dockerfile` incompatible con salida Vite (`dist` vs `build`).
120
+ 3. Doble fuente de verdad (Streamlit app y React app conviviendo sin contratos unificados).
121
+ 4. Persistencia local por archivos (`joblib`) no ideal para multiinstancia.
122
+
123
+ ## 8) Backlog accionable (orden sugerido)
124
+
125
+ 1. Implementar API real en `backend/main.py`.
126
+ 2. Conectar `ChatWindow.tsx` a nueva API de chats/mensajes.
127
+ 3. Reemplazar estado local de chats en `App.tsx` por datos del backend.
128
+ 4. Migrar streaming al frontend con SSE.
129
+ 5. Ajustar Docker para HF y probar build end-to-end.
130
+
131
+ ## 9) Criterios de “migración completada”
132
+
133
+ - La UI React reproduce toda la UX importante de Streamlit (menu inicial, ejemplos, historial, multi-chat).
134
+ - Toda llamada a Gemini pasa por FastAPI (ninguna key en frontend).
135
+ - Persistencia de chats disponible al recargar.
136
+ - Docker build exitoso y despliegue en HF Space operativo.
137
+
138
+ ---
139
+
140
+ Si quieres, en la siguiente iteración puedo convertir este plan en tareas técnicas concretas por archivo (`backend/main.py`, `frontend/src/components/*`, `Dockerfile`) y proponerte un PR incremental fase por fase.
README.md CHANGED
@@ -1,97 +1,51 @@
1
- # Chatbot Email App - React + FastAPI 2
2
 
3
- Una aplicación moderna de chatbot para generar copies de correos con IA usando Gemini API.
4
 
5
- ## 🚀 Características
 
 
 
6
 
7
- - ✉️ Generación de emails narrativos con Gemini AI
8
- - 💬 Chat interactivo y responsive
9
- - 📱 Interfaz moderna con React + TypeScript
10
- - ⚡ Backend rápido con FastAPI
11
- - 🐳 Fácil de desplegar en HF Spaces con Docker
12
-
13
- ## 📋 Requisitos
14
-
15
- - Docker (para HF Spaces)
16
- - Node.js 18+ (para desarrollo local)
17
- - Python 3.8+ (para desarrollo local)
18
-
19
- ## 🛠️ Instalación Local
20
 
21
  ### Backend
22
-
23
  ```bash
24
  cd backend
25
  pip install -r requirements.txt
 
26
  ```
27
 
28
  ### Frontend
29
-
30
  ```bash
31
  cd frontend
32
  npm install
33
- ```
34
-
35
- ## 🏃 Ejecutar Localmente
36
-
37
- ### Terminal 1 - Backend
38
-
39
- ```bash
40
- cd backend
41
- python -m uvicorn main:app --reload
42
- ```
43
-
44
- ### Terminal 2 - Frontend
45
-
46
- ```bash
47
- cd frontend
48
  npm run dev
49
  ```
50
 
51
- Accede a `http://localhost:5173`
52
-
53
- ## 🐳 Desplegar en HF Spaces
54
-
55
- 1. Ve a [huggingface.co/spaces](https://huggingface.co/spaces)
56
- 2. Click en "Create new Space"
57
- 3. Selecciona "Docker" como SDK
58
- 4. Conecta tu repositorio de GitHub
59
- 5. Configura las variables de entorno:
60
- - `GOOGLE_API_KEY`: Tu clave de Gemini API
61
-
62
- ## 📁 Estructura del Proyecto
63
-
64
- ```
65
- chatbot-email-app/
66
- ├── backend/
67
- │ ├── main.py
68
- │ └── requirements.txt
69
- ├── frontend/
70
- │ ├── src/
71
- │ │ ├── components/
72
- │ │ ├── App.tsx
73
- │ │ ├── App.css
74
- │ │ └── main.tsx
75
- │ ├── package.json
76
- │ ├── tsconfig.json
77
- │ ├── vite.config.ts
78
- │ └── index.html
79
- ├── Dockerfile
80
- └── README.md
81
- ```
82
-
83
- ## 🔑 Variables de Entorno
84
 
85
- Crea un archivo `.env` en la raíz del backend:
86
 
87
- ```
88
- GOOGLE_API_KEY=your_gemini_api_key_here
89
- ```
90
-
91
- ## 📄 Licencia
92
-
93
- Apache License 2.0
94
-
95
- ## 👨‍💻 Autor
96
-
97
- **squaalldev** - Jesús Cabrera
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Chatbot Email App - React + FastAPI
2
 
3
+ Aplicación de chat para generar emails narrativos con Gemini.
4
 
5
+ ## Stack
6
+ - Frontend: React + TypeScript + Vite
7
+ - Backend: FastAPI
8
+ - IA: Google Gemini (`google-genai`)
9
 
10
+ ## Desarrollo local
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  ### Backend
 
13
  ```bash
14
  cd backend
15
  pip install -r requirements.txt
16
+ uvicorn main:app --reload
17
  ```
18
 
19
  ### Frontend
 
20
  ```bash
21
  cd frontend
22
  npm install
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  npm run dev
24
  ```
25
 
26
+ Frontend en `http://localhost:3000` y backend en `http://localhost:8000`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
+ ## Variables de entorno
29
 
30
+ ### Backend
31
+ - `GOOGLE_API_KEY` (requerida)
32
+ - `GEMINI_MODEL` (opcional, default: `gemini-2.5-flash`)
33
+ - `APP_DATA_DIR` (opcional, default: `backend/data`)
34
+
35
+ ## Deploy en Hugging Face Spaces (Docker)
36
+
37
+ 1. Crea un Space tipo **Docker**.
38
+ 2. Sube este repositorio.
39
+ 3. En **Settings → Secrets**, agrega:
40
+ - `GOOGLE_API_KEY`
41
+ 4. (Opcional) agrega `GEMINI_MODEL`.
42
+
43
+ > Sí, en HF puedes inyectar el secret directamente y esta app lo toma desde `os.environ["GOOGLE_API_KEY"]` en runtime.
44
+
45
+ ## Endpoints API
46
+ - `GET /api/health`
47
+ - `GET /api/chats`
48
+ - `POST /api/chats`
49
+ - `GET /api/chats/{chat_id}/messages`
50
+ - `POST /api/chats/{chat_id}/messages`
51
+ - Compatibilidad legacy: `POST /api/chat/send`
backend/__init__.py ADDED
File without changes
backend/gemini_service.py DELETED
@@ -1,31 +0,0 @@
1
- import requests
2
- import json
3
-
4
- class GeminiService:
5
- def __init__(self, api_key):
6
- self.api_key = api_key
7
- self.endpoint = 'https://gemini.googleapis.com/v1/email/generate'
8
-
9
- def generate_email(self, system_prompt, chat_history):
10
- # Prepare the payload for the API request
11
- payload = {
12
- 'system_prompt': system_prompt,
13
- 'chat_history': chat_history
14
- }
15
-
16
- headers = {
17
- 'Authorization': f'Bearer {self.api_key}',
18
- 'Content-Type': 'application/json'
19
- }
20
-
21
- try:
22
- response = requests.post(self.endpoint, headers=headers, json=payload)
23
- response.raise_for_status() # Raise an error for bad responses
24
- return response.json() # Return the JSON response
25
- except requests.exceptions.RequestException as e:
26
- print(f'Error during API call: {e}')
27
- return None
28
-
29
- # Example usage:
30
- # service = GeminiService('your_api_key')
31
- # email_response = service.generate_email('Generate an email for...', ['Chat history item 1', 'Chat history item 2'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/main.py CHANGED
@@ -1,11 +1,260 @@
1
- from fastapi import FastAPI
 
 
 
 
 
 
 
 
 
 
 
 
2
  from fastapi.staticfiles import StaticFiles
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
- app = FastAPI()
 
 
 
 
 
 
 
 
 
 
5
 
6
- # Serve the React frontend build
7
- app.mount("/static", StaticFiles(directory="static"), name="static")
8
 
9
- @app.get("/")
10
- async def read_root():
11
- return {"message": "Welcome to the FastAPI backend serving the React app"}
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import time
6
+ import uuid
7
+ from pathlib import Path
8
+ from typing import Literal
9
+
10
+ from fastapi import FastAPI, HTTPException, Request
11
+ from dotenv import load_dotenv
12
+ from fastapi.middleware.cors import CORSMiddleware
13
+ from fastapi.responses import FileResponse
14
  from fastapi.staticfiles import StaticFiles
15
+ from google import genai
16
+ from google.genai import types
17
+ from pydantic import BaseModel, Field
18
+
19
+ from backend.system_prompts import get_enhanced_prompt, get_unified_email_prompt
20
+
21
+ load_dotenv()
22
+
23
+ DEFAULT_MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash")
24
+ GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
25
+ DATA_DIR = Path(os.environ.get("APP_DATA_DIR", "backend/data"))
26
+ FRONTEND_DIST = Path("frontend/dist")
27
+
28
+
29
+ class MessageIn(BaseModel):
30
+ content: str = Field(min_length=1)
31
+ is_example: bool = False
32
+
33
+
34
+ class CreateChatIn(BaseModel):
35
+ title: str | None = None
36
+
37
+
38
+ class ChatSummary(BaseModel):
39
+ chat_id: str
40
+ title: str
41
+ updated_at: float
42
+
43
+
44
+ class MessageOut(BaseModel):
45
+ role: Literal["user", "assistant"]
46
+ content: str
47
+
48
+
49
+ class ChatMessageResponse(BaseModel):
50
+ response: str
51
+ chat_id: str
52
+
53
+
54
+ app = FastAPI(title="Chatbot Email API")
55
+
56
+ app.add_middleware(
57
+ CORSMiddleware,
58
+ allow_origins=["*"],
59
+ allow_credentials=True,
60
+ allow_methods=["*"],
61
+ allow_headers=["*"],
62
+ )
63
+
64
+
65
+ if FRONTEND_DIST.exists():
66
+ app.mount("/assets", StaticFiles(directory=FRONTEND_DIST / "assets"), name="assets")
67
+
68
+
69
+ def _namespace(request: Request) -> str:
70
+ user_id = request.headers.get("x-user-id") or request.query_params.get("uid") or "default"
71
+ return "".join(ch for ch in user_id if ch.isalnum() or ch in ("-", "_")) or "default"
72
+
73
+
74
+ def _user_dir(namespace: str) -> Path:
75
+ user_dir = DATA_DIR / namespace
76
+ user_dir.mkdir(parents=True, exist_ok=True)
77
+ return user_dir
78
+
79
+
80
+ def _index_path(namespace: str) -> Path:
81
+ return _user_dir(namespace) / "chats_index.json"
82
+
83
+
84
+ def _chat_path(namespace: str, chat_id: str) -> Path:
85
+ return _user_dir(namespace) / f"{chat_id}.json"
86
+
87
+
88
+ def _read_json(path: Path, default):
89
+ if not path.exists():
90
+ return default
91
+ return json.loads(path.read_text(encoding="utf-8"))
92
+
93
+
94
+ def _write_json(path: Path, payload) -> None:
95
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
96
+
97
+
98
+ def _load_index(namespace: str) -> dict[str, dict]:
99
+ return _read_json(_index_path(namespace), default={})
100
+
101
+
102
+ def _save_index(namespace: str, data: dict[str, dict]) -> None:
103
+ _write_json(_index_path(namespace), data)
104
+
105
+
106
+ def _load_messages(namespace: str, chat_id: str) -> list[dict]:
107
+ payload = _read_json(_chat_path(namespace, chat_id), default={"messages": []})
108
+ return payload.get("messages", [])
109
+
110
+
111
+ def _save_messages(namespace: str, chat_id: str, messages: list[dict]) -> None:
112
+ _write_json(_chat_path(namespace, chat_id), {"messages": messages})
113
+
114
+
115
+ def _generate_title(client: genai.Client, prompt: str) -> str:
116
+ try:
117
+ result = client.models.generate_content(
118
+ model=DEFAULT_MODEL,
119
+ contents=(
120
+ "Genera un título natural y humano en español (3 a 6 palabras) "
121
+ "que resuma esta consulta. Devuelve solo el título final: "
122
+ f"'{prompt}'"
123
+ ),
124
+ )
125
+ title = " ".join((result.text or "").strip().replace('"', "").split())
126
+ return " ".join(title.split()[:6]) or f"Sesión-{int(time.time())}"
127
+ except Exception:
128
+ return f"Sesión-{int(time.time())}"
129
+
130
+
131
+
132
+
133
+ def _initialize_model(api_key: str | None = None) -> genai.Client:
134
+ resolved_api_key = api_key or GOOGLE_API_KEY
135
+ if not resolved_api_key:
136
+ raise HTTPException(
137
+ status_code=500,
138
+ detail=(
139
+ "GOOGLE_API_KEY no está configurada. En Hugging Face Spaces agrega este valor en Settings > Secrets."
140
+ ),
141
+ )
142
+
143
+ try:
144
+ return genai.Client(api_key=resolved_api_key)
145
+ except Exception as exc:
146
+ raise HTTPException(
147
+ status_code=500,
148
+ detail=f"No se pudo inicializar Gemini con GOOGLE_API_KEY: {exc}",
149
+ ) from exc
150
+
151
+ def _build_contents(messages: list[dict]) -> list[types.Content]:
152
+ contents: list[types.Content] = []
153
+ for msg in messages:
154
+ role = "user" if msg["role"] == "user" else "model"
155
+ contents.append(types.Content(role=role, parts=[types.Part(text=msg["content"])]))
156
+ return contents
157
+
158
+
159
+ @app.get("/api/health")
160
+ def health_check():
161
+ return {"ok": True}
162
+
163
+
164
+ @app.get("/api/chats", response_model=list[ChatSummary])
165
+ def list_chats(request: Request):
166
+ index = _load_index(_namespace(request))
167
+ rows = [
168
+ ChatSummary(chat_id=chat_id, title=row["title"], updated_at=row.get("updated_at", 0.0))
169
+ for chat_id, row in index.items()
170
+ ]
171
+ return sorted(rows, key=lambda row: row.updated_at, reverse=True)
172
+
173
+
174
+ @app.post("/api/chats", response_model=ChatSummary)
175
+ def create_chat(payload: CreateChatIn, request: Request):
176
+ namespace = _namespace(request)
177
+ chat_id = str(int(time.time() * 1000)) + uuid.uuid4().hex[:6]
178
+ index = _load_index(namespace)
179
+ now = time.time()
180
+ title = payload.title or f"Sesión-{chat_id[-6:]}"
181
+ index[chat_id] = {"title": title, "updated_at": now}
182
+ _save_index(namespace, index)
183
+ _save_messages(namespace, chat_id, [])
184
+ return ChatSummary(chat_id=chat_id, title=title, updated_at=now)
185
+
186
+
187
+ @app.get("/api/chats/{chat_id}/messages", response_model=list[MessageOut])
188
+ def get_messages(chat_id: str, request: Request):
189
+ namespace = _namespace(request)
190
+ index = _load_index(namespace)
191
+ if chat_id not in index:
192
+ raise HTTPException(status_code=404, detail="Chat no encontrado")
193
+ messages = _load_messages(namespace, chat_id)
194
+ return [MessageOut(role=m["role"], content=m["content"]) for m in messages]
195
+
196
+
197
+ @app.post("/api/chats/{chat_id}/messages", response_model=ChatMessageResponse)
198
+ def send_message(chat_id: str, payload: MessageIn, request: Request):
199
+ namespace = _namespace(request)
200
+ index = _load_index(namespace)
201
+ if chat_id not in index:
202
+ raise HTTPException(status_code=404, detail="Chat no encontrado")
203
+
204
+ messages = _load_messages(namespace, chat_id)
205
+ user_message = {"role": "user", "content": payload.content}
206
+ messages.append(user_message)
207
+
208
+ client = _initialize_model()
209
+ first_user_message = len([m for m in messages if m["role"] == "user"]) == 1
210
+ enhanced_prompt = get_enhanced_prompt(
211
+ payload.content,
212
+ is_example=payload.is_example,
213
+ is_first_user_message=first_user_message,
214
+ )
215
+
216
+ messages_for_model = messages[:-1] + [{"role": "user", "content": enhanced_prompt}]
217
+
218
+ response = client.models.generate_content(
219
+ model=DEFAULT_MODEL,
220
+ contents=_build_contents(messages_for_model),
221
+ config=types.GenerateContentConfig(system_instruction=get_unified_email_prompt()),
222
+ )
223
+
224
+ assistant_text = (response.text or "").strip()
225
+ if not assistant_text:
226
+ assistant_text = "No pude generar respuesta en este intento."
227
+
228
+ messages.append({"role": "assistant", "content": assistant_text})
229
+ _save_messages(namespace, chat_id, messages)
230
+
231
+ if not index[chat_id].get("title") or index[chat_id]["title"].startswith("Sesión-"):
232
+ index[chat_id]["title"] = _generate_title(client, payload.content)
233
+ index[chat_id]["updated_at"] = time.time()
234
+ _save_index(namespace, index)
235
+
236
+ return ChatMessageResponse(response=assistant_text, chat_id=chat_id)
237
+
238
 
239
+ # Backward compatibility for previous React call
240
+ @app.post("/api/chat/send", response_model=ChatMessageResponse)
241
+ def send_message_legacy(payload: MessageIn, request: Request):
242
+ namespace = _namespace(request)
243
+ index = _load_index(namespace)
244
+ if not index:
245
+ chat = create_chat(CreateChatIn(title="Nuevo chat"), request)
246
+ else:
247
+ latest_chat = sorted(index.items(), key=lambda item: item[1].get("updated_at", 0), reverse=True)[0]
248
+ chat = ChatSummary(chat_id=latest_chat[0], title=latest_chat[1]["title"], updated_at=latest_chat[1]["updated_at"])
249
+ return send_message(chat.chat_id, payload, request)
250
 
 
 
251
 
252
+ @app.get("/{full_path:path}")
253
+ def serve_react_app(full_path: str):
254
+ index_file = FRONTEND_DIST / "index.html"
255
+ if index_file.exists():
256
+ return FileResponse(index_file)
257
+ return {
258
+ "message": "API running. Frontend build not found.",
259
+ "hint": "Build frontend with: cd frontend && npm run build",
260
+ }
backend/requirements.txt CHANGED
@@ -1,2 +1,5 @@
1
  fastapi
2
- gemini
 
 
 
 
1
  fastapi
2
+ uvicorn
3
+ google-genai
4
+ pydantic
5
+ python-dotenv
backend/system_prompts.py CHANGED
@@ -1,47 +1,110 @@
1
- # Email Generation Prompt System
2
 
3
- def email_generation_prompt():
4
- print("Welcome to the Email Generation System! Please answer the following questions:")
5
 
6
- questions = [
7
- "1. What is the main purpose of your email? (e.g., inquiry, response, update)",
8
- "2. Who is the recipient of your email? (e.g., colleague, client, friend)",
9
- "3. What specific information do you want to convey?",
10
- "4. What tone do you want to use? (e.g., formal, casual, friendly)",
11
- "5. Do you want to include any attachments? (yes/no)"
12
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
- responses = []
 
 
 
 
15
 
16
- for question in questions:
17
- response = input(question + " ")
18
- responses.append(response)
19
 
20
- print("Thank you for your responses! Here is a draft of your email:")
21
- draft_email(responses)
22
 
 
23
 
24
- def draft_email(responses):
25
- purpose, recipient, info, tone, attachments = responses
26
- draft = f"Dear {recipient},\n\n"
27
 
28
- if tone == "formal":
29
- draft += f"I am writing to you regarding {purpose}.\n"
30
- elif tone == "casual":
31
- draft += f"Hey {recipient}, just wanted to talk about {purpose}.\n"
32
- else:
33
- draft += f"Hello {recipient}, I hope this message finds you well. I wanted to discuss {purpose}.\n"
34
 
35
- draft += f"Here is the information you requested: {info}.\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- if attachments.lower() == "yes":
38
- draft += "Please find the attachments included.\n"
39
- else:
40
- draft += "If you need any additional information, feel free to reach out.\n"
41
 
42
- draft += "\nBest regards,\nYour Name"
 
 
 
 
 
 
 
43
 
44
- print(draft)
 
 
 
 
 
 
45
 
46
- # Uncomment to run the email generation prompt
47
- # email_generation_prompt()
 
1
+ from __future__ import annotations
2
 
 
 
3
 
4
+ def get_unified_email_prompt() -> str:
5
+ return """### [IMPRIMACIÓN COGNITIVA]
6
+ - Modelos fundacionales: Storytelling Marketing, Show Don't Tell, PAS, Golden Circle (empezar con el porqué).
7
+ - Corpus de conocimiento: estilo tipo Seth Godin, estructura narrativa tipo StoryBrand, persuasión sutil estilo Cialdini.
8
+ - Léxico clave: Anécdota catalizadora, Puente narrativo, Epifanía, Lección clave, Llamada a la acción contextual, Resonancia emocional.
9
+ - Usa marcos fundacionales de copy (AIDA, PASA y PASTOR) solo como estructura interna.
10
+
11
+ ### [PERSONA]
12
+ Actúa como estratega de email marketing y storyteller experto en copy conversacional.
13
+ Tono: empático, amable, curioso, conversacional y perspicaz.
14
+ Audiencia: suscriptores con relación de confianza, que esperan valor y no venta agresiva.
15
+
16
+ ### [MISIÓN]
17
+ Guiar de forma interactiva para crear emails de marketing.
18
+ Antes de redactar el email final, debes recopilar estos datos:
19
+ 1) Audiencia objetivo.
20
+ 2) Producto a promover.
21
+ 3) Nombre para firma.
22
+ 4) Llamado a la acción (CTA).
23
+ 5) Ángulo (anécdota/situación/observación; puede incluir personajes de Disney/anime).
24
+ NO debes generar ningún email final antes de recibir los 5 elementos.
25
+
26
+ ### [PRIMERA RESPUESTA OBLIGATORIA]
27
+ Si aún no tienes los 5 datos, inicia con la PRIMERA pregunta del flujo operativo (no pidas todo junto).
28
+
29
+ ### [FLUJO OPERATIVO]
30
+ Haz solo 1 pregunta a la vez y espera respuesta:
31
+ 1) AUDIENCIA:
32
+ "¿A quién le vas a escribir este email? Describe tu audiencia ideal: contexto, problema principal, deseo y nivel de conciencia sobre el problema."
33
+ 2) PRODUCTO:
34
+ "¿Qué producto o servicio vas a promover y qué transformación principal consigue la persona que lo compra?"
35
+ 3) NOMBRE:
36
+ "¿Con qué nombre quieres firmar el correo?"
37
+ 4) CTA:
38
+ "¿Qué acción concreta quieres que la audiencia realice al final del email? (responder, agendar llamada, comprar, visitar enlace, etc.)"
39
+ 5) ÁNGULO (OBLIGATORIO):
40
+ "¿Qué ángulo, anécdota o situación específica quieres usar en este correo? (Puedes apoyarte en referencias como Disney/anime si encaja)."
41
+
42
+ ### [RAZONAMIENTO PASO A PASO]
43
+ 1) Descubrimiento guiado (5 preguntas, una por vez; ángulo obligatorio).
44
+ 2) Identificación de dolor/deseo central y transformación del producto.
45
+ 3) Construir puente narrativo a partir del ángulo dado y redactar con enfoque conversacional.
46
+ 4) Usar AIDA, PASA o PASTOR internamente para ordenar el mensaje cuando haga falta.
47
+ 5) Adaptar lenguaje al nivel de conciencia de la audiencia y cerrar con CTA explícito.
48
+ 6) Usar el nombre de firma proporcionado en el cierre final del email.
49
 
50
+ ### [RESTRICCIONES]
51
+ - No uses clichés de marketing.
52
+ - No fuerces la conexión historia-producto; pide más contexto si hace falta.
53
+ - No te enfoques en características, precios o descuentos.
54
+ - Nunca menciones al usuario qué fórmula interna usaste (AIDA, PASA o PASTOR).
55
 
56
+ ### [FORMATO DE SALIDA FINAL - EXACTO]
57
+ **Asunto:** [Texto del Asunto en negrita]
 
58
 
59
+ ---
 
60
 
61
+ **Cuerpo del Email:**
62
 
63
+ [Párrafo 1: La anécdota]
 
 
64
 
65
+ [Párrafo 2: La transición hacia la lección]
 
 
 
 
 
66
 
67
+ [Párrafo 3: La lección clave y cómo se conecta con un problema general]
68
+
69
+ [Párrafo 4: Presentación del producto como la herramienta para aplicar la lección]
70
+
71
+ [Párrafo 5: Llamada a la acción clara y directa]
72
+
73
+ [Cierre personal]
74
+ """
75
+
76
+
77
+ def is_greeting(text: str, is_first_user_message: bool) -> bool:
78
+ normalized = (text or "").lower().strip()
79
+ greetings = [
80
+ "hola",
81
+ "hey",
82
+ "saludos",
83
+ "buenos días",
84
+ "buenas tardes",
85
+ "buenas noches",
86
+ "hi",
87
+ "hello",
88
+ ]
89
+ is_simple = any(greeting in normalized for greeting in greetings) and len(normalized.split()) < 4
90
+ return is_simple and is_first_user_message
91
 
 
 
 
 
92
 
93
+ def get_enhanced_prompt(prompt: str, *, is_example: bool, is_first_user_message: bool) -> str:
94
+ if is_greeting(prompt, is_first_user_message):
95
+ return (
96
+ "Responde ÚNICAMENTE con esta frase, sin agregar nada más: "
97
+ "\"¡Perfecto! Empecemos por la primera: "
98
+ "¿Quién es tu audiencia ideal para este correo? "
99
+ "Descríbela con detalle (contexto, problema principal, deseo y nivel de conciencia).\""
100
+ )
101
 
102
+ if is_example:
103
+ return (
104
+ f"El usuario seleccionó esta pregunta del menú: '{prompt}'. "
105
+ "Respóndela de forma directa, útil y conversacional, con ejemplos concretos. "
106
+ "Después de responder, invita al usuario a iniciar el flujo de 5 preguntas "
107
+ "en este orden: audiencia, producto, nombre, CTA y ángulo."
108
+ )
109
 
110
+ return prompt
 
frontend/package-lock.json ADDED
@@ -0,0 +1,1729 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chatbot-email-app",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "chatbot-email-app",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "react": "^18.2.0",
12
+ "react-dom": "^18.2.0"
13
+ },
14
+ "devDependencies": {
15
+ "@types/react": "^18.2.0",
16
+ "@types/react-dom": "^18.2.0",
17
+ "@vitejs/plugin-react": "^4.2.0",
18
+ "typescript": "^5.3.0",
19
+ "vite": "^5.0.0"
20
+ }
21
+ },
22
+ "node_modules/@babel/code-frame": {
23
+ "version": "7.29.0",
24
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
25
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
26
+ "dev": true,
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "@babel/helper-validator-identifier": "^7.28.5",
30
+ "js-tokens": "^4.0.0",
31
+ "picocolors": "^1.1.1"
32
+ },
33
+ "engines": {
34
+ "node": ">=6.9.0"
35
+ }
36
+ },
37
+ "node_modules/@babel/compat-data": {
38
+ "version": "7.29.0",
39
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
40
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
41
+ "dev": true,
42
+ "license": "MIT",
43
+ "engines": {
44
+ "node": ">=6.9.0"
45
+ }
46
+ },
47
+ "node_modules/@babel/core": {
48
+ "version": "7.29.0",
49
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
50
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
51
+ "dev": true,
52
+ "license": "MIT",
53
+ "dependencies": {
54
+ "@babel/code-frame": "^7.29.0",
55
+ "@babel/generator": "^7.29.0",
56
+ "@babel/helper-compilation-targets": "^7.28.6",
57
+ "@babel/helper-module-transforms": "^7.28.6",
58
+ "@babel/helpers": "^7.28.6",
59
+ "@babel/parser": "^7.29.0",
60
+ "@babel/template": "^7.28.6",
61
+ "@babel/traverse": "^7.29.0",
62
+ "@babel/types": "^7.29.0",
63
+ "@jridgewell/remapping": "^2.3.5",
64
+ "convert-source-map": "^2.0.0",
65
+ "debug": "^4.1.0",
66
+ "gensync": "^1.0.0-beta.2",
67
+ "json5": "^2.2.3",
68
+ "semver": "^6.3.1"
69
+ },
70
+ "engines": {
71
+ "node": ">=6.9.0"
72
+ },
73
+ "funding": {
74
+ "type": "opencollective",
75
+ "url": "https://opencollective.com/babel"
76
+ }
77
+ },
78
+ "node_modules/@babel/generator": {
79
+ "version": "7.29.1",
80
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
81
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
82
+ "dev": true,
83
+ "license": "MIT",
84
+ "dependencies": {
85
+ "@babel/parser": "^7.29.0",
86
+ "@babel/types": "^7.29.0",
87
+ "@jridgewell/gen-mapping": "^0.3.12",
88
+ "@jridgewell/trace-mapping": "^0.3.28",
89
+ "jsesc": "^3.0.2"
90
+ },
91
+ "engines": {
92
+ "node": ">=6.9.0"
93
+ }
94
+ },
95
+ "node_modules/@babel/helper-compilation-targets": {
96
+ "version": "7.28.6",
97
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
98
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
99
+ "dev": true,
100
+ "license": "MIT",
101
+ "dependencies": {
102
+ "@babel/compat-data": "^7.28.6",
103
+ "@babel/helper-validator-option": "^7.27.1",
104
+ "browserslist": "^4.24.0",
105
+ "lru-cache": "^5.1.1",
106
+ "semver": "^6.3.1"
107
+ },
108
+ "engines": {
109
+ "node": ">=6.9.0"
110
+ }
111
+ },
112
+ "node_modules/@babel/helper-globals": {
113
+ "version": "7.28.0",
114
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
115
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
116
+ "dev": true,
117
+ "license": "MIT",
118
+ "engines": {
119
+ "node": ">=6.9.0"
120
+ }
121
+ },
122
+ "node_modules/@babel/helper-module-imports": {
123
+ "version": "7.28.6",
124
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
125
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
126
+ "dev": true,
127
+ "license": "MIT",
128
+ "dependencies": {
129
+ "@babel/traverse": "^7.28.6",
130
+ "@babel/types": "^7.28.6"
131
+ },
132
+ "engines": {
133
+ "node": ">=6.9.0"
134
+ }
135
+ },
136
+ "node_modules/@babel/helper-module-transforms": {
137
+ "version": "7.28.6",
138
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
139
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
140
+ "dev": true,
141
+ "license": "MIT",
142
+ "dependencies": {
143
+ "@babel/helper-module-imports": "^7.28.6",
144
+ "@babel/helper-validator-identifier": "^7.28.5",
145
+ "@babel/traverse": "^7.28.6"
146
+ },
147
+ "engines": {
148
+ "node": ">=6.9.0"
149
+ },
150
+ "peerDependencies": {
151
+ "@babel/core": "^7.0.0"
152
+ }
153
+ },
154
+ "node_modules/@babel/helper-plugin-utils": {
155
+ "version": "7.28.6",
156
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
157
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
158
+ "dev": true,
159
+ "license": "MIT",
160
+ "engines": {
161
+ "node": ">=6.9.0"
162
+ }
163
+ },
164
+ "node_modules/@babel/helper-string-parser": {
165
+ "version": "7.27.1",
166
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
167
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
168
+ "dev": true,
169
+ "license": "MIT",
170
+ "engines": {
171
+ "node": ">=6.9.0"
172
+ }
173
+ },
174
+ "node_modules/@babel/helper-validator-identifier": {
175
+ "version": "7.28.5",
176
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
177
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
178
+ "dev": true,
179
+ "license": "MIT",
180
+ "engines": {
181
+ "node": ">=6.9.0"
182
+ }
183
+ },
184
+ "node_modules/@babel/helper-validator-option": {
185
+ "version": "7.27.1",
186
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
187
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
188
+ "dev": true,
189
+ "license": "MIT",
190
+ "engines": {
191
+ "node": ">=6.9.0"
192
+ }
193
+ },
194
+ "node_modules/@babel/helpers": {
195
+ "version": "7.29.2",
196
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
197
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
198
+ "dev": true,
199
+ "license": "MIT",
200
+ "dependencies": {
201
+ "@babel/template": "^7.28.6",
202
+ "@babel/types": "^7.29.0"
203
+ },
204
+ "engines": {
205
+ "node": ">=6.9.0"
206
+ }
207
+ },
208
+ "node_modules/@babel/parser": {
209
+ "version": "7.29.2",
210
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
211
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
212
+ "dev": true,
213
+ "license": "MIT",
214
+ "dependencies": {
215
+ "@babel/types": "^7.29.0"
216
+ },
217
+ "bin": {
218
+ "parser": "bin/babel-parser.js"
219
+ },
220
+ "engines": {
221
+ "node": ">=6.0.0"
222
+ }
223
+ },
224
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
225
+ "version": "7.27.1",
226
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
227
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
228
+ "dev": true,
229
+ "license": "MIT",
230
+ "dependencies": {
231
+ "@babel/helper-plugin-utils": "^7.27.1"
232
+ },
233
+ "engines": {
234
+ "node": ">=6.9.0"
235
+ },
236
+ "peerDependencies": {
237
+ "@babel/core": "^7.0.0-0"
238
+ }
239
+ },
240
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
241
+ "version": "7.27.1",
242
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
243
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
244
+ "dev": true,
245
+ "license": "MIT",
246
+ "dependencies": {
247
+ "@babel/helper-plugin-utils": "^7.27.1"
248
+ },
249
+ "engines": {
250
+ "node": ">=6.9.0"
251
+ },
252
+ "peerDependencies": {
253
+ "@babel/core": "^7.0.0-0"
254
+ }
255
+ },
256
+ "node_modules/@babel/template": {
257
+ "version": "7.28.6",
258
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
259
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
260
+ "dev": true,
261
+ "license": "MIT",
262
+ "dependencies": {
263
+ "@babel/code-frame": "^7.28.6",
264
+ "@babel/parser": "^7.28.6",
265
+ "@babel/types": "^7.28.6"
266
+ },
267
+ "engines": {
268
+ "node": ">=6.9.0"
269
+ }
270
+ },
271
+ "node_modules/@babel/traverse": {
272
+ "version": "7.29.0",
273
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
274
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
275
+ "dev": true,
276
+ "license": "MIT",
277
+ "dependencies": {
278
+ "@babel/code-frame": "^7.29.0",
279
+ "@babel/generator": "^7.29.0",
280
+ "@babel/helper-globals": "^7.28.0",
281
+ "@babel/parser": "^7.29.0",
282
+ "@babel/template": "^7.28.6",
283
+ "@babel/types": "^7.29.0",
284
+ "debug": "^4.3.1"
285
+ },
286
+ "engines": {
287
+ "node": ">=6.9.0"
288
+ }
289
+ },
290
+ "node_modules/@babel/types": {
291
+ "version": "7.29.0",
292
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
293
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
294
+ "dev": true,
295
+ "license": "MIT",
296
+ "dependencies": {
297
+ "@babel/helper-string-parser": "^7.27.1",
298
+ "@babel/helper-validator-identifier": "^7.28.5"
299
+ },
300
+ "engines": {
301
+ "node": ">=6.9.0"
302
+ }
303
+ },
304
+ "node_modules/@esbuild/aix-ppc64": {
305
+ "version": "0.21.5",
306
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
307
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
308
+ "cpu": [
309
+ "ppc64"
310
+ ],
311
+ "dev": true,
312
+ "license": "MIT",
313
+ "optional": true,
314
+ "os": [
315
+ "aix"
316
+ ],
317
+ "engines": {
318
+ "node": ">=12"
319
+ }
320
+ },
321
+ "node_modules/@esbuild/android-arm": {
322
+ "version": "0.21.5",
323
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
324
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
325
+ "cpu": [
326
+ "arm"
327
+ ],
328
+ "dev": true,
329
+ "license": "MIT",
330
+ "optional": true,
331
+ "os": [
332
+ "android"
333
+ ],
334
+ "engines": {
335
+ "node": ">=12"
336
+ }
337
+ },
338
+ "node_modules/@esbuild/android-arm64": {
339
+ "version": "0.21.5",
340
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
341
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
342
+ "cpu": [
343
+ "arm64"
344
+ ],
345
+ "dev": true,
346
+ "license": "MIT",
347
+ "optional": true,
348
+ "os": [
349
+ "android"
350
+ ],
351
+ "engines": {
352
+ "node": ">=12"
353
+ }
354
+ },
355
+ "node_modules/@esbuild/android-x64": {
356
+ "version": "0.21.5",
357
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
358
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
359
+ "cpu": [
360
+ "x64"
361
+ ],
362
+ "dev": true,
363
+ "license": "MIT",
364
+ "optional": true,
365
+ "os": [
366
+ "android"
367
+ ],
368
+ "engines": {
369
+ "node": ">=12"
370
+ }
371
+ },
372
+ "node_modules/@esbuild/darwin-arm64": {
373
+ "version": "0.21.5",
374
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
375
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
376
+ "cpu": [
377
+ "arm64"
378
+ ],
379
+ "dev": true,
380
+ "license": "MIT",
381
+ "optional": true,
382
+ "os": [
383
+ "darwin"
384
+ ],
385
+ "engines": {
386
+ "node": ">=12"
387
+ }
388
+ },
389
+ "node_modules/@esbuild/darwin-x64": {
390
+ "version": "0.21.5",
391
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
392
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
393
+ "cpu": [
394
+ "x64"
395
+ ],
396
+ "dev": true,
397
+ "license": "MIT",
398
+ "optional": true,
399
+ "os": [
400
+ "darwin"
401
+ ],
402
+ "engines": {
403
+ "node": ">=12"
404
+ }
405
+ },
406
+ "node_modules/@esbuild/freebsd-arm64": {
407
+ "version": "0.21.5",
408
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
409
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
410
+ "cpu": [
411
+ "arm64"
412
+ ],
413
+ "dev": true,
414
+ "license": "MIT",
415
+ "optional": true,
416
+ "os": [
417
+ "freebsd"
418
+ ],
419
+ "engines": {
420
+ "node": ">=12"
421
+ }
422
+ },
423
+ "node_modules/@esbuild/freebsd-x64": {
424
+ "version": "0.21.5",
425
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
426
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
427
+ "cpu": [
428
+ "x64"
429
+ ],
430
+ "dev": true,
431
+ "license": "MIT",
432
+ "optional": true,
433
+ "os": [
434
+ "freebsd"
435
+ ],
436
+ "engines": {
437
+ "node": ">=12"
438
+ }
439
+ },
440
+ "node_modules/@esbuild/linux-arm": {
441
+ "version": "0.21.5",
442
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
443
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
444
+ "cpu": [
445
+ "arm"
446
+ ],
447
+ "dev": true,
448
+ "license": "MIT",
449
+ "optional": true,
450
+ "os": [
451
+ "linux"
452
+ ],
453
+ "engines": {
454
+ "node": ">=12"
455
+ }
456
+ },
457
+ "node_modules/@esbuild/linux-arm64": {
458
+ "version": "0.21.5",
459
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
460
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
461
+ "cpu": [
462
+ "arm64"
463
+ ],
464
+ "dev": true,
465
+ "license": "MIT",
466
+ "optional": true,
467
+ "os": [
468
+ "linux"
469
+ ],
470
+ "engines": {
471
+ "node": ">=12"
472
+ }
473
+ },
474
+ "node_modules/@esbuild/linux-ia32": {
475
+ "version": "0.21.5",
476
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
477
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
478
+ "cpu": [
479
+ "ia32"
480
+ ],
481
+ "dev": true,
482
+ "license": "MIT",
483
+ "optional": true,
484
+ "os": [
485
+ "linux"
486
+ ],
487
+ "engines": {
488
+ "node": ">=12"
489
+ }
490
+ },
491
+ "node_modules/@esbuild/linux-loong64": {
492
+ "version": "0.21.5",
493
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
494
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
495
+ "cpu": [
496
+ "loong64"
497
+ ],
498
+ "dev": true,
499
+ "license": "MIT",
500
+ "optional": true,
501
+ "os": [
502
+ "linux"
503
+ ],
504
+ "engines": {
505
+ "node": ">=12"
506
+ }
507
+ },
508
+ "node_modules/@esbuild/linux-mips64el": {
509
+ "version": "0.21.5",
510
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
511
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
512
+ "cpu": [
513
+ "mips64el"
514
+ ],
515
+ "dev": true,
516
+ "license": "MIT",
517
+ "optional": true,
518
+ "os": [
519
+ "linux"
520
+ ],
521
+ "engines": {
522
+ "node": ">=12"
523
+ }
524
+ },
525
+ "node_modules/@esbuild/linux-ppc64": {
526
+ "version": "0.21.5",
527
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
528
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
529
+ "cpu": [
530
+ "ppc64"
531
+ ],
532
+ "dev": true,
533
+ "license": "MIT",
534
+ "optional": true,
535
+ "os": [
536
+ "linux"
537
+ ],
538
+ "engines": {
539
+ "node": ">=12"
540
+ }
541
+ },
542
+ "node_modules/@esbuild/linux-riscv64": {
543
+ "version": "0.21.5",
544
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
545
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
546
+ "cpu": [
547
+ "riscv64"
548
+ ],
549
+ "dev": true,
550
+ "license": "MIT",
551
+ "optional": true,
552
+ "os": [
553
+ "linux"
554
+ ],
555
+ "engines": {
556
+ "node": ">=12"
557
+ }
558
+ },
559
+ "node_modules/@esbuild/linux-s390x": {
560
+ "version": "0.21.5",
561
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
562
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
563
+ "cpu": [
564
+ "s390x"
565
+ ],
566
+ "dev": true,
567
+ "license": "MIT",
568
+ "optional": true,
569
+ "os": [
570
+ "linux"
571
+ ],
572
+ "engines": {
573
+ "node": ">=12"
574
+ }
575
+ },
576
+ "node_modules/@esbuild/linux-x64": {
577
+ "version": "0.21.5",
578
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
579
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
580
+ "cpu": [
581
+ "x64"
582
+ ],
583
+ "dev": true,
584
+ "license": "MIT",
585
+ "optional": true,
586
+ "os": [
587
+ "linux"
588
+ ],
589
+ "engines": {
590
+ "node": ">=12"
591
+ }
592
+ },
593
+ "node_modules/@esbuild/netbsd-x64": {
594
+ "version": "0.21.5",
595
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
596
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
597
+ "cpu": [
598
+ "x64"
599
+ ],
600
+ "dev": true,
601
+ "license": "MIT",
602
+ "optional": true,
603
+ "os": [
604
+ "netbsd"
605
+ ],
606
+ "engines": {
607
+ "node": ">=12"
608
+ }
609
+ },
610
+ "node_modules/@esbuild/openbsd-x64": {
611
+ "version": "0.21.5",
612
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
613
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
614
+ "cpu": [
615
+ "x64"
616
+ ],
617
+ "dev": true,
618
+ "license": "MIT",
619
+ "optional": true,
620
+ "os": [
621
+ "openbsd"
622
+ ],
623
+ "engines": {
624
+ "node": ">=12"
625
+ }
626
+ },
627
+ "node_modules/@esbuild/sunos-x64": {
628
+ "version": "0.21.5",
629
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
630
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
631
+ "cpu": [
632
+ "x64"
633
+ ],
634
+ "dev": true,
635
+ "license": "MIT",
636
+ "optional": true,
637
+ "os": [
638
+ "sunos"
639
+ ],
640
+ "engines": {
641
+ "node": ">=12"
642
+ }
643
+ },
644
+ "node_modules/@esbuild/win32-arm64": {
645
+ "version": "0.21.5",
646
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
647
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
648
+ "cpu": [
649
+ "arm64"
650
+ ],
651
+ "dev": true,
652
+ "license": "MIT",
653
+ "optional": true,
654
+ "os": [
655
+ "win32"
656
+ ],
657
+ "engines": {
658
+ "node": ">=12"
659
+ }
660
+ },
661
+ "node_modules/@esbuild/win32-ia32": {
662
+ "version": "0.21.5",
663
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
664
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
665
+ "cpu": [
666
+ "ia32"
667
+ ],
668
+ "dev": true,
669
+ "license": "MIT",
670
+ "optional": true,
671
+ "os": [
672
+ "win32"
673
+ ],
674
+ "engines": {
675
+ "node": ">=12"
676
+ }
677
+ },
678
+ "node_modules/@esbuild/win32-x64": {
679
+ "version": "0.21.5",
680
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
681
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
682
+ "cpu": [
683
+ "x64"
684
+ ],
685
+ "dev": true,
686
+ "license": "MIT",
687
+ "optional": true,
688
+ "os": [
689
+ "win32"
690
+ ],
691
+ "engines": {
692
+ "node": ">=12"
693
+ }
694
+ },
695
+ "node_modules/@jridgewell/gen-mapping": {
696
+ "version": "0.3.13",
697
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
698
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
699
+ "dev": true,
700
+ "license": "MIT",
701
+ "dependencies": {
702
+ "@jridgewell/sourcemap-codec": "^1.5.0",
703
+ "@jridgewell/trace-mapping": "^0.3.24"
704
+ }
705
+ },
706
+ "node_modules/@jridgewell/remapping": {
707
+ "version": "2.3.5",
708
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
709
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
710
+ "dev": true,
711
+ "license": "MIT",
712
+ "dependencies": {
713
+ "@jridgewell/gen-mapping": "^0.3.5",
714
+ "@jridgewell/trace-mapping": "^0.3.24"
715
+ }
716
+ },
717
+ "node_modules/@jridgewell/resolve-uri": {
718
+ "version": "3.1.2",
719
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
720
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
721
+ "dev": true,
722
+ "license": "MIT",
723
+ "engines": {
724
+ "node": ">=6.0.0"
725
+ }
726
+ },
727
+ "node_modules/@jridgewell/sourcemap-codec": {
728
+ "version": "1.5.5",
729
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
730
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
731
+ "dev": true,
732
+ "license": "MIT"
733
+ },
734
+ "node_modules/@jridgewell/trace-mapping": {
735
+ "version": "0.3.31",
736
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
737
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
738
+ "dev": true,
739
+ "license": "MIT",
740
+ "dependencies": {
741
+ "@jridgewell/resolve-uri": "^3.1.0",
742
+ "@jridgewell/sourcemap-codec": "^1.4.14"
743
+ }
744
+ },
745
+ "node_modules/@rolldown/pluginutils": {
746
+ "version": "1.0.0-beta.27",
747
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
748
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
749
+ "dev": true,
750
+ "license": "MIT"
751
+ },
752
+ "node_modules/@rollup/rollup-android-arm-eabi": {
753
+ "version": "4.60.1",
754
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
755
+ "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
756
+ "cpu": [
757
+ "arm"
758
+ ],
759
+ "dev": true,
760
+ "license": "MIT",
761
+ "optional": true,
762
+ "os": [
763
+ "android"
764
+ ]
765
+ },
766
+ "node_modules/@rollup/rollup-android-arm64": {
767
+ "version": "4.60.1",
768
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
769
+ "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
770
+ "cpu": [
771
+ "arm64"
772
+ ],
773
+ "dev": true,
774
+ "license": "MIT",
775
+ "optional": true,
776
+ "os": [
777
+ "android"
778
+ ]
779
+ },
780
+ "node_modules/@rollup/rollup-darwin-arm64": {
781
+ "version": "4.60.1",
782
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
783
+ "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
784
+ "cpu": [
785
+ "arm64"
786
+ ],
787
+ "dev": true,
788
+ "license": "MIT",
789
+ "optional": true,
790
+ "os": [
791
+ "darwin"
792
+ ]
793
+ },
794
+ "node_modules/@rollup/rollup-darwin-x64": {
795
+ "version": "4.60.1",
796
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
797
+ "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
798
+ "cpu": [
799
+ "x64"
800
+ ],
801
+ "dev": true,
802
+ "license": "MIT",
803
+ "optional": true,
804
+ "os": [
805
+ "darwin"
806
+ ]
807
+ },
808
+ "node_modules/@rollup/rollup-freebsd-arm64": {
809
+ "version": "4.60.1",
810
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
811
+ "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
812
+ "cpu": [
813
+ "arm64"
814
+ ],
815
+ "dev": true,
816
+ "license": "MIT",
817
+ "optional": true,
818
+ "os": [
819
+ "freebsd"
820
+ ]
821
+ },
822
+ "node_modules/@rollup/rollup-freebsd-x64": {
823
+ "version": "4.60.1",
824
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
825
+ "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
826
+ "cpu": [
827
+ "x64"
828
+ ],
829
+ "dev": true,
830
+ "license": "MIT",
831
+ "optional": true,
832
+ "os": [
833
+ "freebsd"
834
+ ]
835
+ },
836
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
837
+ "version": "4.60.1",
838
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
839
+ "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
840
+ "cpu": [
841
+ "arm"
842
+ ],
843
+ "dev": true,
844
+ "license": "MIT",
845
+ "optional": true,
846
+ "os": [
847
+ "linux"
848
+ ]
849
+ },
850
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
851
+ "version": "4.60.1",
852
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
853
+ "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
854
+ "cpu": [
855
+ "arm"
856
+ ],
857
+ "dev": true,
858
+ "license": "MIT",
859
+ "optional": true,
860
+ "os": [
861
+ "linux"
862
+ ]
863
+ },
864
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
865
+ "version": "4.60.1",
866
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
867
+ "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
868
+ "cpu": [
869
+ "arm64"
870
+ ],
871
+ "dev": true,
872
+ "license": "MIT",
873
+ "optional": true,
874
+ "os": [
875
+ "linux"
876
+ ]
877
+ },
878
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
879
+ "version": "4.60.1",
880
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
881
+ "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
882
+ "cpu": [
883
+ "arm64"
884
+ ],
885
+ "dev": true,
886
+ "license": "MIT",
887
+ "optional": true,
888
+ "os": [
889
+ "linux"
890
+ ]
891
+ },
892
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
893
+ "version": "4.60.1",
894
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
895
+ "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
896
+ "cpu": [
897
+ "loong64"
898
+ ],
899
+ "dev": true,
900
+ "license": "MIT",
901
+ "optional": true,
902
+ "os": [
903
+ "linux"
904
+ ]
905
+ },
906
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
907
+ "version": "4.60.1",
908
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
909
+ "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
910
+ "cpu": [
911
+ "loong64"
912
+ ],
913
+ "dev": true,
914
+ "license": "MIT",
915
+ "optional": true,
916
+ "os": [
917
+ "linux"
918
+ ]
919
+ },
920
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
921
+ "version": "4.60.1",
922
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
923
+ "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
924
+ "cpu": [
925
+ "ppc64"
926
+ ],
927
+ "dev": true,
928
+ "license": "MIT",
929
+ "optional": true,
930
+ "os": [
931
+ "linux"
932
+ ]
933
+ },
934
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
935
+ "version": "4.60.1",
936
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
937
+ "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
938
+ "cpu": [
939
+ "ppc64"
940
+ ],
941
+ "dev": true,
942
+ "license": "MIT",
943
+ "optional": true,
944
+ "os": [
945
+ "linux"
946
+ ]
947
+ },
948
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
949
+ "version": "4.60.1",
950
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
951
+ "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
952
+ "cpu": [
953
+ "riscv64"
954
+ ],
955
+ "dev": true,
956
+ "license": "MIT",
957
+ "optional": true,
958
+ "os": [
959
+ "linux"
960
+ ]
961
+ },
962
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
963
+ "version": "4.60.1",
964
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
965
+ "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
966
+ "cpu": [
967
+ "riscv64"
968
+ ],
969
+ "dev": true,
970
+ "license": "MIT",
971
+ "optional": true,
972
+ "os": [
973
+ "linux"
974
+ ]
975
+ },
976
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
977
+ "version": "4.60.1",
978
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
979
+ "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
980
+ "cpu": [
981
+ "s390x"
982
+ ],
983
+ "dev": true,
984
+ "license": "MIT",
985
+ "optional": true,
986
+ "os": [
987
+ "linux"
988
+ ]
989
+ },
990
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
991
+ "version": "4.60.1",
992
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
993
+ "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
994
+ "cpu": [
995
+ "x64"
996
+ ],
997
+ "dev": true,
998
+ "license": "MIT",
999
+ "optional": true,
1000
+ "os": [
1001
+ "linux"
1002
+ ]
1003
+ },
1004
+ "node_modules/@rollup/rollup-linux-x64-musl": {
1005
+ "version": "4.60.1",
1006
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
1007
+ "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
1008
+ "cpu": [
1009
+ "x64"
1010
+ ],
1011
+ "dev": true,
1012
+ "license": "MIT",
1013
+ "optional": true,
1014
+ "os": [
1015
+ "linux"
1016
+ ]
1017
+ },
1018
+ "node_modules/@rollup/rollup-openbsd-x64": {
1019
+ "version": "4.60.1",
1020
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
1021
+ "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
1022
+ "cpu": [
1023
+ "x64"
1024
+ ],
1025
+ "dev": true,
1026
+ "license": "MIT",
1027
+ "optional": true,
1028
+ "os": [
1029
+ "openbsd"
1030
+ ]
1031
+ },
1032
+ "node_modules/@rollup/rollup-openharmony-arm64": {
1033
+ "version": "4.60.1",
1034
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
1035
+ "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
1036
+ "cpu": [
1037
+ "arm64"
1038
+ ],
1039
+ "dev": true,
1040
+ "license": "MIT",
1041
+ "optional": true,
1042
+ "os": [
1043
+ "openharmony"
1044
+ ]
1045
+ },
1046
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1047
+ "version": "4.60.1",
1048
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
1049
+ "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
1050
+ "cpu": [
1051
+ "arm64"
1052
+ ],
1053
+ "dev": true,
1054
+ "license": "MIT",
1055
+ "optional": true,
1056
+ "os": [
1057
+ "win32"
1058
+ ]
1059
+ },
1060
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1061
+ "version": "4.60.1",
1062
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
1063
+ "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
1064
+ "cpu": [
1065
+ "ia32"
1066
+ ],
1067
+ "dev": true,
1068
+ "license": "MIT",
1069
+ "optional": true,
1070
+ "os": [
1071
+ "win32"
1072
+ ]
1073
+ },
1074
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1075
+ "version": "4.60.1",
1076
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
1077
+ "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
1078
+ "cpu": [
1079
+ "x64"
1080
+ ],
1081
+ "dev": true,
1082
+ "license": "MIT",
1083
+ "optional": true,
1084
+ "os": [
1085
+ "win32"
1086
+ ]
1087
+ },
1088
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1089
+ "version": "4.60.1",
1090
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
1091
+ "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
1092
+ "cpu": [
1093
+ "x64"
1094
+ ],
1095
+ "dev": true,
1096
+ "license": "MIT",
1097
+ "optional": true,
1098
+ "os": [
1099
+ "win32"
1100
+ ]
1101
+ },
1102
+ "node_modules/@types/babel__core": {
1103
+ "version": "7.20.5",
1104
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1105
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1106
+ "dev": true,
1107
+ "license": "MIT",
1108
+ "dependencies": {
1109
+ "@babel/parser": "^7.20.7",
1110
+ "@babel/types": "^7.20.7",
1111
+ "@types/babel__generator": "*",
1112
+ "@types/babel__template": "*",
1113
+ "@types/babel__traverse": "*"
1114
+ }
1115
+ },
1116
+ "node_modules/@types/babel__generator": {
1117
+ "version": "7.27.0",
1118
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1119
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1120
+ "dev": true,
1121
+ "license": "MIT",
1122
+ "dependencies": {
1123
+ "@babel/types": "^7.0.0"
1124
+ }
1125
+ },
1126
+ "node_modules/@types/babel__template": {
1127
+ "version": "7.4.4",
1128
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1129
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1130
+ "dev": true,
1131
+ "license": "MIT",
1132
+ "dependencies": {
1133
+ "@babel/parser": "^7.1.0",
1134
+ "@babel/types": "^7.0.0"
1135
+ }
1136
+ },
1137
+ "node_modules/@types/babel__traverse": {
1138
+ "version": "7.28.0",
1139
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1140
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1141
+ "dev": true,
1142
+ "license": "MIT",
1143
+ "dependencies": {
1144
+ "@babel/types": "^7.28.2"
1145
+ }
1146
+ },
1147
+ "node_modules/@types/estree": {
1148
+ "version": "1.0.8",
1149
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
1150
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1151
+ "dev": true,
1152
+ "license": "MIT"
1153
+ },
1154
+ "node_modules/@types/prop-types": {
1155
+ "version": "15.7.15",
1156
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
1157
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
1158
+ "dev": true,
1159
+ "license": "MIT"
1160
+ },
1161
+ "node_modules/@types/react": {
1162
+ "version": "18.3.28",
1163
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
1164
+ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
1165
+ "dev": true,
1166
+ "license": "MIT",
1167
+ "dependencies": {
1168
+ "@types/prop-types": "*",
1169
+ "csstype": "^3.2.2"
1170
+ }
1171
+ },
1172
+ "node_modules/@types/react-dom": {
1173
+ "version": "18.3.7",
1174
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
1175
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
1176
+ "dev": true,
1177
+ "license": "MIT",
1178
+ "peerDependencies": {
1179
+ "@types/react": "^18.0.0"
1180
+ }
1181
+ },
1182
+ "node_modules/@vitejs/plugin-react": {
1183
+ "version": "4.7.0",
1184
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
1185
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
1186
+ "dev": true,
1187
+ "license": "MIT",
1188
+ "dependencies": {
1189
+ "@babel/core": "^7.28.0",
1190
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1191
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1192
+ "@rolldown/pluginutils": "1.0.0-beta.27",
1193
+ "@types/babel__core": "^7.20.5",
1194
+ "react-refresh": "^0.17.0"
1195
+ },
1196
+ "engines": {
1197
+ "node": "^14.18.0 || >=16.0.0"
1198
+ },
1199
+ "peerDependencies": {
1200
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1201
+ }
1202
+ },
1203
+ "node_modules/baseline-browser-mapping": {
1204
+ "version": "2.10.19",
1205
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz",
1206
+ "integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==",
1207
+ "dev": true,
1208
+ "license": "Apache-2.0",
1209
+ "bin": {
1210
+ "baseline-browser-mapping": "dist/cli.cjs"
1211
+ },
1212
+ "engines": {
1213
+ "node": ">=6.0.0"
1214
+ }
1215
+ },
1216
+ "node_modules/browserslist": {
1217
+ "version": "4.28.2",
1218
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
1219
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
1220
+ "dev": true,
1221
+ "funding": [
1222
+ {
1223
+ "type": "opencollective",
1224
+ "url": "https://opencollective.com/browserslist"
1225
+ },
1226
+ {
1227
+ "type": "tidelift",
1228
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1229
+ },
1230
+ {
1231
+ "type": "github",
1232
+ "url": "https://github.com/sponsors/ai"
1233
+ }
1234
+ ],
1235
+ "license": "MIT",
1236
+ "dependencies": {
1237
+ "baseline-browser-mapping": "^2.10.12",
1238
+ "caniuse-lite": "^1.0.30001782",
1239
+ "electron-to-chromium": "^1.5.328",
1240
+ "node-releases": "^2.0.36",
1241
+ "update-browserslist-db": "^1.2.3"
1242
+ },
1243
+ "bin": {
1244
+ "browserslist": "cli.js"
1245
+ },
1246
+ "engines": {
1247
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1248
+ }
1249
+ },
1250
+ "node_modules/caniuse-lite": {
1251
+ "version": "1.0.30001788",
1252
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
1253
+ "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==",
1254
+ "dev": true,
1255
+ "funding": [
1256
+ {
1257
+ "type": "opencollective",
1258
+ "url": "https://opencollective.com/browserslist"
1259
+ },
1260
+ {
1261
+ "type": "tidelift",
1262
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1263
+ },
1264
+ {
1265
+ "type": "github",
1266
+ "url": "https://github.com/sponsors/ai"
1267
+ }
1268
+ ],
1269
+ "license": "CC-BY-4.0"
1270
+ },
1271
+ "node_modules/convert-source-map": {
1272
+ "version": "2.0.0",
1273
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1274
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1275
+ "dev": true,
1276
+ "license": "MIT"
1277
+ },
1278
+ "node_modules/csstype": {
1279
+ "version": "3.2.3",
1280
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1281
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1282
+ "dev": true,
1283
+ "license": "MIT"
1284
+ },
1285
+ "node_modules/debug": {
1286
+ "version": "4.4.3",
1287
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1288
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1289
+ "dev": true,
1290
+ "license": "MIT",
1291
+ "dependencies": {
1292
+ "ms": "^2.1.3"
1293
+ },
1294
+ "engines": {
1295
+ "node": ">=6.0"
1296
+ },
1297
+ "peerDependenciesMeta": {
1298
+ "supports-color": {
1299
+ "optional": true
1300
+ }
1301
+ }
1302
+ },
1303
+ "node_modules/electron-to-chromium": {
1304
+ "version": "1.5.340",
1305
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz",
1306
+ "integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==",
1307
+ "dev": true,
1308
+ "license": "ISC"
1309
+ },
1310
+ "node_modules/esbuild": {
1311
+ "version": "0.21.5",
1312
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
1313
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
1314
+ "dev": true,
1315
+ "hasInstallScript": true,
1316
+ "license": "MIT",
1317
+ "bin": {
1318
+ "esbuild": "bin/esbuild"
1319
+ },
1320
+ "engines": {
1321
+ "node": ">=12"
1322
+ },
1323
+ "optionalDependencies": {
1324
+ "@esbuild/aix-ppc64": "0.21.5",
1325
+ "@esbuild/android-arm": "0.21.5",
1326
+ "@esbuild/android-arm64": "0.21.5",
1327
+ "@esbuild/android-x64": "0.21.5",
1328
+ "@esbuild/darwin-arm64": "0.21.5",
1329
+ "@esbuild/darwin-x64": "0.21.5",
1330
+ "@esbuild/freebsd-arm64": "0.21.5",
1331
+ "@esbuild/freebsd-x64": "0.21.5",
1332
+ "@esbuild/linux-arm": "0.21.5",
1333
+ "@esbuild/linux-arm64": "0.21.5",
1334
+ "@esbuild/linux-ia32": "0.21.5",
1335
+ "@esbuild/linux-loong64": "0.21.5",
1336
+ "@esbuild/linux-mips64el": "0.21.5",
1337
+ "@esbuild/linux-ppc64": "0.21.5",
1338
+ "@esbuild/linux-riscv64": "0.21.5",
1339
+ "@esbuild/linux-s390x": "0.21.5",
1340
+ "@esbuild/linux-x64": "0.21.5",
1341
+ "@esbuild/netbsd-x64": "0.21.5",
1342
+ "@esbuild/openbsd-x64": "0.21.5",
1343
+ "@esbuild/sunos-x64": "0.21.5",
1344
+ "@esbuild/win32-arm64": "0.21.5",
1345
+ "@esbuild/win32-ia32": "0.21.5",
1346
+ "@esbuild/win32-x64": "0.21.5"
1347
+ }
1348
+ },
1349
+ "node_modules/escalade": {
1350
+ "version": "3.2.0",
1351
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1352
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1353
+ "dev": true,
1354
+ "license": "MIT",
1355
+ "engines": {
1356
+ "node": ">=6"
1357
+ }
1358
+ },
1359
+ "node_modules/fsevents": {
1360
+ "version": "2.3.3",
1361
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1362
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1363
+ "dev": true,
1364
+ "hasInstallScript": true,
1365
+ "license": "MIT",
1366
+ "optional": true,
1367
+ "os": [
1368
+ "darwin"
1369
+ ],
1370
+ "engines": {
1371
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1372
+ }
1373
+ },
1374
+ "node_modules/gensync": {
1375
+ "version": "1.0.0-beta.2",
1376
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1377
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1378
+ "dev": true,
1379
+ "license": "MIT",
1380
+ "engines": {
1381
+ "node": ">=6.9.0"
1382
+ }
1383
+ },
1384
+ "node_modules/js-tokens": {
1385
+ "version": "4.0.0",
1386
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1387
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1388
+ "license": "MIT"
1389
+ },
1390
+ "node_modules/jsesc": {
1391
+ "version": "3.1.0",
1392
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1393
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1394
+ "dev": true,
1395
+ "license": "MIT",
1396
+ "bin": {
1397
+ "jsesc": "bin/jsesc"
1398
+ },
1399
+ "engines": {
1400
+ "node": ">=6"
1401
+ }
1402
+ },
1403
+ "node_modules/json5": {
1404
+ "version": "2.2.3",
1405
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1406
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1407
+ "dev": true,
1408
+ "license": "MIT",
1409
+ "bin": {
1410
+ "json5": "lib/cli.js"
1411
+ },
1412
+ "engines": {
1413
+ "node": ">=6"
1414
+ }
1415
+ },
1416
+ "node_modules/loose-envify": {
1417
+ "version": "1.4.0",
1418
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
1419
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
1420
+ "license": "MIT",
1421
+ "dependencies": {
1422
+ "js-tokens": "^3.0.0 || ^4.0.0"
1423
+ },
1424
+ "bin": {
1425
+ "loose-envify": "cli.js"
1426
+ }
1427
+ },
1428
+ "node_modules/lru-cache": {
1429
+ "version": "5.1.1",
1430
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1431
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1432
+ "dev": true,
1433
+ "license": "ISC",
1434
+ "dependencies": {
1435
+ "yallist": "^3.0.2"
1436
+ }
1437
+ },
1438
+ "node_modules/ms": {
1439
+ "version": "2.1.3",
1440
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1441
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1442
+ "dev": true,
1443
+ "license": "MIT"
1444
+ },
1445
+ "node_modules/nanoid": {
1446
+ "version": "3.3.11",
1447
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1448
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1449
+ "dev": true,
1450
+ "funding": [
1451
+ {
1452
+ "type": "github",
1453
+ "url": "https://github.com/sponsors/ai"
1454
+ }
1455
+ ],
1456
+ "license": "MIT",
1457
+ "bin": {
1458
+ "nanoid": "bin/nanoid.cjs"
1459
+ },
1460
+ "engines": {
1461
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1462
+ }
1463
+ },
1464
+ "node_modules/node-releases": {
1465
+ "version": "2.0.37",
1466
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
1467
+ "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
1468
+ "dev": true,
1469
+ "license": "MIT"
1470
+ },
1471
+ "node_modules/picocolors": {
1472
+ "version": "1.1.1",
1473
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1474
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1475
+ "dev": true,
1476
+ "license": "ISC"
1477
+ },
1478
+ "node_modules/postcss": {
1479
+ "version": "8.5.10",
1480
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
1481
+ "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
1482
+ "dev": true,
1483
+ "funding": [
1484
+ {
1485
+ "type": "opencollective",
1486
+ "url": "https://opencollective.com/postcss/"
1487
+ },
1488
+ {
1489
+ "type": "tidelift",
1490
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1491
+ },
1492
+ {
1493
+ "type": "github",
1494
+ "url": "https://github.com/sponsors/ai"
1495
+ }
1496
+ ],
1497
+ "license": "MIT",
1498
+ "dependencies": {
1499
+ "nanoid": "^3.3.11",
1500
+ "picocolors": "^1.1.1",
1501
+ "source-map-js": "^1.2.1"
1502
+ },
1503
+ "engines": {
1504
+ "node": "^10 || ^12 || >=14"
1505
+ }
1506
+ },
1507
+ "node_modules/react": {
1508
+ "version": "18.3.1",
1509
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1510
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1511
+ "license": "MIT",
1512
+ "dependencies": {
1513
+ "loose-envify": "^1.1.0"
1514
+ },
1515
+ "engines": {
1516
+ "node": ">=0.10.0"
1517
+ }
1518
+ },
1519
+ "node_modules/react-dom": {
1520
+ "version": "18.3.1",
1521
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
1522
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1523
+ "license": "MIT",
1524
+ "dependencies": {
1525
+ "loose-envify": "^1.1.0",
1526
+ "scheduler": "^0.23.2"
1527
+ },
1528
+ "peerDependencies": {
1529
+ "react": "^18.3.1"
1530
+ }
1531
+ },
1532
+ "node_modules/react-refresh": {
1533
+ "version": "0.17.0",
1534
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
1535
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
1536
+ "dev": true,
1537
+ "license": "MIT",
1538
+ "engines": {
1539
+ "node": ">=0.10.0"
1540
+ }
1541
+ },
1542
+ "node_modules/rollup": {
1543
+ "version": "4.60.1",
1544
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
1545
+ "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
1546
+ "dev": true,
1547
+ "license": "MIT",
1548
+ "dependencies": {
1549
+ "@types/estree": "1.0.8"
1550
+ },
1551
+ "bin": {
1552
+ "rollup": "dist/bin/rollup"
1553
+ },
1554
+ "engines": {
1555
+ "node": ">=18.0.0",
1556
+ "npm": ">=8.0.0"
1557
+ },
1558
+ "optionalDependencies": {
1559
+ "@rollup/rollup-android-arm-eabi": "4.60.1",
1560
+ "@rollup/rollup-android-arm64": "4.60.1",
1561
+ "@rollup/rollup-darwin-arm64": "4.60.1",
1562
+ "@rollup/rollup-darwin-x64": "4.60.1",
1563
+ "@rollup/rollup-freebsd-arm64": "4.60.1",
1564
+ "@rollup/rollup-freebsd-x64": "4.60.1",
1565
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
1566
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
1567
+ "@rollup/rollup-linux-arm64-gnu": "4.60.1",
1568
+ "@rollup/rollup-linux-arm64-musl": "4.60.1",
1569
+ "@rollup/rollup-linux-loong64-gnu": "4.60.1",
1570
+ "@rollup/rollup-linux-loong64-musl": "4.60.1",
1571
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
1572
+ "@rollup/rollup-linux-ppc64-musl": "4.60.1",
1573
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
1574
+ "@rollup/rollup-linux-riscv64-musl": "4.60.1",
1575
+ "@rollup/rollup-linux-s390x-gnu": "4.60.1",
1576
+ "@rollup/rollup-linux-x64-gnu": "4.60.1",
1577
+ "@rollup/rollup-linux-x64-musl": "4.60.1",
1578
+ "@rollup/rollup-openbsd-x64": "4.60.1",
1579
+ "@rollup/rollup-openharmony-arm64": "4.60.1",
1580
+ "@rollup/rollup-win32-arm64-msvc": "4.60.1",
1581
+ "@rollup/rollup-win32-ia32-msvc": "4.60.1",
1582
+ "@rollup/rollup-win32-x64-gnu": "4.60.1",
1583
+ "@rollup/rollup-win32-x64-msvc": "4.60.1",
1584
+ "fsevents": "~2.3.2"
1585
+ }
1586
+ },
1587
+ "node_modules/scheduler": {
1588
+ "version": "0.23.2",
1589
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
1590
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
1591
+ "license": "MIT",
1592
+ "dependencies": {
1593
+ "loose-envify": "^1.1.0"
1594
+ }
1595
+ },
1596
+ "node_modules/semver": {
1597
+ "version": "6.3.1",
1598
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
1599
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
1600
+ "dev": true,
1601
+ "license": "ISC",
1602
+ "bin": {
1603
+ "semver": "bin/semver.js"
1604
+ }
1605
+ },
1606
+ "node_modules/source-map-js": {
1607
+ "version": "1.2.1",
1608
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1609
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1610
+ "dev": true,
1611
+ "license": "BSD-3-Clause",
1612
+ "engines": {
1613
+ "node": ">=0.10.0"
1614
+ }
1615
+ },
1616
+ "node_modules/typescript": {
1617
+ "version": "5.9.3",
1618
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
1619
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
1620
+ "dev": true,
1621
+ "license": "Apache-2.0",
1622
+ "bin": {
1623
+ "tsc": "bin/tsc",
1624
+ "tsserver": "bin/tsserver"
1625
+ },
1626
+ "engines": {
1627
+ "node": ">=14.17"
1628
+ }
1629
+ },
1630
+ "node_modules/update-browserslist-db": {
1631
+ "version": "1.2.3",
1632
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
1633
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
1634
+ "dev": true,
1635
+ "funding": [
1636
+ {
1637
+ "type": "opencollective",
1638
+ "url": "https://opencollective.com/browserslist"
1639
+ },
1640
+ {
1641
+ "type": "tidelift",
1642
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1643
+ },
1644
+ {
1645
+ "type": "github",
1646
+ "url": "https://github.com/sponsors/ai"
1647
+ }
1648
+ ],
1649
+ "license": "MIT",
1650
+ "dependencies": {
1651
+ "escalade": "^3.2.0",
1652
+ "picocolors": "^1.1.1"
1653
+ },
1654
+ "bin": {
1655
+ "update-browserslist-db": "cli.js"
1656
+ },
1657
+ "peerDependencies": {
1658
+ "browserslist": ">= 4.21.0"
1659
+ }
1660
+ },
1661
+ "node_modules/vite": {
1662
+ "version": "5.4.21",
1663
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
1664
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
1665
+ "dev": true,
1666
+ "license": "MIT",
1667
+ "dependencies": {
1668
+ "esbuild": "^0.21.3",
1669
+ "postcss": "^8.4.43",
1670
+ "rollup": "^4.20.0"
1671
+ },
1672
+ "bin": {
1673
+ "vite": "bin/vite.js"
1674
+ },
1675
+ "engines": {
1676
+ "node": "^18.0.0 || >=20.0.0"
1677
+ },
1678
+ "funding": {
1679
+ "url": "https://github.com/vitejs/vite?sponsor=1"
1680
+ },
1681
+ "optionalDependencies": {
1682
+ "fsevents": "~2.3.3"
1683
+ },
1684
+ "peerDependencies": {
1685
+ "@types/node": "^18.0.0 || >=20.0.0",
1686
+ "less": "*",
1687
+ "lightningcss": "^1.21.0",
1688
+ "sass": "*",
1689
+ "sass-embedded": "*",
1690
+ "stylus": "*",
1691
+ "sugarss": "*",
1692
+ "terser": "^5.4.0"
1693
+ },
1694
+ "peerDependenciesMeta": {
1695
+ "@types/node": {
1696
+ "optional": true
1697
+ },
1698
+ "less": {
1699
+ "optional": true
1700
+ },
1701
+ "lightningcss": {
1702
+ "optional": true
1703
+ },
1704
+ "sass": {
1705
+ "optional": true
1706
+ },
1707
+ "sass-embedded": {
1708
+ "optional": true
1709
+ },
1710
+ "stylus": {
1711
+ "optional": true
1712
+ },
1713
+ "sugarss": {
1714
+ "optional": true
1715
+ },
1716
+ "terser": {
1717
+ "optional": true
1718
+ }
1719
+ }
1720
+ },
1721
+ "node_modules/yallist": {
1722
+ "version": "3.1.1",
1723
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
1724
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
1725
+ "dev": true,
1726
+ "license": "ISC"
1727
+ }
1728
+ }
1729
+ }
frontend/src/App.css CHANGED
@@ -1,91 +1,68 @@
1
- * {
2
- margin: 0;
3
- padding: 0;
4
- box-sizing: border-box;
 
 
 
 
 
 
5
  }
6
 
 
 
7
  body {
8
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
9
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
10
- sans-serif;
11
- -webkit-font-smoothing: antialiased;
12
- -moz-osx-font-smoothing: grayscale;
13
- background: #1a1a2e;
14
  min-height: 100vh;
15
  }
16
 
17
- .app-container {
18
- display: flex;
19
- height: 100vh;
20
- background: #1a1a2e;
21
- }
22
 
23
  .sidebar {
24
- width: 300px;
25
- background: #16213e;
26
- border-right: 1px solid #0f3460;
27
  overflow-y: auto;
28
- padding: 16px;
29
  }
30
 
31
- .main-content {
32
- flex: 1;
33
- display: flex;
34
- flex-direction: column;
35
- }
36
 
37
- .chat-history {
38
- height: 100%;
39
- }
40
 
41
  .new-chat-btn {
42
  width: 100%;
43
  padding: 12px;
44
- background: #0f3460;
45
- color: white;
46
- border: none;
47
- border-radius: 8px;
48
  cursor: pointer;
49
- font-size: 14px;
50
- margin-bottom: 16px;
51
- transition: background 0.3s;
52
- }
53
-
54
- .new-chat-btn:hover {
55
- background: #1a5f7a;
56
  }
57
 
58
- .chat-list {
59
- display: flex;
60
- flex-direction: column;
61
- gap: 8px;
62
- }
63
 
64
  .chat-item {
65
- padding: 12px;
66
- background: #0f3460;
67
- color: white;
68
- border: none;
69
- border-radius: 6px;
70
  cursor: pointer;
71
- font-size: 13px;
72
- text-align: left;
73
- transition: background 0.2s;
74
  }
75
 
76
- .chat-item:hover {
77
- background: #1a5f7a;
78
- }
79
 
80
- .chat-item.active {
81
- background: #e94560;
82
- }
83
-
84
- .chat-window {
85
- display: flex;
86
- flex-direction: column;
87
- height: 100%;
88
- }
89
 
90
  .messages {
91
  flex: 1;
@@ -93,7 +70,7 @@ body {
93
  padding: 24px;
94
  display: flex;
95
  flex-direction: column;
96
- gap: 16px;
97
  }
98
 
99
  .initial-menu {
@@ -103,153 +80,120 @@ body {
103
  justify-content: center;
104
  margin: auto;
105
  text-align: center;
 
106
  }
107
 
108
- .initial-menu h1 {
109
- color: white;
110
- font-size: 2.5em;
111
- margin-bottom: 12px;
112
- }
113
-
114
- .initial-menu p {
115
- color: #a0a0a0;
116
- font-size: 16px;
117
- margin-bottom: 32px;
118
  }
 
 
 
119
 
120
  .example-buttons {
121
  display: grid;
122
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
123
  gap: 12px;
124
- max-width: 600px;
125
  }
126
 
127
  .example-btn {
128
  padding: 12px 16px;
129
- background: #0f3460;
130
- color: white;
131
- border: 1px solid #e94560;
132
- border-radius: 6px;
133
  cursor: pointer;
134
- font-size: 13px;
135
- transition: all 0.3s;
136
- }
137
-
138
- .example-btn:hover {
139
- background: #e94560;
140
- transform: translateY(-2px);
141
- }
142
-
143
- .message {
144
- display: flex;
145
- gap: 12px;
146
- align-items: flex-start;
147
  }
148
 
149
- .message.user {
150
- justify-content: flex-end;
151
- }
152
 
153
- .message.assistant {
154
- justify-content: flex-start;
155
- }
156
 
157
  .avatar {
158
- font-size: 20px;
 
 
 
 
 
 
 
159
  flex-shrink: 0;
160
  }
161
 
162
  .message .content {
163
- max-width: 600px;
164
- padding: 12px 16px;
165
- border-radius: 8px;
166
- line-height: 1.5;
167
- }
168
-
169
- .message.user .content {
170
- background: #0f3460;
171
- color: white;
172
  }
173
 
174
- .message.assistant .content {
175
- background: #e94560;
176
- color: white;
177
- }
178
 
179
  .message.error .content {
180
- background: #ff6b6b;
181
- color: white;
182
  }
183
 
184
- .message.loading {
185
- color: #a0a0a0;
186
- font-style: italic;
187
- }
188
 
189
- .input-area {
190
- display: flex;
191
- gap: 12px;
192
- padding: 16px 24px;
193
- border-top: 1px solid #0f3460;
194
- }
195
 
196
  .input-area input {
197
  flex: 1;
198
- padding: 12px 16px;
199
- background: #0f3460;
200
- color: white;
201
- border: 1px solid #1a5f7a;
202
- border-radius: 6px;
203
- font-size: 14px;
204
- transition: border-color 0.2s;
205
- }
206
-
207
- .input-area input:focus {
208
- outline: none;
209
- border-color: #e94560;
210
  }
211
 
212
- .input-area input::placeholder {
213
- color: #666;
214
- }
215
 
216
  .input-area button {
217
- padding: 12px 24px;
218
- background: #e94560;
219
- color: white;
 
220
  border: none;
221
- border-radius: 6px;
222
  cursor: pointer;
223
- font-weight: 600;
224
- transition: background 0.2s;
225
  }
226
 
227
- .input-area button:hover {
228
- background: #d63550;
229
- }
230
-
231
- .input-area button:disabled {
232
- background: #666;
233
- cursor: not-allowed;
234
- }
235
 
236
  @media (max-width: 768px) {
237
- .app-container {
238
- flex-direction: column;
239
- }
240
 
241
  .sidebar {
242
  width: 100%;
243
- max-height: 200px;
244
  border-right: none;
245
- border-bottom: 1px solid #0f3460;
246
  }
247
 
248
- .example-buttons {
249
- grid-template-columns: 1fr;
250
- }
251
 
252
- .message .content {
253
- max-width: 100%;
254
- }
 
 
 
 
 
 
 
255
  }
 
1
+ :root {
2
+ --bg: #121317;
3
+ --panel: #202739;
4
+ --panel-2: #1f2636;
5
+ --panel-3: #242c3f;
6
+ --line: #343d52;
7
+ --text: #ffffff;
8
+ --muted: #98a0b3;
9
+ --brand: #35cfd2;
10
+ --brand-soft: rgba(53, 207, 210, 0.18);
11
  }
12
 
13
+ * { margin: 0; padding: 0; box-sizing: border-box; }
14
+
15
  body {
16
+ font-family: 'Segoe UI', 'Inter', sans-serif;
17
+ background: var(--bg);
18
+ color: var(--text);
 
 
 
19
  min-height: 100vh;
20
  }
21
 
22
+ .app-container { display: flex; height: 100vh; background: var(--bg); }
 
 
 
 
23
 
24
  .sidebar {
25
+ width: 290px;
26
+ background: linear-gradient(180deg, #242b3d 0%, #20283a 100%);
27
+ border-right: 1px solid var(--line);
28
  overflow-y: auto;
29
+ padding: 26px 12px;
30
  }
31
 
32
+ .main-content { flex: 1; display: flex; flex-direction: column; }
33
+ .chat-history { height: 100%; }
 
 
 
34
 
35
+ .sidebar-title { color: var(--text); font-size: 36px; margin: 46px 0 28px; }
36
+ .sidebar-subtitle { color: var(--muted); margin-bottom: 10px; font-size: 18px; }
 
37
 
38
  .new-chat-btn {
39
  width: 100%;
40
  padding: 12px;
41
+ background: transparent;
42
+ color: var(--text);
43
+ border: 1px solid var(--line);
44
+ border-radius: 10px;
45
  cursor: pointer;
46
+ font-size: 22px;
47
+ margin-bottom: 20px;
 
 
 
 
 
48
  }
49
 
50
+ .chat-list { display: flex; flex-direction: column; gap: 8px; }
 
 
 
 
51
 
52
  .chat-item {
53
+ padding: 11px 12px;
54
+ background: transparent;
55
+ color: var(--text);
56
+ border: 1px solid transparent;
57
+ border-radius: 8px;
58
  cursor: pointer;
59
+ font-size: 16px;
 
 
60
  }
61
 
62
+ .chat-item:hover,
63
+ .chat-item.active { border-color: var(--line); background: rgba(255, 255, 255, 0.03); }
 
64
 
65
+ .chat-window { display: flex; flex-direction: column; height: 100%; }
 
 
 
 
 
 
 
 
66
 
67
  .messages {
68
  flex: 1;
 
70
  padding: 24px;
71
  display: flex;
72
  flex-direction: column;
73
+ gap: 20px;
74
  }
75
 
76
  .initial-menu {
 
80
  justify-content: center;
81
  margin: auto;
82
  text-align: center;
83
+ gap: 8px;
84
  }
85
 
86
+ .brand-logo-text {
87
+ font-size: 34px;
88
+ font-weight: 800;
89
+ letter-spacing: 0.5px;
90
+ color: #6ed6ff;
91
+ text-shadow: 0 0 14px rgba(53, 207, 210, 0.3);
 
 
 
 
92
  }
93
+ .initial-menu h1 { color: var(--brand); font-size: 44px; font-weight: 700; }
94
+ .brand-author { font-size: 30px; margin-bottom: 10px; }
95
+ .initial-menu p { color: var(--text); font-size: 16px; margin-bottom: 24px; }
96
 
97
  .example-buttons {
98
  display: grid;
99
+ grid-template-columns: repeat(4, minmax(150px, 1fr));
100
  gap: 12px;
101
+ max-width: 850px;
102
  }
103
 
104
  .example-btn {
105
  padding: 12px 16px;
106
+ background: transparent;
107
+ color: var(--text);
108
+ border: 1px solid var(--line);
109
+ border-radius: 10px;
110
  cursor: pointer;
111
+ font-size: 18px;
 
 
 
 
 
 
 
 
 
 
 
 
112
  }
113
 
114
+ .example-btn:hover { border-color: var(--brand); background: var(--brand-soft); }
 
 
115
 
116
+ .message { display: flex; gap: 10px; align-items: flex-start; }
 
 
117
 
118
  .avatar {
119
+ width: 30px;
120
+ height: 30px;
121
+ display: inline-flex;
122
+ align-items: center;
123
+ justify-content: center;
124
+ background: #0f1727;
125
+ border: 1px solid #2d3a53;
126
+ border-radius: 8px;
127
  flex-shrink: 0;
128
  }
129
 
130
  .message .content {
131
+ width: 100%;
132
+ max-width: 900px;
133
+ padding: 14px 18px;
134
+ border-radius: 10px;
135
+ line-height: 1.6;
136
+ font-size: 18px;
 
 
 
137
  }
138
 
139
+ .message.user .content { background: var(--panel-2); color: var(--text); }
140
+ .message.assistant .content { background: transparent; color: var(--text); padding-left: 0; }
 
 
141
 
142
  .message.error .content {
143
+ background: rgba(255, 82, 82, 0.15);
144
+ border: 1px solid rgba(255, 82, 82, 0.35);
145
  }
146
 
147
+ .message.loading { color: var(--muted); font-style: italic; margin-left: 40px; }
 
 
 
148
 
149
+ .input-area { display: flex; gap: 10px; padding: 18px 24px 26px; }
 
 
 
 
 
150
 
151
  .input-area input {
152
  flex: 1;
153
+ padding: 15px 18px;
154
+ background: var(--panel-3);
155
+ color: var(--text);
156
+ border: 1px solid transparent;
157
+ border-radius: 10px;
158
+ font-size: 30px;
 
 
 
 
 
 
159
  }
160
 
161
+ .input-area input:focus { outline: none; border-color: var(--brand); }
162
+ .input-area input::placeholder { color: #9aa3b8; }
 
163
 
164
  .input-area button {
165
+ width: 46px;
166
+ border-radius: 10px;
167
+ background: #414b60;
168
+ color: #bfc6d6;
169
  border: none;
 
170
  cursor: pointer;
171
+ font-size: 24px;
 
172
  }
173
 
174
+ .input-area button:hover { background: #4e5a73; }
175
+ .input-area button:disabled { opacity: 0.6; cursor: not-allowed; }
 
 
 
 
 
 
176
 
177
  @media (max-width: 768px) {
178
+ .app-container { flex-direction: column; }
 
 
179
 
180
  .sidebar {
181
  width: 100%;
182
+ max-height: 210px;
183
  border-right: none;
184
+ border-bottom: 1px solid var(--line);
185
  }
186
 
187
+ .sidebar-title { margin-top: 0; font-size: 22px; }
 
 
188
 
189
+ .new-chat-btn,
190
+ .chat-item,
191
+ .example-btn,
192
+ .message .content,
193
+ .input-area input,
194
+ .initial-menu h1,
195
+ .initial-menu p,
196
+ .brand-author { font-size: 16px; }
197
+
198
+ .example-buttons { grid-template-columns: 1fr; }
199
  }
frontend/src/App.tsx CHANGED
@@ -1,31 +1,87 @@
1
- import React, { useState } from 'react';
2
  import './App.css';
3
  import ChatWindow from './components/ChatWindow';
4
  import ChatHistory from './components/ChatHistory';
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  function App() {
7
- const [currentChatId, setCurrentChatId] = useState(Date.now().toString());
8
- const [chats, setChats] = useState<Record<string, any[]>>({});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  return (
11
  <div className="app-container">
12
  <div className="sidebar">
13
- <ChatHistory
14
  chats={chats}
15
  currentChatId={currentChatId}
16
  onSelectChat={setCurrentChatId}
17
- onNewChat={() => setCurrentChatId(Date.now().toString())}
18
  />
19
  </div>
20
  <div className="main-content">
21
- <ChatWindow
22
  chatId={currentChatId}
23
- onUpdateChat={(messages: any[]) => {
24
- setChats(prev => ({
25
- ...prev,
26
- [currentChatId]: messages
27
- }));
28
- }}
29
  />
30
  </div>
31
  </div>
 
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
  import './App.css';
3
  import ChatWindow from './components/ChatWindow';
4
  import ChatHistory from './components/ChatHistory';
5
 
6
+ export interface ChatSummary {
7
+ chat_id: string;
8
+ title: string;
9
+ updated_at: number;
10
+ }
11
+
12
+ const USER_STORAGE_KEY = 'chatbot_uid';
13
+
14
+ function getOrCreateUid() {
15
+ const existing = localStorage.getItem(USER_STORAGE_KEY);
16
+ if (existing) return existing;
17
+ const generated = crypto.randomUUID();
18
+ localStorage.setItem(USER_STORAGE_KEY, generated);
19
+ return generated;
20
+ }
21
+
22
  function App() {
23
+ const uid = useMemo(() => getOrCreateUid(), []);
24
+ const [currentChatId, setCurrentChatId] = useState<string | null>(null);
25
+ const [chats, setChats] = useState<ChatSummary[]>([]);
26
+
27
+ const fetchWithUid = (path: string, init?: RequestInit) => {
28
+ const separator = path.includes('?') ? '&' : '?';
29
+ return fetch(`${path}${separator}uid=${encodeURIComponent(uid)}`, init);
30
+ };
31
+
32
+ const fetchChats = async () => {
33
+ const response = await fetchWithUid('/api/chats');
34
+ if (!response.ok) {
35
+ throw new Error('No se pudieron cargar los chats');
36
+ }
37
+ const data: ChatSummary[] = await response.json();
38
+ setChats(data);
39
+ if (!currentChatId && data.length > 0) {
40
+ setCurrentChatId(data[0].chat_id);
41
+ }
42
+ };
43
+
44
+ useEffect(() => {
45
+ fetchChats().catch(console.error);
46
+ }, []);
47
+
48
+ const createChat = async () => {
49
+ const response = await fetchWithUid('/api/chats', {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/json' },
52
+ body: JSON.stringify({}),
53
+ });
54
+ if (!response.ok) {
55
+ throw new Error('No se pudo crear el chat');
56
+ }
57
+
58
+ const created: ChatSummary = await response.json();
59
+ await fetchChats();
60
+ setCurrentChatId(created.chat_id);
61
+ return created.chat_id;
62
+ };
63
+
64
+ const ensureChat = async () => {
65
+ if (currentChatId) return currentChatId;
66
+ return createChat();
67
+ };
68
 
69
  return (
70
  <div className="app-container">
71
  <div className="sidebar">
72
+ <ChatHistory
73
  chats={chats}
74
  currentChatId={currentChatId}
75
  onSelectChat={setCurrentChatId}
76
+ onNewChat={createChat}
77
  />
78
  </div>
79
  <div className="main-content">
80
+ <ChatWindow
81
  chatId={currentChatId}
82
+ uid={uid}
83
+ onRefreshChats={fetchChats}
84
+ onEnsureChat={ensureChat}
 
 
 
85
  />
86
  </div>
87
  </div>
frontend/src/components/ChatHistory.tsx CHANGED
@@ -1,23 +1,30 @@
1
  import React from 'react';
 
2
 
3
- function ChatHistory({ chats, currentChatId, onSelectChat, onNewChat }: any) {
 
 
 
 
 
 
 
4
  return (
5
  <div className="chat-history">
 
6
  <button className="new-chat-btn" onClick={onNewChat}>
7
  + Nuevo chat
8
  </button>
 
9
  <div className="chat-list">
10
- {Object.entries(chats).map(([chatId, messages]: any) => (
11
  <div
12
- key={chatId}
13
- className={`chat-item ${chatId === currentChatId ? 'active' : ''}`}
14
- onClick={() => onSelectChat(chatId)}
15
  >
16
- {chatId === currentChatId ? '● ' : ''}
17
- {messages && messages.length > 0
18
- ? messages[0].content.substring(0, 30) + '...'
19
- : `Chat ${chatId}`
20
- }
21
  </div>
22
  ))}
23
  </div>
 
1
  import React from 'react';
2
+ import type { ChatSummary } from '../App';
3
 
4
+ interface Props {
5
+ chats: ChatSummary[];
6
+ currentChatId: string | null;
7
+ onSelectChat: (chatId: string) => void;
8
+ onNewChat: () => void;
9
+ }
10
+
11
+ function ChatHistory({ chats, currentChatId, onSelectChat, onNewChat }: Props) {
12
  return (
13
  <div className="chat-history">
14
+ <h2 className="sidebar-title">Chats Anteriores</h2>
15
  <button className="new-chat-btn" onClick={onNewChat}>
16
  + Nuevo chat
17
  </button>
18
+ <p className="sidebar-subtitle">Sesiones</p>
19
  <div className="chat-list">
20
+ {chats.map((chat) => (
21
  <div
22
+ key={chat.chat_id}
23
+ className={`chat-item ${chat.chat_id === currentChatId ? 'active' : ''}`}
24
+ onClick={() => onSelectChat(chat.chat_id)}
25
  >
26
+ {chat.chat_id === currentChatId ? '● ' : ''}
27
+ {chat.title}
 
 
 
28
  </div>
29
  ))}
30
  </div>
frontend/src/components/ChatWindow.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from 'react';
2
 
3
  interface Message {
4
  id: string;
@@ -7,80 +7,169 @@ interface Message {
7
  avatar: string;
8
  }
9
 
10
- function ChatWindow({ chatId, onUpdateChat }: any) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  const [messages, setMessages] = useState<Message[]>([]);
12
  const [input, setInput] = useState('');
13
  const [loading, setLoading] = useState(false);
14
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  useEffect(() => {
16
- setMessages([]);
 
 
 
 
 
 
 
 
17
  }, [chatId]);
18
 
19
- const handleSendMessage = async () => {
20
- if (!input.trim()) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  const userMessage: Message = {
23
- id: Date.now().toString(),
24
  role: 'user',
25
- content: input,
26
  avatar: '👤',
27
  };
28
 
29
- setMessages(prev => [...prev, userMessage]);
30
- setInput('');
31
  setLoading(true);
32
 
33
  try {
34
- const response = await fetch('/api/chat/send', {
35
  method: 'POST',
36
  headers: { 'Content-Type': 'application/json' },
37
- body: JSON.stringify({
38
- chat_id: chatId,
39
- content: input,
40
- }),
41
  });
42
 
43
- if (!response.ok) throw new Error('Error sending message');
 
 
 
44
 
45
  const data = await response.json();
 
46
  const assistantMessage: Message = {
47
- id: (Date.now() + 1).toString(),
48
  role: 'assistant',
49
- content: data.response,
50
- avatar: '🤖',
51
  };
52
 
53
- setMessages(prev => [...prev, assistantMessage]);
54
- onUpdateChat([...messages, userMessage, assistantMessage]);
 
55
  } catch (error) {
56
- console.error('Error:', error);
57
- setMessages(prev => [...prev, {
58
- id: Date.now().toString(),
59
- role: 'error',
60
- content: 'Error sending message',
61
- avatar: '⚠️',
62
- }]);
 
 
 
63
  } finally {
64
  setLoading(false);
65
  }
66
  };
67
 
 
 
 
 
 
 
 
68
  return (
69
  <div className="chat-window">
70
  <div className="messages">
71
  {messages.length === 0 && (
72
  <div className="initial-menu">
 
73
  <h1>Email Story Creator</h1>
74
- <p>✉️ Experto en emails narrativos que conectan historias con ventas</p>
 
75
  <div className="example-buttons">
76
- <button className="example-btn">Definir audiencia 🎯</button>
77
- <button className="example-btn">Propuesta de valor 💎</button>
78
- <button className="example-btn">CTA que convierte 🚀</button>
79
- <button className="example-btn">Asunto + gancho ✉️</button>
 
 
 
 
 
 
 
 
80
  </div>
81
  </div>
82
  )}
83
- {messages.map(msg => (
84
  <div key={msg.id} className={`message ${msg.role}`}>
85
  <span className="avatar">{msg.avatar}</span>
86
  <div className="content">{msg.content}</div>
@@ -94,12 +183,12 @@ function ChatWindow({ chatId, onUpdateChat }: any) {
94
  type="text"
95
  value={input}
96
  onChange={(e) => setInput(e.target.value)}
97
- onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
98
  placeholder="Escribe aquí tus instrucciones"
99
  disabled={loading}
100
  />
101
- <button onClick={handleSendMessage} disabled={loading}>
102
- Enviar
103
  </button>
104
  </div>
105
  </div>
 
1
+ import React, { useEffect, useState } from 'react';
2
 
3
  interface Message {
4
  id: string;
 
7
  avatar: string;
8
  }
9
 
10
+ interface ApiMessage {
11
+ role: 'user' | 'assistant';
12
+ content: string;
13
+ }
14
+
15
+ interface Props {
16
+ chatId: string | null;
17
+ uid: string;
18
+ onRefreshChats: () => Promise<void>;
19
+ onEnsureChat: () => Promise<string>;
20
+ }
21
+
22
+ const EXAMPLE_PROMPTS = [
23
+ 'Ayúdame a definir una audiencia concreta para este correo: dolor principal, deseo y nivel de conciencia.',
24
+ 'Convierte mi producto en una promesa clara de transformación sin listar características aburridas.',
25
+ 'Dame 3 opciones de CTA claras para este email, con baja fricción y orientadas a una sola acción.',
26
+ 'Propón 5 asuntos y 3 ganchos de apertura para aumentar aperturas y clics de este correo.',
27
+ ];
28
+
29
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
30
+
31
+ function toUiMessage(msg: ApiMessage, idx: number): Message {
32
+ return {
33
+ id: `${msg.role}-${idx}-${Date.now()}`,
34
+ role: msg.role,
35
+ content: msg.content,
36
+ avatar: msg.role === 'user' ? '👤' : '🤖',
37
+ };
38
+ }
39
+
40
+ function ChatWindow({ chatId, uid, onRefreshChats, onEnsureChat }: Props) {
41
  const [messages, setMessages] = useState<Message[]>([]);
42
  const [input, setInput] = useState('');
43
  const [loading, setLoading] = useState(false);
44
 
45
+ const withUid = (path: string) => `${path}${path.includes('?') ? '&' : '?'}uid=${encodeURIComponent(uid)}`;
46
+
47
+ const loadMessages = async (targetChatId: string) => {
48
+ const response = await fetch(withUid(`/api/chats/${targetChatId}/messages`));
49
+ if (!response.ok) {
50
+ throw new Error('No se pudo cargar el historial');
51
+ }
52
+
53
+ const data: ApiMessage[] = await response.json();
54
+ setMessages(data.map((msg, idx) => toUiMessage(msg, idx)));
55
+ };
56
+
57
  useEffect(() => {
58
+ if (!chatId) {
59
+ setMessages([]);
60
+ return;
61
+ }
62
+
63
+ loadMessages(chatId).catch((error) => {
64
+ console.error(error);
65
+ setMessages([]);
66
+ });
67
  }, [chatId]);
68
 
69
+ const animateAssistantText = async (messageId: string, fullText: string) => {
70
+ let partial = '';
71
+ for (const char of fullText) {
72
+ partial += char;
73
+ setMessages((prev) =>
74
+ prev.map((msg) =>
75
+ msg.id === messageId
76
+ ? {
77
+ ...msg,
78
+ content: partial,
79
+ }
80
+ : msg,
81
+ ),
82
+ );
83
+ await sleep(8);
84
+ }
85
+ };
86
+
87
+ const sendToCurrentChat = async (content: string, isExample = false) => {
88
+ const activeChatId = await onEnsureChat();
89
 
90
  const userMessage: Message = {
91
+ id: `${Date.now()}-user`,
92
  role: 'user',
93
+ content,
94
  avatar: '👤',
95
  };
96
 
97
+ setMessages((prev) => [...prev, userMessage]);
 
98
  setLoading(true);
99
 
100
  try {
101
+ const response = await fetch(withUid(`/api/chats/${activeChatId}/messages`), {
102
  method: 'POST',
103
  headers: { 'Content-Type': 'application/json' },
104
+ body: JSON.stringify({ content, is_example: isExample }),
 
 
 
105
  });
106
 
107
+ if (!response.ok) {
108
+ const payload = await response.json().catch(() => ({}));
109
+ throw new Error(payload.detail || 'Error enviando mensaje');
110
+ }
111
 
112
  const data = await response.json();
113
+ const assistantId = `${Date.now()}-assistant`;
114
  const assistantMessage: Message = {
115
+ id: assistantId,
116
  role: 'assistant',
117
+ content: '',
118
+ avatar: '😀',
119
  };
120
 
121
+ setMessages((prev) => [...prev, assistantMessage]);
122
+ await animateAssistantText(assistantId, data.response ?? '');
123
+ await onRefreshChats();
124
  } catch (error) {
125
+ const msg = error instanceof Error ? error.message : 'Error enviando mensaje';
126
+ setMessages((prev) => [
127
+ ...prev,
128
+ {
129
+ id: `${Date.now()}-error`,
130
+ role: 'error',
131
+ content: msg,
132
+ avatar: '⚠️',
133
+ },
134
+ ]);
135
  } finally {
136
  setLoading(false);
137
  }
138
  };
139
 
140
+ const handleSendMessage = async () => {
141
+ if (!input.trim()) return;
142
+ const content = input;
143
+ setInput('');
144
+ await sendToCurrentChat(content, false);
145
+ };
146
+
147
  return (
148
  <div className="chat-window">
149
  <div className="messages">
150
  {messages.length === 0 && (
151
  <div className="initial-menu">
152
+ <div className="brand-logo-text">RoboCopy</div>
153
  <h1>Email Story Creator</h1>
154
+ <span className="brand-author">By Jesús Cabrera</span>
155
+ <p>✉️ Experto en emails narrativos que conectan historias con ventas de forma natural</p>
156
  <div className="example-buttons">
157
+ <button className="example-btn" onClick={() => sendToCurrentChat(EXAMPLE_PROMPTS[0], true)}>
158
+ Definir audiencia 🎯
159
+ </button>
160
+ <button className="example-btn" onClick={() => sendToCurrentChat(EXAMPLE_PROMPTS[1], true)}>
161
+ Propuesta de valor 💎
162
+ </button>
163
+ <button className="example-btn" onClick={() => sendToCurrentChat(EXAMPLE_PROMPTS[2], true)}>
164
+ CTA que convierte 🚀
165
+ </button>
166
+ <button className="example-btn" onClick={() => sendToCurrentChat(EXAMPLE_PROMPTS[3], true)}>
167
+ Asunto + gancho ✉️
168
+ </button>
169
  </div>
170
  </div>
171
  )}
172
+ {messages.map((msg) => (
173
  <div key={msg.id} className={`message ${msg.role}`}>
174
  <span className="avatar">{msg.avatar}</span>
175
  <div className="content">{msg.content}</div>
 
183
  type="text"
184
  value={input}
185
  onChange={(e) => setInput(e.target.value)}
186
+ onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()}
187
  placeholder="Escribe aquí tus instrucciones"
188
  disabled={loading}
189
  />
190
+ <button onClick={handleSendMessage} disabled={loading} aria-label="Enviar mensaje">
191
+
192
  </button>
193
  </div>
194
  </div>
frontend/tsconfig.json CHANGED
@@ -1 +1,18 @@
1
- {\n "compilerOptions": {\n "target": "es5",\n "module": "commonjs",\n "strict": true,\n "jsx": "react",\n "moduleResolution": "node",\n "esModuleInterop": true,\n "skipLibCheck": true,\n "forceConsistentCasingInFileNames": true\n },\n "include": [\n "src/**/*"\n ]\n}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "Bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true
15
+ },
16
+ "include": ["src"],
17
+ "references": [{ "path": "./tsconfig.node.json" }]
18
+ }
frontend/tsconfig.node.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "allowSyntheticDefaultImports": true
8
+ },
9
+ "include": ["vite.config.ts"]
10
+ }