Álvaro Valenzuela Valdes commited on
Commit
b8e6434
·
0 Parent(s):

Initial commit: AndesOps AI Platform Demo Ready

Browse files
Files changed (48) hide show
  1. .env.example +7 -0
  2. .gitignore +22 -0
  3. README.md +79 -0
  4. backend/Dockerfile +6 -0
  5. backend/app/__init__.py +0 -0
  6. backend/app/config.py +17 -0
  7. backend/app/database.py +20 -0
  8. backend/app/main.py +35 -0
  9. backend/app/models/tender.py +25 -0
  10. backend/app/routers/analysis.py +37 -0
  11. backend/app/routers/company.py +25 -0
  12. backend/app/routers/documents.py +27 -0
  13. backend/app/routers/health.py +30 -0
  14. backend/app/routers/tenders.py +57 -0
  15. backend/app/schemas/analysis.py +45 -0
  16. backend/app/schemas/company.py +12 -0
  17. backend/app/schemas/tender.py +25 -0
  18. backend/app/services/agents.py +92 -0
  19. backend/app/services/llm.py +131 -0
  20. backend/app/services/mercado_publico.py +243 -0
  21. backend/app/services/persistence.py +25 -0
  22. backend/app/services/report.py +40 -0
  23. backend/app/services/sync.py +68 -0
  24. backend/requirements.txt +8 -0
  25. docker-compose.yml +22 -0
  26. frontend/Dockerfile +6 -0
  27. frontend/app/layout.tsx +15 -0
  28. frontend/app/page.tsx +187 -0
  29. frontend/components/AgentAnalysis.tsx +213 -0
  30. frontend/components/AnalysisHistory.tsx +71 -0
  31. frontend/components/BrandLoader.tsx +69 -0
  32. frontend/components/CompanyProfile.tsx +110 -0
  33. frontend/components/Dashboard.tsx +298 -0
  34. frontend/components/ProposalDraft.tsx +17 -0
  35. frontend/components/Reports.tsx +57 -0
  36. frontend/components/Sidebar.tsx +59 -0
  37. frontend/components/StatCard.tsx +17 -0
  38. frontend/components/TenderSearch.tsx +414 -0
  39. frontend/globals.css +34 -0
  40. frontend/lib/api.ts +106 -0
  41. frontend/lib/types.ts +68 -0
  42. frontend/next-env.d.ts +5 -0
  43. frontend/next.config.js +6 -0
  44. frontend/package-lock.json +1662 -0
  45. frontend/package.json +25 -0
  46. frontend/postcss.config.js +6 -0
  47. frontend/tailwind.config.ts +18 -0
  48. frontend/tsconfig.json +39 -0
.env.example ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Backend configuration
2
+ MERCADO_PUBLICO_TICKET=your_mercado_publico_ticket_here
3
+ GEMINI_API_KEY=your_gemini_api_key_here
4
+ GEMINI_MODEL=gemini-1.5-flash
5
+
6
+ # Frontend configuration
7
+ NEXT_PUBLIC_API_BASE=http://localhost:8000
.gitignore ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ backend/.venv/
3
+ backend/__pycache__/
4
+ backend/**/*.pyc
5
+ backend/**/*.pyo
6
+ backend/.env
7
+ backend/test_*.py
8
+ backend/populate_db.py
9
+ backend/purge_mock.py
10
+
11
+ # Node / Next.js
12
+ frontend/node_modules/
13
+ frontend/.next/
14
+ frontend/.env*
15
+ frontend/npm-debug.log*
16
+
17
+ # General
18
+ .env
19
+ .DS_Store
20
+ *.db
21
+ *.sqlite
22
+ .vscode/
README.md ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AndesOps AI
2
+
3
+ AndesOps AI es un MVP hackathon que transforma datos de licitaciones públicas en inteligencia accionable para empresas chilenas. El producto ayuda a descubrir, analizar y preparar respuestas para oportunidades en Mercado Público usando una estructura profesional de backend y frontend.
4
+
5
+ ## Problema
6
+
7
+ Las empresas enfrentan desafíos para evaluar licitaciones públicas rápidamente, entender riesgos, calcular el fit con su oferta y producir una propuesta competitiva.
8
+
9
+ ## Solución
10
+
11
+ AndesOps AI ofrece:
12
+
13
+ - Búsqueda de oportunidades de Mercado Público con fallback a datos mock.
14
+ - Perfil de empresa para evaluar encaje.
15
+ - Análisis de tender con score, riesgos, brechas de cumplimiento y plan de acción.
16
+ - Borrador de propuesta y reporte en Markdown.
17
+
18
+ ## Arquitectura
19
+
20
+ - Backend: FastAPI + Python
21
+ - Frontend: Next.js + React + TypeScript + Tailwind CSS
22
+ - Datos: JSON mock inicial con opción Mercado Público API
23
+ - Capa AI: abstracción de proveedor con fallback determinista
24
+
25
+ ## Estructura del repositorio
26
+
27
+ - `backend/`: API FastAPI y servicios de análisis
28
+ - `frontend/`: aplicación Next.js con interfaz enterprise-ready
29
+ - `.env.example`: variables de entorno para backend y frontend
30
+ - `docker-compose.yml`: configuración básica para desarrollo
31
+
32
+ ## Setup local
33
+
34
+ ### Backend
35
+
36
+ ```powershell
37
+ cd backend
38
+ python -m venv .venv
39
+ .\.venv\Scripts\Activate.ps1
40
+ pip install -r requirements.txt
41
+ uvicorn app.main:app --reload --port 8000
42
+ ```
43
+
44
+ ### Frontend
45
+
46
+ ```powershell
47
+ cd frontend
48
+ npm install
49
+ npm run dev
50
+ ```
51
+
52
+ ### Variables de entorno
53
+
54
+ Copiar `.env.example` a `.env` y ajustar:
55
+
56
+ - `MERCADO_PUBLICO_TICKET`: ticket de Mercado Público si está disponible.
57
+ - `GEMINI_API_KEY`: clave de Gemini para análisis con LLM.
58
+ - `NEXT_PUBLIC_API_BASE`: URL base del backend (por defecto `http://localhost:8000`).
59
+
60
+ ## Endpoints principales
61
+
62
+ - `GET /health`
63
+ - `GET /api/tenders?keyword=software`
64
+ - `GET /api/tenders/{code}`
65
+ - `POST /api/company-profile`
66
+ - `POST /api/analyze`
67
+
68
+ ## Hackathon submission notes
69
+
70
+ Este MVP se centra en una experiencia demoable y enterprise-ready con:
71
+
72
+ - Interfaz limpia y moderna
73
+ - Flujo completo de búsqueda, selección, análisis y propuesta
74
+ - Fallback a datos mock para demostrar sin dependencias externas
75
+ - Estructura preparada para añadir soporte real de Mercado Público y Gemini
76
+
77
+ ## Licencia
78
+
79
+ MIT License
backend/Dockerfile ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt ./
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY . .
6
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
backend/app/__init__.py ADDED
File without changes
backend/app/config.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings
2
+
3
+
4
+ class Settings(BaseSettings):
5
+ mercado_publico_ticket: str | None = None
6
+ gemini_api_key: str | None = None
7
+ gemini_model: str = "gemini-1.5-flash"
8
+ next_public_api_base: str | None = None
9
+ database_url: str | None = None
10
+
11
+ class Config:
12
+ env_file = ".env"
13
+ env_file_encoding = "utf-8"
14
+ extra = "ignore"
15
+
16
+
17
+ settings = Settings()
backend/app/database.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.ext.declarative import declarative_base
3
+ from sqlalchemy.orm import sessionmaker
4
+ from app.config import settings
5
+
6
+ SQLALCHEMY_DATABASE_URL = settings.database_url or "mysql+pymysql://root:@localhost/andesai_db"
7
+
8
+ engine = create_engine(
9
+ SQLALCHEMY_DATABASE_URL
10
+ )
11
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
12
+
13
+ Base = declarative_base()
14
+
15
+ def get_db():
16
+ db = SessionLocal()
17
+ try:
18
+ yield db
19
+ finally:
20
+ db.close()
backend/app/main.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from app.routers import analysis, company, health, tenders, documents
4
+ from app.config import settings
5
+ from app.database import engine, Base
6
+
7
+ # Create tables
8
+ Base.metadata.create_all(bind=engine)
9
+
10
+ app = FastAPI(title="AndesOps AI")
11
+
12
+ origins = [
13
+ "http://localhost:3000",
14
+ "http://localhost:3001",
15
+ "http://127.0.0.1:3000",
16
+ ]
17
+
18
+ app.add_middleware(
19
+ CORSMiddleware,
20
+ allow_origins=origins,
21
+ allow_credentials=True,
22
+ allow_methods=["*"],
23
+ allow_headers=["*"],
24
+ )
25
+
26
+ # Move health under /api for consistency and to fix 404s
27
+ app.include_router(health.router, prefix="/api", tags=["Health"])
28
+ app.include_router(tenders.router, prefix="/api", tags=["Tenders"])
29
+ app.include_router(analysis.router, prefix="/api", tags=["Analysis"])
30
+ app.include_router(company.router, prefix="/api", tags=["Company"])
31
+ app.include_router(documents.router, prefix="/api", tags=["Documents"])
32
+
33
+ @app.get("/")
34
+ def read_root():
35
+ return {"message": "Welcome to AndesOps AI API"}
backend/app/models/tender.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, String, Float, DateTime, Text, JSON
2
+ from app.database import Base
3
+ from datetime import datetime
4
+
5
+ class TenderModel(Base):
6
+ __tablename__ = "tenders"
7
+
8
+ code = Column(String(50), primary_key=True, index=True)
9
+ name = Column(String(255), index=True)
10
+ buyer = Column(String(255), index=True)
11
+ status = Column(String(100))
12
+ closing_date = Column(DateTime, nullable=True)
13
+ description = Column(Text)
14
+ estimated_amount = Column(Float, nullable=True)
15
+ source = Column(String(50), default="Mercado Publico")
16
+ region = Column(String(100), nullable=True)
17
+ sector = Column(String(100), nullable=True)
18
+
19
+ # Storage for nested structures as JSON for simplicity in this hackathon
20
+ items = Column(JSON, nullable=True)
21
+ attachments = Column(JSON, nullable=True)
22
+
23
+ # Metadata for the app logic
24
+ last_updated = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
25
+ is_followed = Column(DateTime, nullable=True) # Date when it was followed, null if not
backend/app/routers/analysis.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from typing import List
3
+
4
+ from fastapi import APIRouter
5
+
6
+ from app.schemas.analysis import AnalysisRecord, AnalysisRequest, AnalysisResult
7
+ from app.services.agents import run_full_analysis
8
+ from app.services.persistence import save_to_json, load_from_json
9
+
10
+ router = APIRouter()
11
+
12
+ # Load initial history from disk
13
+ analysis_history: List[AnalysisRecord] = load_from_json(AnalysisRecord, "analysis_history.json")
14
+
15
+
16
+ @router.post("/analyze", response_model=AnalysisResult)
17
+ async def analyze_opportunity(request: AnalysisRequest):
18
+ result = await run_full_analysis(request.tender, request.company_profile, request.document_text)
19
+ record = AnalysisRecord(
20
+ tender_code=request.tender.code,
21
+ tender_name=request.tender.name,
22
+ analyzed_at=datetime.utcnow(),
23
+ analysis=result,
24
+ )
25
+ analysis_history.insert(0, record)
26
+ if len(analysis_history) > 20:
27
+ analysis_history.pop()
28
+
29
+ # Persist to disk
30
+ save_to_json(analysis_history, "analysis_history.json")
31
+
32
+ return result
33
+
34
+
35
+ @router.get("/analysis-history", response_model=List[AnalysisRecord])
36
+ def get_analysis_history():
37
+ return analysis_history
backend/app/routers/company.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ from app.schemas.company import CompanyProfile
3
+ from app.services.persistence import save_to_json, load_from_json
4
+
5
+ router = APIRouter()
6
+
7
+ # Load initial profile from disk
8
+ _profiles = load_from_json(CompanyProfile, "company_profile.json")
9
+ company_profile_cache: CompanyProfile | None = _profiles[0] if _profiles else None
10
+
11
+
12
+ @router.post("/company-profile", response_model=CompanyProfile)
13
+ def save_company_profile(profile: CompanyProfile):
14
+ global company_profile_cache
15
+ company_profile_cache = profile
16
+ # Persist to disk (as a list of one item for simplicity with current service)
17
+ save_to_json([profile], "company_profile.json")
18
+ return profile
19
+
20
+
21
+ @router.get("/company-profile", response_model=CompanyProfile)
22
+ def get_company_profile():
23
+ if company_profile_cache is None:
24
+ raise HTTPException(status_code=404, detail="No company profile saved")
25
+ return company_profile_cache
backend/app/routers/documents.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ from fastapi import APIRouter, File, UploadFile
3
+ from pypdf import PdfReader
4
+
5
+ router = APIRouter()
6
+
7
+ @router.post("/upload-document")
8
+ async def upload_document(file: UploadFile = File(...)):
9
+ if not file.filename.lower().endswith(".pdf"):
10
+ return {"error": "Solo se admiten archivos PDF por ahora."}
11
+
12
+ try:
13
+ content = await file.read()
14
+ pdf_file = io.BytesIO(content)
15
+ reader = PdfReader(pdf_file)
16
+
17
+ extracted_text = ""
18
+ for page in reader.pages:
19
+ extracted_text += page.extract_text() + "\n"
20
+
21
+ return {
22
+ "filename": file.filename,
23
+ "text": extracted_text[:100000], # Limit to 100k chars for context
24
+ "length": len(extracted_text)
25
+ }
26
+ except Exception as e:
27
+ return {"error": f"Error al procesar el PDF: {str(e)}"}
backend/app/routers/health.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends
2
+ from sqlalchemy.orm import Session
3
+ from sqlalchemy import func
4
+ from app.database import get_db
5
+ from app.models.tender import TenderModel
6
+
7
+ router = APIRouter()
8
+
9
+ @router.get("/health")
10
+ def health_check():
11
+ return {"status": "ok", "service": "andesops-ai"}
12
+
13
+ @router.get("/health/db-status")
14
+ def get_db_status(db: Session = Depends(get_db)):
15
+ total_tenders = db.query(TenderModel).count()
16
+
17
+ # Top buyers in our local DB
18
+ top_buyers = db.query(
19
+ TenderModel.buyer,
20
+ func.count(TenderModel.code).label('count')
21
+ ).group_by(TenderModel.buyer).order_by(func.count(TenderModel.code).desc()).limit(3).all()
22
+
23
+ # Last update time
24
+ last_tender = db.query(TenderModel).order_by(TenderModel.last_updated.desc()).first()
25
+
26
+ return {
27
+ "total_records": total_tenders,
28
+ "last_sync": last_tender.last_updated if last_tender else None,
29
+ "top_buyers": [{"name": b[0], "count": b[1]} for b in top_buyers if b[0]]
30
+ }
backend/app/routers/tenders.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional
2
+ from fastapi import APIRouter, Query, Depends
3
+ from sqlalchemy.orm import Session
4
+ from sqlalchemy import or_, desc
5
+
6
+ from app.schemas.tender import Tender
7
+ from app.database import get_db
8
+ from app.models.tender import TenderModel
9
+ from app.services.sync import sync_tenders_to_db, clean_expired_tenders
10
+
11
+ router = APIRouter()
12
+
13
+ @router.get("/tenders", response_model=List[Tender])
14
+ async def search_tender_opportunities(
15
+ keyword: Optional[str] = None,
16
+ buyer: Optional[str] = None,
17
+ region: Optional[str] = None,
18
+ skip: int = 0,
19
+ limit: int = 50,
20
+ db: Session = Depends(get_db)
21
+ ):
22
+ # 1. Búsqueda en DB con paginación
23
+ query = db.query(TenderModel)
24
+
25
+ if keyword:
26
+ query = query.filter(
27
+ or_(
28
+ TenderModel.name.ilike(f"%{keyword}%"),
29
+ TenderModel.code.ilike(f"%{keyword}%"),
30
+ TenderModel.description.ilike(f"%{keyword}%")
31
+ )
32
+ )
33
+
34
+ if buyer:
35
+ query = query.filter(TenderModel.buyer.ilike(f"%{buyer}%"))
36
+
37
+ if region:
38
+ query = query.filter(TenderModel.region.ilike(f"%{region}%"))
39
+
40
+ # Ordenar por fecha de cierre (más próximas primero)
41
+ results = query.order_by(TenderModel.closing_date.asc()).offset(skip).limit(limit).all()
42
+
43
+ # 2. Si la DB está vacía y no hay filtros, sugerir sincronización (no hace nada automático)
44
+ if not results and keyword and len(keyword) > 3:
45
+ await sync_tenders_to_db(db, keyword=keyword)
46
+ results = query.offset(skip).limit(limit).all()
47
+
48
+ return results
49
+
50
+ @router.get("/tenders/count")
51
+ def get_tenders_count(db: Session = Depends(get_db)):
52
+ """Devuelve el total de licitaciones en la base de datos."""
53
+ return {"total": db.query(TenderModel).count()}
54
+
55
+ @router.post("/tenders/sync")
56
+ async def manual_sync(keyword: Optional[str] = None, db: Session = Depends(get_db)):
57
+ return await sync_tenders_to_db(db, keyword=keyword)
backend/app/schemas/analysis.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from pydantic import BaseModel
3
+ from typing import List
4
+
5
+ from app.schemas.company import CompanyProfile
6
+ from app.schemas.tender import Tender
7
+
8
+
9
+ class RiskItem(BaseModel):
10
+ title: str
11
+ severity: str
12
+ explanation: str
13
+
14
+
15
+ class ActionItem(BaseModel):
16
+ task: str
17
+ priority: str
18
+ owner: str
19
+ timeline: str
20
+
21
+
22
+ class AnalysisRequest(BaseModel):
23
+ tender: Tender
24
+ company_profile: CompanyProfile
25
+ document_text: str | None = None
26
+
27
+
28
+ class AnalysisResult(BaseModel):
29
+ fit_score: int
30
+ decision: str
31
+ executive_summary: str
32
+ key_requirements: List[str]
33
+ risks: List[RiskItem]
34
+ compliance_gaps: List[str]
35
+ action_plan: List[ActionItem]
36
+ proposal_draft: str
37
+ report_markdown: str
38
+ audit_log: List[str] = []
39
+
40
+
41
+ class AnalysisRecord(BaseModel):
42
+ tender_code: str
43
+ tender_name: str
44
+ analyzed_at: datetime
45
+ analysis: AnalysisResult
backend/app/schemas/company.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import List
3
+
4
+
5
+ class CompanyProfile(BaseModel):
6
+ name: str
7
+ industry: str
8
+ services: List[str]
9
+ experience: str
10
+ certifications: List[str]
11
+ regions: List[str]
12
+ documents_available: List[str]
backend/app/schemas/tender.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import List, Optional
3
+
4
+ class TenderItem(BaseModel):
5
+ name: str
6
+ quantity: float
7
+ unit: str
8
+
9
+ class TenderAttachment(BaseModel):
10
+ name: str
11
+ url: str
12
+
13
+ class Tender(BaseModel):
14
+ code: str
15
+ name: str
16
+ buyer: str
17
+ status: str
18
+ closing_date: str
19
+ description: str
20
+ estimated_amount: Optional[float] = None
21
+ source: str
22
+ region: Optional[str] = None
23
+ sector: Optional[str] = None
24
+ items: List[TenderItem] = []
25
+ attachments: List[TenderAttachment] = []
backend/app/services/agents.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import asyncio
3
+ from typing import List, Dict, Any
4
+ from app.schemas.analysis import AnalysisResult, RiskItem, ActionItem
5
+ from app.schemas.company import CompanyProfile
6
+ from app.schemas.tender import Tender
7
+ from app.services.llm import generate_analysis, call_gemini, _parse_gemini_response
8
+ from app.services.report import generate_markdown_report
9
+
10
+ async def legal_agent_task(tender: Tender, company: CompanyProfile, document_text: str = "") -> str:
11
+ prompt = (
12
+ f"AGENT ROLE: Legal & Compliance Expert\n"
13
+ f"GOAL: Analyze administrative bases and compliance risks.\n"
14
+ f"TENDER: {tender.name} - {tender.description}\n"
15
+ f"COMPANY: {company.name} (Docs: {', '.join(company.documents_available)})\n"
16
+ f"EXTRACTED TEXT: {document_text[:5000]}\n"
17
+ f"TASK: Identify 3 legal gaps and 2 critical deadlines. Be concise and professional."
18
+ )
19
+ return await asyncio.to_thread(call_gemini, prompt)
20
+
21
+ async def technical_agent_task(tender: Tender, company: CompanyProfile, document_text: str = "") -> str:
22
+ prompt = (
23
+ f"AGENT ROLE: Technical Architect\n"
24
+ f"GOAL: Evaluate technical feasibility and product-market fit.\n"
25
+ f"TENDER: {tender.name} - {tender.description}\n"
26
+ f"COMPANY: {company.industry} - {company.experience}\n"
27
+ f"EXTRACTED TEXT: {document_text[:5000]}\n"
28
+ f"TASK: Identify 3 technical challenges and how our stack fits. Be concise."
29
+ )
30
+ return await asyncio.to_thread(call_gemini, prompt)
31
+
32
+ async def strategy_agent_task(tender: Tender, company: CompanyProfile, document_text: str = "") -> str:
33
+ prompt = (
34
+ f"AGENT ROLE: Risk & Strategy Specialist\n"
35
+ f"GOAL: Calculate ROI, competitive risks, and overall strategy.\n"
36
+ f"TENDER: {tender.name} - {tender.estimated_amount} CLP\n"
37
+ f"COMPANY: {company.name}\n"
38
+ f"TASK: Identify 3 strategic risks and 1 'Win Strategy'. Be concise."
39
+ )
40
+ return await asyncio.to_thread(call_gemini, prompt)
41
+
42
+ async def run_full_analysis(tender: Tender, company_profile: CompanyProfile, document_text: str | None = None) -> AnalysisResult:
43
+ audit_log = ["🚀 Iniciando mesa de expertos agéntica..."]
44
+ doc_text = document_text or ""
45
+
46
+ # Run agents in parallel
47
+ audit_log.append("👨‍⚖️ Agente Legal: Analizando bases administrativas...")
48
+ audit_log.append("👨‍💻 Agente Técnico: Evaluando requerimientos y arquitectura...")
49
+ audit_log.append("🕵️ Agente de Riesgo: Escaneando competitividad y rentabilidad...")
50
+
51
+ legal_task = legal_agent_task(tender, company_profile, doc_text)
52
+ tech_task = technical_agent_task(tender, company_profile, doc_text)
53
+ strat_task = strategy_agent_task(tender, company_profile, doc_text)
54
+
55
+ responses = await asyncio.gather(legal_task, tech_task, strat_task)
56
+ legal_resp, tech_resp, strat_resp = responses
57
+
58
+ audit_log.append("💡 Consolidando hallazgos de los expertos...")
59
+
60
+ # Final Synthesis
61
+ synthesis_prompt = (
62
+ f"Eres el ORQUESTADOR DE ANDESOPS AI.\n"
63
+ f"Has recibido reportes de tus expertos para la licitación: {tender.name}.\n\n"
64
+ f"REPORTE LEGAL: {legal_resp}\n\n"
65
+ f"REPORTE TÉCNICO: {tech_resp}\n\n"
66
+ f"REPORTE ESTRATÉGICO: {strat_resp}\n\n"
67
+ f"INSTRUCCIONES: Genera el AnalysisResult final en JSON.\n"
68
+ f"Incluye un resumen ejecutivo que mencione los puntos de los expertos.\n"
69
+ f"El fit_score debe ser un consenso de los tres.\n"
70
+ f"Responde ÚNICAMENTE con el JSON."
71
+ )
72
+
73
+ final_output = await asyncio.to_thread(call_gemini, synthesis_prompt)
74
+ parse_result = _parse_gemini_response(final_output)
75
+
76
+ if parse_result:
77
+ try:
78
+ # Add individual thoughts to the audit log for visual impact
79
+ audit_log.append(f"Resumen Legal: {legal_resp[:150]}...")
80
+ audit_log.append(f"Resumen Técnico: {tech_resp[:150]}...")
81
+
82
+ if not parse_result.get("report_markdown"):
83
+ parse_result["report_markdown"] = generate_markdown_report(parse_result)
84
+
85
+ result = AnalysisResult(**parse_result)
86
+ result.audit_log = audit_log + (result.audit_log or [])
87
+ return result
88
+ except Exception as e:
89
+ print(f"Synthesis Error: {e}")
90
+
91
+ # Fallback to single agent if parallel fails or synthesis is bad
92
+ return generate_analysis(tender, company_profile, document_text)
backend/app/services/llm.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib
2
+ import json
3
+ import re
4
+ from typing import Any
5
+
6
+ import google.generativeai as genai
7
+ from app.config import settings
8
+ from app.schemas.analysis import AnalysisResult
9
+ from app.schemas.company import CompanyProfile
10
+ from app.schemas.tender import Tender
11
+ from app.services.report import generate_markdown_report
12
+
13
+ # Configure Gemini
14
+ if settings.gemini_api_key:
15
+ genai.configure(api_key=settings.gemini_api_key)
16
+
17
+ def get_gemini_model():
18
+ return genai.GenerativeModel(
19
+ model_name=settings.gemini_model,
20
+ generation_config={
21
+ "temperature": 0.2,
22
+ "top_p": 0.95,
23
+ "top_k": 64,
24
+ "max_output_tokens": 8192, # Increased for document processing
25
+ "response_mime_type": "application/json",
26
+ }
27
+ )
28
+
29
+ def call_gemini(prompt: str) -> str:
30
+ if not settings.gemini_api_key:
31
+ return ""
32
+
33
+ try:
34
+ model = get_gemini_model()
35
+ response = model.generate_content(prompt)
36
+ return response.text
37
+ except Exception as e:
38
+ print(f"Error calling Gemini: {e}")
39
+ return ""
40
+
41
+ def _normalize_gemini_output(output: str) -> str:
42
+ if not output:
43
+ return output
44
+
45
+ text = output.strip()
46
+ if text.startswith("```"):
47
+ parts = text.split("```")
48
+ if len(parts) >= 2:
49
+ first_part = parts[1].strip()
50
+ if first_part.lower().startswith("json"):
51
+ text = first_part[4:].strip()
52
+ else:
53
+ text = first_part
54
+
55
+ return text
56
+
57
+ def _parse_gemini_response(output: str) -> dict[str, Any] | None:
58
+ candidate = _normalize_gemini_output(output)
59
+ try:
60
+ return json.loads(candidate)
61
+ except json.JSONDecodeError:
62
+ json_match = re.search(r"\{.*\}", candidate, re.DOTALL)
63
+ if json_match:
64
+ try:
65
+ return json.loads(json_match.group(0))
66
+ except:
67
+ pass
68
+ return None
69
+
70
+ def _build_analysis_prompt(tender: Tender, company: CompanyProfile, document_text: str | None = None) -> str:
71
+ doc_context = f"\n\nCONTENIDO DE LAS BASES (DOCUMENTO ADJUNTO):\n{document_text}\n" if document_text else ""
72
+ return (
73
+ "Eres un Sistema Multi-Agente de Élite para análisis de licitaciones públicas en Chile (Mercado Público).\n"
74
+ "Tu objetivo es realizar un análisis exhaustivo y colaborativo entre tres expertos virtuales:\n\n"
75
+ "1. **Agente Legal & Cumplimiento**: Enfocado en bases administrativas, plazos y requisitos de documentos.\n"
76
+ "2. **Agente Técnico**: Enfocado en el encaje de la oferta, arquitectura y capacidades de ejecución.\n"
77
+ "3. **Agente de Riesgos & Estrategia**: Enfocado en rentabilidad, riesgos del proyecto y competitividad.\n\n"
78
+ "INSTRUCCIONES:\n"
79
+ "- Analiza la licitación, el perfil de la empresa y el contenido de las bases adjuntas si están disponibles.\n"
80
+ "- Genera una decisión consensuada.\n"
81
+ "- Devuelve un objeto JSON con la estructura exacta de 'AnalysisResult'.\n\n"
82
+ "CAMPOS REQUERIDOS EN EL JSON:\n"
83
+ "- fit_score: (0-100) puntaje de encaje.\n"
84
+ "- decision: 'Recommended', 'Review Carefully' o 'Not Recommended'.\n"
85
+ "- executive_summary: Resumen ejecutivo profesional en español.\n"
86
+ "- key_requirements: Lista de los 5 requisitos más críticos.\n"
87
+ "- risks: Lista de objetos {title, severity: 'High'|'Medium'|'Low', explanation}.\n"
88
+ "- compliance_gaps: Lista de posibles brechas o documentos faltantes.\n"
89
+ "- action_plan: Lista de objetos {task, priority, owner, timeline}.\n"
90
+ "- proposal_draft: Un borrador de propuesta comercial (Markdown) convincente.\n"
91
+ "- audit_log: Lista de pasos que tomaron los agentes para llegar a esta conclusión.\n\n"
92
+ f"DATOS DE LA LICITACIÓN:\n{tender.model_dump_json(indent=2)}\n\n"
93
+ f"DATOS DE LA EMPRESA:\n{company.model_dump_json(indent=2)}\n"
94
+ f"{doc_context}\n\n"
95
+ "RESPONDE ÚNICAMENTE CON EL JSON VÁLIDO."
96
+ )
97
+
98
+ def generate_mock_analysis(tender: Tender, company: CompanyProfile) -> AnalysisResult:
99
+ raw = f"{tender.code}:{tender.name}:{company.name}"
100
+ digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()
101
+ score = int(digest[:8], 16) % 41 + 55
102
+
103
+ return AnalysisResult(
104
+ fit_score=score,
105
+ decision="Recommended" if score > 75 else "Review Carefully",
106
+ executive_summary=f"Análisis automático para {tender.name}. Se observa un encaje técnico razonable.",
107
+ key_requirements=["Documentación legal", "Experiencia técnica", "Garantía de seriedad"],
108
+ risks=[{"title": "Plazo ajustado", "severity": "Medium", "explanation": "El tiempo de entrega es crítico."}],
109
+ compliance_gaps=["Validar boleta de garantía"],
110
+ action_plan=[{"task": "Revisar bases", "priority": "High", "owner": "Legal", "timeline": "2 días"}],
111
+ proposal_draft="Borrador generado automáticamente...",
112
+ report_markdown="# Reporte de Licitación",
113
+ audit_log=["Iniciando análisis de respaldo...", "Generando datos mock."]
114
+ )
115
+
116
+ def generate_analysis(tender: Tender, company: CompanyProfile, document_text: str | None = None) -> AnalysisResult:
117
+ prompt = _build_analysis_prompt(tender, company, document_text)
118
+ output = call_gemini(prompt)
119
+ parse_result = _parse_gemini_response(output)
120
+
121
+ if parse_result:
122
+ try:
123
+ if not parse_result.get("report_markdown"):
124
+ parse_result["report_markdown"] = generate_markdown_report(parse_result)
125
+ return AnalysisResult(**parse_result)
126
+ except Exception as e:
127
+ print(f"Error mapping to AnalysisResult: {e}")
128
+
129
+ analysis = generate_mock_analysis(tender, company)
130
+ analysis.audit_log.append("Fallback: Gemini no retornó un análisis válido.")
131
+ return analysis
backend/app/services/mercado_publico.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Any, List, Optional
4
+ from datetime import datetime
5
+
6
+ import httpx
7
+
8
+ from app.config import settings
9
+ from app.schemas.tender import Tender, TenderItem, TenderAttachment
10
+
11
+ # No mock data paths allowed
12
+
13
+ # API Mappings from official documentation
14
+ TENDER_TYPES = {
15
+ "L1": "Licitación Pública < 100 UTM",
16
+ "LE": "Licitación Pública 100-1000 UTM",
17
+ "LP": "Licitación Pública > 1000 UTM",
18
+ "LS": "Licitación Pública Servicios Personales",
19
+ "LR": "Licitación Pública (Regulada)",
20
+ "A1": "Licitación Privada sin oferentes previos",
21
+ "B1": "Licitación Privada excluida Ley de Compras",
22
+ "CO": "Licitación Privada 100-1000 UTM",
23
+ "B2": "Licitación Privada > 1000 UTM",
24
+ "D1": "Trato Directo Proveedor Único",
25
+ "C2": "Trato Directo (Cotización)",
26
+ "C1": "Compra Directa (Orden de compra)",
27
+ "O1": "Obras Públicas",
28
+ }
29
+
30
+ TENDER_STATUS_CODES = {
31
+ "5": "Publicada",
32
+ "6": "Cerrada",
33
+ "7": "Desierta",
34
+ "8": "Adjudicada",
35
+ "18": "Revocada",
36
+ "19": "Suspendida",
37
+ }
38
+
39
+
40
+ def _extract_type_label(code: str) -> str:
41
+ """Extract tender type from code like '1000-6-LE26' -> 'LE' -> 'Licitación Pública 100-1000 UTM'."""
42
+ if not code:
43
+ return ""
44
+ parts = code.split("-")
45
+ if len(parts) >= 3:
46
+ type_code = "".join(c for c in parts[2] if c.isalpha())
47
+ return TENDER_TYPES.get(type_code, type_code)
48
+ return ""
49
+
50
+
51
+ # Mock data loading removed for production-ready hackathon version
52
+
53
+
54
+ def normalize_list_tender(raw: dict[str, Any]) -> Tender:
55
+ """Normalize a tender from the LISTING endpoint (basic data only)."""
56
+ code = raw.get("CodigoExterno", "")
57
+ status_code = str(raw.get("CodigoEstado", ""))
58
+ status_desc = TENDER_STATUS_CODES.get(status_code, f"Estado {status_code}")
59
+ closing = raw.get("FechaCierre", "")
60
+
61
+ return Tender(
62
+ code=code,
63
+ name=raw.get("Nombre", "Sin título"),
64
+ buyer="", # Not available in list endpoint
65
+ status=status_desc,
66
+ closing_date=str(closing) if closing else "",
67
+ description=raw.get("Nombre", ""),
68
+ estimated_amount=None,
69
+ source="Mercado Público",
70
+ region=None,
71
+ sector=_extract_type_label(code),
72
+ items=[],
73
+ attachments=[],
74
+ )
75
+
76
+
77
+ def normalize_detail_tender(raw: dict[str, Any]) -> Tender:
78
+ """Normalize a tender from the DETAIL endpoint (full data)."""
79
+ # Items
80
+ items_data = raw.get("Items", {})
81
+ raw_items = items_data.get("Listado", []) if isinstance(items_data, dict) else []
82
+ items = []
83
+ for ri in raw_items:
84
+ if isinstance(ri, dict):
85
+ items.append(TenderItem(
86
+ name=ri.get("NombreProducto", ri.get("Producto", "Item")),
87
+ quantity=float(ri.get("Cantidad", 0)),
88
+ unit=ri.get("UnidadMedida", "Unidad"),
89
+ ))
90
+
91
+ # Attachments
92
+ adj_data = raw.get("Adjuntos", [])
93
+ attachments = []
94
+ if isinstance(adj_data, list):
95
+ for ra in adj_data:
96
+ if isinstance(ra, dict):
97
+ attachments.append(TenderAttachment(
98
+ name=ra.get("NombreArchivo", ra.get("Nombre", "Adjunto")),
99
+ url=ra.get("URL", ra.get("Url", "#")),
100
+ ))
101
+
102
+ code = raw.get("CodigoExterno", raw.get("Codigo", ""))
103
+ status_code = str(raw.get("CodigoEstado", raw.get("Estado", "")))
104
+ status_desc = TENDER_STATUS_CODES.get(status_code, f"Estado {status_code}")
105
+
106
+ # Buyer info
107
+ comprador = raw.get("Comprador", {})
108
+ buyer_name = ""
109
+ if isinstance(comprador, dict):
110
+ buyer_name = comprador.get("NombreOrganismo", comprador.get("Nombre", ""))
111
+ if not buyer_name:
112
+ buyer_name = raw.get("Organismo", raw.get("NombreOrganismo", ""))
113
+
114
+ return Tender(
115
+ code=code,
116
+ name=raw.get("Nombre", "Sin título"),
117
+ buyer=str(buyer_name) if buyer_name else "No disponible",
118
+ status=status_desc,
119
+ closing_date=str(raw.get("FechaCierre", "")),
120
+ description=raw.get("Descripcion", raw.get("Nombre", "")),
121
+ estimated_amount=raw.get("MontoEstimado", None),
122
+ source="Mercado Público",
123
+ region=None,
124
+ sector=_extract_type_label(code),
125
+ items=items,
126
+ attachments=attachments,
127
+ )
128
+
129
+
130
+ async def _api_call_list(params: dict) -> List[Tender]:
131
+ """Call listing endpoint (returns basic data for many tenders)."""
132
+ if not settings.mercado_publico_ticket:
133
+ print("[MercadoPublico] No ticket configured")
134
+ return []
135
+
136
+ params["ticket"] = settings.mercado_publico_ticket
137
+ try:
138
+ print(f"[MercadoPublico] List request: {params}")
139
+ async with httpx.AsyncClient(timeout=60) as client:
140
+ url = "https://api.mercadopublico.cl/servicios/v1/publico/licitaciones.json"
141
+ response = await client.get(url, params=params)
142
+ response.raise_for_status()
143
+ body = response.json()
144
+ items = body.get("Listado", [])
145
+ cantidad = body.get("Cantidad", 0)
146
+ print(f"[MercadoPublico] Got {cantidad} items from API")
147
+ if isinstance(items, dict):
148
+ items = [items]
149
+ return [normalize_list_tender(i) for i in items if isinstance(i, dict)]
150
+ except Exception as e:
151
+ print(f"[MercadoPublico] List API Error: {e}")
152
+ return []
153
+
154
+
155
+ async def _api_call_detail(code: str) -> Optional[Tender]:
156
+ """Call detail endpoint for a single tender by code (returns full data)."""
157
+ if not settings.mercado_publico_ticket:
158
+ return None
159
+
160
+ params = {"codigo": code, "ticket": settings.mercado_publico_ticket}
161
+ try:
162
+ print(f"[MercadoPublico] Detail request for: {code}")
163
+ async with httpx.AsyncClient(timeout=30) as client:
164
+ url = "https://api.mercadopublico.cl/servicios/v1/publico/licitaciones.json"
165
+ response = await client.get(url, params=params)
166
+ response.raise_for_status()
167
+ body = response.json()
168
+ items = body.get("Listado", [])
169
+ if isinstance(items, dict):
170
+ items = [items]
171
+ if items and isinstance(items[0], dict):
172
+ return normalize_detail_tender(items[0])
173
+ return None
174
+ except Exception as e:
175
+ print(f"[MercadoPublico] Detail API Error: {e}")
176
+ return None
177
+
178
+
179
+ async def fetch_tenders(keyword: str = None, date: str = None, state: str = "activas") -> List[Tender]:
180
+ """Main search function. Searches by keyword (code or text) or by date/state."""
181
+
182
+ # If keyword looks like a tender code (contains dashes and alphanumeric), search by code
183
+ if keyword and "-" in keyword:
184
+ result = await _api_call_detail(keyword)
185
+ return [result] if result else []
186
+
187
+ # Otherwise, get listing and filter client-side
188
+ params: dict[str, str] = {}
189
+ if state:
190
+ params["estado"] = state
191
+ if date:
192
+ params["fecha"] = date
193
+
194
+ results = await _api_call_list(params)
195
+
196
+ # Client-side keyword filter (API listing doesn't support text search)
197
+ if keyword:
198
+ kw = keyword.lower()
199
+ results = [
200
+ r for r in results
201
+ if kw in r.name.lower() or kw in r.code.lower()
202
+ ]
203
+
204
+ # Return all results to populate the local DB
205
+ return results
206
+
207
+
208
+ async def get_tenders_by_buyer(buyer_code: str, date: str = None) -> List[Tender]:
209
+ params: dict[str, str] = {"CodigoOrganismo": buyer_code}
210
+ if date:
211
+ params["fecha"] = date
212
+ return await _api_call_list(params)
213
+
214
+
215
+ async def get_tenders_by_provider(provider_code: str, date: str = None) -> List[Tender]:
216
+ params: dict[str, str] = {"CodigoProveedor": provider_code}
217
+ if date:
218
+ params["fecha"] = date
219
+ return await _api_call_list(params)
220
+
221
+
222
+ async def get_tender_by_code(code: str) -> Optional[Tender]:
223
+ return await _api_call_detail(code)
224
+
225
+
226
+ async def search_organizations(keyword: str = None) -> List[dict]:
227
+ """Search buyer organizations using the Mercado Público directory."""
228
+ if not settings.mercado_publico_ticket:
229
+ return []
230
+ try:
231
+ async with httpx.AsyncClient(timeout=10) as client:
232
+ url = "https://api.mercadopublico.cl/servicios/v1/Publico/Empresas/BuscarComprador"
233
+ params = {"ticket": settings.mercado_publico_ticket}
234
+ response = await client.get(url, params=params)
235
+ body = response.json()
236
+ items = body if isinstance(body, list) else []
237
+ if keyword:
238
+ kw = keyword.lower()
239
+ return [i for i in items if kw in str(i.get("NombreEmpresa", "")).lower()][:50]
240
+ return items[:50]
241
+ except Exception as e:
242
+ print(f"[MercadoPublico] Org Search Error: {e}")
243
+ return []
backend/app/services/persistence.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ from typing import List, Type, TypeVar
4
+ from pydantic import BaseModel
5
+
6
+ T = TypeVar("T", bound=BaseModel)
7
+
8
+ DATA_DIR = Path(__file__).resolve().parent.parent / "data"
9
+ DATA_DIR.mkdir(exist_ok=True)
10
+
11
+ def save_to_json(data: List[BaseModel], filename: str):
12
+ path = DATA_DIR / filename
13
+ with path.open("w", encoding="utf-8") as f:
14
+ json.dump([item.model_dump(mode="json") for item in data], f, indent=2, ensure_ascii=False)
15
+
16
+ def load_from_json(model_class: Type[T], filename: str) -> List[T]:
17
+ path = DATA_DIR / filename
18
+ if not path.exists():
19
+ return []
20
+ with path.open("r", encoding="utf-8") as f:
21
+ try:
22
+ raw = json.load(f)
23
+ return [model_class(**item) for item in raw]
24
+ except:
25
+ return []
backend/app/services/report.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any
2
+
3
+
4
+ def _value(analysis: Any, key: str):
5
+ if isinstance(analysis, dict):
6
+ return analysis.get(key, "")
7
+ return getattr(analysis, key, "")
8
+
9
+
10
+ def generate_markdown_report(analysis: Any) -> str:
11
+ lines = [
12
+ f"# Informe de Análisis: {_value(analysis, 'fit_score')}% de ajuste",
13
+ "",
14
+ f"**Decisión:** {_value(analysis, 'decision')}",
15
+ "",
16
+ "## Resumen Ejecutivo",
17
+ _value(analysis, "executive_summary"),
18
+ "",
19
+ "## Requisitos Clave",
20
+ ]
21
+ for req in _value(analysis, "key_requirements") or []:
22
+ lines.append(f"- {req}")
23
+ lines.append("")
24
+ lines.append("## Riesgos")
25
+ for risk in _value(analysis, "risks") or []:
26
+ lines.append(f"- **{risk['title']}** ({risk['severity']}): {risk['explanation']}")
27
+ lines.append("")
28
+ lines.append("## Brechas de Cumplimiento")
29
+ for gap in _value(analysis, "compliance_gaps") or []:
30
+ lines.append(f"- {gap}")
31
+ lines.append("")
32
+ lines.append("## Plan de Acción")
33
+ for item in _value(analysis, "action_plan") or []:
34
+ lines.append(
35
+ f"- **{item['task']}** | Prioridad: {item['priority']} | Responsable: {item['owner']} | Tiempo: {item['timeline']}"
36
+ )
37
+ lines.append("")
38
+ lines.append("## Borrador de Propuesta")
39
+ lines.append(_value(analysis, "proposal_draft"))
40
+ return "\n".join(lines)
backend/app/services/sync.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from datetime import datetime
3
+ from app.models.tender import TenderModel
4
+ from app.services.mercado_publico import fetch_tenders, get_tender_by_code
5
+ import json
6
+
7
+ async def sync_tenders_to_db(db: Session, keyword: str = None):
8
+ """
9
+ Fetches tenders from API and saves/updates them in the MySQL database.
10
+ """
11
+ print(f"[Sync] Starting synchronization... keyword={keyword}")
12
+ try:
13
+ api_tenders = await fetch_tenders(keyword=keyword)
14
+ if not api_tenders:
15
+ print("[Sync] WARNING: API returned ZERO tenders. Check Ticket or API Status.")
16
+ else:
17
+ print(f"[Sync] API returned {len(api_tenders)} tenders for processing.")
18
+ except Exception as e:
19
+ print(f"[Sync] FATAL ERROR during fetch: {e}")
20
+ return {"error": str(e)}
21
+
22
+ count_new = 0
23
+ count_updated = 0
24
+
25
+ for api_t in api_tenders:
26
+ # Check if exists
27
+ db_tender = db.query(TenderModel).filter(TenderModel.code == api_t.code).first()
28
+
29
+ # Convert Pydantic model to dict for DB
30
+ tender_data = {
31
+ "code": api_t.code,
32
+ "name": api_t.name,
33
+ "buyer": api_t.buyer,
34
+ "status": api_t.status,
35
+ "closing_date": datetime.fromisoformat(api_t.closing_date.replace("Z", "")) if api_t.closing_date else None,
36
+ "description": api_t.description,
37
+ "estimated_amount": api_t.estimated_amount,
38
+ "source": api_t.source,
39
+ "region": api_t.region,
40
+ "sector": api_t.sector,
41
+ "items": [item.dict() for item in api_t.items] if api_t.items else [],
42
+ "attachments": [att.dict() for att in api_t.attachments] if api_t.attachments else []
43
+ }
44
+
45
+ if db_tender:
46
+ # Update existing
47
+ for key, value in tender_data.items():
48
+ setattr(db_tender, key, value)
49
+ count_updated += 1
50
+ else:
51
+ # Create new
52
+ new_tender = TenderModel(**tender_data)
53
+ db.add(new_tender)
54
+ count_new += 1
55
+
56
+ db.commit()
57
+ print(f"[Sync] Finished. New: {count_new}, Updated: {count_updated}")
58
+ return {"new": count_new, "updated": count_updated}
59
+
60
+ def clean_expired_tenders(db: Session):
61
+ """
62
+ Removes tenders where closing_date is in the past.
63
+ """
64
+ now = datetime.now()
65
+ expired = db.query(TenderModel).filter(TenderModel.closing_date < now).delete()
66
+ db.commit()
67
+ print(f"[Sync] Cleaned {expired} expired tenders.")
68
+ return expired
backend/requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.109.0
2
+ uvicorn[standard]==0.23.2
3
+ httpx==0.27.0
4
+ pydantic==2.8.0
5
+ pydantic-settings==2.4.0
6
+ google-generativeai==0.7.2
7
+ pypdf==4.2.0
8
+ python-multipart==0.0.9
docker-compose.yml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: "3.9"
2
+ services:
3
+ backend:
4
+ build: ./backend
5
+ command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
6
+ ports:
7
+ - "8000:8000"
8
+ volumes:
9
+ - ./backend:/app
10
+ environment:
11
+ - MERCADO_PUBLICO_TICKET=${MERCADO_PUBLICO_TICKET}
12
+ - GEMINI_API_KEY=${GEMINI_API_KEY}
13
+ frontend:
14
+ build:
15
+ context: ./frontend
16
+ command: npm run dev -- --hostname 0.0.0.0
17
+ ports:
18
+ - "3000:3000"
19
+ volumes:
20
+ - ./frontend:/app
21
+ environment:
22
+ - NEXT_PUBLIC_API_BASE=http://backend:8000
frontend/Dockerfile ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ FROM node:20-slim
2
+ WORKDIR /app
3
+ COPY package.json package-lock.json* ./
4
+ RUN npm install
5
+ COPY . .
6
+ CMD ["npm", "run", "dev", "--", "--hostname", "0.0.0.0"]
frontend/app/layout.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import "../globals.css";
2
+ import type { ReactNode } from "react";
3
+
4
+ export const metadata = {
5
+ title: "AndesOps AI",
6
+ description: "Enterprise tender intelligence for Chilean public procurement.",
7
+ };
8
+
9
+ export default function RootLayout({ children }: { children: ReactNode }) {
10
+ return (
11
+ <html lang="es">
12
+ <body>{children}</body>
13
+ </html>
14
+ );
15
+ }
frontend/app/page.tsx ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import Dashboard from "../components/Dashboard";
5
+ import TenderSearch from "../components/TenderSearch";
6
+ import CompanyProfile from "../components/CompanyProfile";
7
+ import AgentAnalysis from "../components/AgentAnalysis";
8
+ import ProposalDraft from "../components/ProposalDraft";
9
+ import Reports from "../components/Reports";
10
+ import Sidebar from "../components/Sidebar";
11
+ import AnalysisHistory from "../components/AnalysisHistory";
12
+ import { analyzeTender, fetchAnalysisHistory, fetchCompanyProfile, healthCheck, saveCompanyProfile, searchTenders } from "../lib/api";
13
+ import type { AnalysisHistoryItem, AnalysisResult, CompanyProfile as CompanyProfileType, Tender } from "../lib/types";
14
+
15
+ const tabs = [
16
+ "Dashboard",
17
+ "Tender Search",
18
+ "My Portfolio",
19
+ "Company Profile",
20
+ "Agent Analysis",
21
+ "Proposal Draft",
22
+ "Reports",
23
+ "History",
24
+ ] as const;
25
+
26
+ type Tab = (typeof tabs)[number];
27
+
28
+ export default function HomePage() {
29
+ const [activeTab, setActiveTab] = useState<Tab>("Dashboard");
30
+ const [tenders, setTenders] = useState<Tender[]>([]);
31
+ const [selectedTender, setSelectedTender] = useState<Tender | null>(null);
32
+ const [companyProfile, setCompanyProfile] = useState<CompanyProfileType>({
33
+ name: "Andes Digital",
34
+ industry: "Software development",
35
+ services: ["AI automation", "web apps", "data dashboards"],
36
+ experience: "5 years building enterprise software",
37
+ certifications: [],
38
+ regions: ["Metropolitana", "Valparaíso"],
39
+ documents_available: ["RUT", "Portfolio", "Financial Statements"],
40
+ });
41
+ const [analysisResult, setAnalysisResult] = useState<AnalysisResult | null>(null);
42
+ const [analysisHistory, setAnalysisHistory] = useState<AnalysisHistoryItem[]>([]);
43
+ const [status, setStatus] = useState("listening");
44
+
45
+ useEffect(() => {
46
+ // Initial tab from URL
47
+ if (typeof window !== 'undefined') {
48
+ const params = new URLSearchParams(window.location.search);
49
+ const tabParam = params.get('tab');
50
+ if (tabParam) {
51
+ const foundTab = tabs.find(t => t.toLowerCase().replace(/ /g, "_") === tabParam);
52
+ if (foundTab) setActiveTab(foundTab);
53
+ }
54
+ }
55
+
56
+ async function init() {
57
+ try {
58
+ await healthCheck();
59
+ setStatus("connected");
60
+ } catch {
61
+ setStatus("offline");
62
+ }
63
+
64
+ try {
65
+ const profile = await fetchCompanyProfile();
66
+ if (profile) setCompanyProfile(profile);
67
+ } catch (e) {
68
+ console.error("Profile load error", e);
69
+ }
70
+
71
+ try {
72
+ const history = await fetchAnalysisHistory();
73
+ setAnalysisHistory(history);
74
+ } catch (e) {
75
+ console.error("History load error", e);
76
+ }
77
+
78
+ try {
79
+ const initialTenders = await searchTenders({});
80
+ setTenders(initialTenders);
81
+ } catch (e) {
82
+ console.error("Tenders load error", e);
83
+ }
84
+ }
85
+
86
+ init();
87
+ }, []);
88
+
89
+ const recommendedCount = useMemo(
90
+ () => (analysisResult?.decision === "Recommended" ? 1 : 0),
91
+ [analysisResult]
92
+ );
93
+
94
+ const highRiskItems = useMemo(
95
+ () => analysisResult?.risks.filter((item) => item.severity === "High").length ?? 0,
96
+ [analysisResult]
97
+ );
98
+
99
+ const reportsGenerated = useMemo(() => (analysisResult ? 1 : 0), [analysisResult]);
100
+
101
+ const handleTenderSelect = (tender: Tender) => {
102
+ setSelectedTender(tender);
103
+ setActiveTab("Agent Analysis");
104
+ window.history.pushState({}, '', `?tab=agent_analysis`);
105
+ };
106
+
107
+ const handleSearch = async (params: { keyword?: string; buyer_code?: string; provider_code?: string; date?: string }) => {
108
+ const results = await searchTenders(params);
109
+ setTenders(results);
110
+ };
111
+
112
+ const handleProfileSave = async (profile: CompanyProfileType) => {
113
+ try {
114
+ const savedProfile = await saveCompanyProfile(profile);
115
+ setCompanyProfile(savedProfile);
116
+ } catch {
117
+ setCompanyProfile(profile);
118
+ }
119
+ };
120
+
121
+ const handleRunAnalysis = async (documentText?: string) => {
122
+ if (!selectedTender) return;
123
+ const result = await analyzeTender(selectedTender, companyProfile, documentText);
124
+ setAnalysisResult(result);
125
+
126
+ try {
127
+ const history = await fetchAnalysisHistory();
128
+ setAnalysisHistory(history);
129
+ } catch (e) {
130
+ console.error(e);
131
+ }
132
+ };
133
+
134
+ return (
135
+ <div className="min-h-screen bg-navy text-slate-100">
136
+ <div className="mx-auto flex min-h-screen max-w-[1440px] gap-6 px-6 py-6">
137
+ <Sidebar
138
+ tabs={tabs}
139
+ activeTab={activeTab}
140
+ onTabSelect={setActiveTab}
141
+ status={status}
142
+ />
143
+ <main className="flex-1 rounded-3xl border border-slate-800 bg-surface p-6 shadow-2xl shadow-slate-900/20">
144
+ {activeTab === "Dashboard" && (
145
+ <Dashboard
146
+ tendersFound={tenders.length}
147
+ recommendedOpportunities={recommendedCount}
148
+ highRiskItems={highRiskItems}
149
+ reportsGenerated={reportsGenerated}
150
+ tenders={tenders}
151
+ />
152
+ )}
153
+ {(activeTab === "Tender Search" || activeTab === "My Portfolio") && (
154
+ <TenderSearch
155
+ tenders={tenders}
156
+ onSearch={handleSearch}
157
+ onAnalyze={handleTenderSelect}
158
+ forceShowFollowed={activeTab === "My Portfolio"}
159
+ />
160
+ )}
161
+ {activeTab === "Company Profile" && (
162
+ <CompanyProfile profile={companyProfile} onSave={handleProfileSave} />
163
+ )}
164
+ {activeTab === "Agent Analysis" && (
165
+ <AgentAnalysis
166
+ tender={selectedTender}
167
+ companyProfile={companyProfile}
168
+ analysis={analysisResult}
169
+ onAnalyze={handleRunAnalysis}
170
+ />
171
+ )}
172
+ {activeTab === "Proposal Draft" && <ProposalDraft proposal={analysisResult?.proposal_draft ?? ""} />}
173
+ {activeTab === "Reports" && <Reports reportMarkdown={analysisResult?.report_markdown ?? ""} />}
174
+ {activeTab === "History" && <AnalysisHistory history={analysisHistory} />}
175
+ </main>
176
+ </div>
177
+ <footer className="mt-8 border-t border-slate-800 bg-navy/50 py-12 text-center">
178
+ <p className="text-xs font-bold uppercase tracking-[0.4em] text-slate-500 mb-2">
179
+ Architecting the Agentic Future
180
+ </p>
181
+ <p className="text-sm font-medium text-slate-400">
182
+ © 2026 <a href="https://rew.cl" target="_blank" className="text-cyan hover:text-sky transition-colors font-bold">REW 2026</a> | IA TECH PREMIUM
183
+ </p>
184
+ </footer>
185
+ </div>
186
+ );
187
+ }
frontend/components/AgentAnalysis.tsx ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import type { AnalysisResult, CompanyProfile, Tender } from "../lib/types";
5
+ import { uploadDocument } from "../lib/api";
6
+
7
+ type Props = {
8
+ tender: Tender | null;
9
+ companyProfile: CompanyProfile;
10
+ analysis: AnalysisResult | null;
11
+ onAnalyze: (documentText?: string) => Promise<void>;
12
+ };
13
+
14
+ const agents = [
15
+ { id: "legal", name: "Dra. Legal", role: "Compliance", avatar: "⚖️", color: "text-amber-400" },
16
+ { id: "tech", name: "Ing. Tech", role: "Architecture", avatar: "👨‍💻", color: "text-cyan" },
17
+ { id: "risk", name: "Sra. Estrategia", role: "ROI & Risk", avatar: "🕵️‍♀️", color: "text-purple-400" },
18
+ ];
19
+
20
+ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnalyze }: Props) {
21
+ const [approved, setApproved] = useState(false);
22
+ const [isRunning, setIsRunning] = useState(false);
23
+ const [file, setFile] = useState<File | null>(null);
24
+ const [isUploading, setIsUploading] = useState(false);
25
+ const [documentText, setDocumentText] = useState<string | "">("");
26
+
27
+ const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
28
+ if (event.target.files && event.target.files[0]) {
29
+ setFile(event.target.files[0]);
30
+ }
31
+ };
32
+
33
+ const handleAnalyzeClick = async () => {
34
+ if (!approved || !tender) return;
35
+ setIsRunning(true);
36
+ let extractedText = documentText;
37
+ if (file && !extractedText) {
38
+ setIsUploading(true);
39
+ try {
40
+ const uploadResult = await uploadDocument(file);
41
+ extractedText = uploadResult.text;
42
+ setDocumentText(extractedText);
43
+ } catch (error) {
44
+ console.error("Error uploading document:", error);
45
+ } finally {
46
+ setIsUploading(false);
47
+ }
48
+ }
49
+ await onAnalyze(extractedText);
50
+ setIsRunning(false);
51
+ };
52
+
53
+ return (
54
+ <div className="space-y-8">
55
+ {/* Tender Header Card */}
56
+ <div className="relative overflow-hidden rounded-3xl border border-slate-800 bg-slate-900/40 p-8 shadow-2xl">
57
+ <div className="absolute -right-20 -top-20 h-64 w-64 rounded-full bg-cyan/5 blur-[100px]" />
58
+ <div className="relative z-10 flex flex-col gap-8 lg:flex-row lg:items-center lg:justify-between">
59
+ <div className="max-w-3xl">
60
+ <div className="flex items-center gap-3 mb-4">
61
+ <span className="rounded-full bg-cyan/10 px-3 py-1 text-[10px] font-bold uppercase tracking-widest text-cyan">Licitación Seleccionada</span>
62
+ <span className="text-xs text-slate-500 font-mono">{tender?.code}</span>
63
+ </div>
64
+ <h2 className="text-4xl font-bold text-white tracking-tight leading-tight">{tender?.name ?? "Esperando selección..."}</h2>
65
+ <p className="mt-4 text-slate-400 font-medium">{tender?.buyer ?? "Busca una oportunidad en Tender Search para iniciar la mesa de expertos."}</p>
66
+
67
+ {tender && (
68
+ <div className="mt-8 flex flex-wrap gap-4">
69
+ <div className="rounded-2xl bg-slate-950/50 p-4 border border-slate-800/50">
70
+ <p className="text-[10px] uppercase text-slate-500 font-bold mb-1">Monto Estimado</p>
71
+ <p className="text-lg font-semibold text-white">
72
+ {tender.estimated_amount ? new Intl.NumberFormat("es-CL", { style: "currency", currency: "CLP", maximumFractionDigits: 0 }).format(tender.estimated_amount) : "No declarado"}
73
+ </p>
74
+ </div>
75
+ <div className="rounded-2xl bg-slate-950/50 p-4 border border-slate-800/50">
76
+ <p className="text-[10px] uppercase text-slate-500 font-bold mb-1">Cierre Técnico</p>
77
+ <p className="text-lg font-semibold text-white">{tender.closing_date}</p>
78
+ </div>
79
+ </div>
80
+ )}
81
+ </div>
82
+
83
+ <div className="flex flex-col gap-4 lg:w-72">
84
+ <div className="rounded-2xl border border-slate-800 bg-slate-950 p-6">
85
+ <h4 className="text-xs font-bold uppercase text-slate-400 mb-4 tracking-tighter">Bases Técnicas (PDF)</h4>
86
+ <input type="file" accept=".pdf" onChange={handleFileChange} className="w-full text-xs text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-[10px] file:font-bold file:bg-slate-800 file:text-cyan hover:file:bg-slate-700 transition cursor-pointer" />
87
+ {file && <p className="mt-2 text-[10px] text-green-400">✓ Archivo cargado correctamente</p>}
88
+ </div>
89
+
90
+ <label className="flex items-center gap-3 p-3 rounded-2xl bg-slate-950/50 cursor-pointer hover:bg-slate-950 transition">
91
+ <input type="checkbox" checked={approved} onChange={(e) => setApproved(e.target.checked)} className="h-5 w-5 rounded border-slate-700 bg-slate-950 text-cyan outline-none" />
92
+ <span className="text-[11px] font-semibold text-slate-300">Autorizar mesa de expertos</span>
93
+ </label>
94
+
95
+ <button
96
+ onClick={handleAnalyzeClick}
97
+ disabled={!tender || !approved || isRunning || isUploading}
98
+ className="w-full rounded-2xl bg-cyan py-5 font-bold text-slate-950 transition hover:bg-sky disabled:opacity-30 disabled:cursor-not-allowed shadow-[0_0_20px_rgba(6,182,212,0.15)]"
99
+ >
100
+ {isUploading ? "Cargando Bases..." : isRunning ? "Agentes Debatiendo..." : "Iniciar Sesión Agéntica"}
101
+ </button>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ {/* Agents Row (War Room) */}
107
+ <div className="grid gap-4 md:grid-cols-3">
108
+ {agents.map((agent) => (
109
+ <div key={agent.id} className={`rounded-3xl border border-slate-800 bg-slate-900/20 p-6 flex items-center gap-4 transition duration-500 ${isRunning ? 'animate-pulse' : ''} ${analysis ? 'border-slate-700 bg-slate-900/40' : ''}`}>
110
+ <div className="text-3xl grayscale-[0.5] hover:grayscale-0 transition">{agent.avatar}</div>
111
+ <div>
112
+ <div className={`text-xs font-bold uppercase tracking-widest ${agent.color}`}>{agent.role}</div>
113
+ <div className="text-sm font-semibold text-white">{agent.name}</div>
114
+ {analysis && <div className="text-[10px] text-green-400 mt-1">Conclusión generada ✓</div>}
115
+ </div>
116
+ </div>
117
+ ))}
118
+ </div>
119
+
120
+ {/* Analysis Results View */}
121
+ {analysis && (
122
+ <div className="grid gap-8 lg:grid-cols-12 animate-in fade-in slide-in-from-bottom-8 duration-1000">
123
+ {/* Main Fit Score & Summary */}
124
+ <div className="lg:col-span-8 space-y-8">
125
+ <div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-8 shadow-inner">
126
+ <div className="flex items-start justify-between">
127
+ <div>
128
+ <div className="text-[11px] font-bold uppercase tracking-[0.3em] text-cyan mb-2">Consenso de la Mesa</div>
129
+ <h3 className="text-5xl font-black text-white">{analysis.fit_score}% <span className="text-2xl font-normal text-slate-500">Fit Score</span></h3>
130
+ </div>
131
+ <div className={`rounded-2xl px-4 py-2 text-xs font-bold uppercase ${analysis.decision === 'Recommended' ? 'bg-green-500/20 text-green-400' : 'bg-amber-500/20 text-amber-400'}`}>
132
+ {analysis.decision}
133
+ </div>
134
+ </div>
135
+ <div className="mt-8 prose prose-invert max-w-none">
136
+ <p className="text-slate-300 text-lg leading-relaxed italic border-l-4 border-cyan pl-6">{analysis.executive_summary}</p>
137
+ </div>
138
+ </div>
139
+
140
+ <div className="grid gap-6 md:grid-cols-2">
141
+ <div className="rounded-3xl border border-slate-800 bg-slate-900/40 p-6">
142
+ <h4 className="text-[11px] font-bold uppercase tracking-widest text-amber-400 mb-6">Gaps de Cumplimiento (Legal)</h4>
143
+ <ul className="space-y-4">
144
+ {analysis.compliance_gaps.map((gap, i) => (
145
+ <li key={i} className="flex gap-3 text-sm text-slate-300">
146
+ <span className="text-amber-400">⚠</span> {gap}
147
+ </li>
148
+ ))}
149
+ </ul>
150
+ </div>
151
+ <div className="rounded-3xl border border-slate-800 bg-slate-900/40 p-6">
152
+ <h4 className="text-[11px] font-bold uppercase tracking-widest text-cyan mb-6">Requerimientos Clave (Técnico)</h4>
153
+ <ul className="space-y-4">
154
+ {analysis.key_requirements.map((req, i) => (
155
+ <li key={i} className="flex gap-3 text-sm text-slate-300">
156
+ <span className="text-cyan">▹</span> {req}
157
+ </li>
158
+ ))}
159
+ </ul>
160
+ </div>
161
+ </div>
162
+
163
+ <div className="rounded-3xl border border-slate-800 bg-slate-900/40 p-8">
164
+ <h4 className="text-[11px] font-bold uppercase tracking-widest text-purple-400 mb-8 text-center">Matriz de Riesgos Identificados</h4>
165
+ <div className="grid gap-4 md:grid-cols-2">
166
+ {analysis.risks.map((risk, i) => (
167
+ <div key={i} className="group rounded-2xl bg-slate-950 p-5 border border-slate-800 hover:border-purple-500/50 transition">
168
+ <div className="flex items-center justify-between mb-3">
169
+ <span className="font-bold text-white group-hover:text-purple-400 transition">{risk.title}</span>
170
+ <span className={`text-[9px] font-black px-2 py-0.5 rounded uppercase ${risk.severity === 'High' ? 'bg-red-500/20 text-red-500' : 'bg-slate-800 text-slate-400'}`}>{risk.severity}</span>
171
+ </div>
172
+ <p className="text-xs text-slate-500 leading-relaxed">{risk.explanation}</p>
173
+ </div>
174
+ ))}
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+ {/* Right Col: Audit Trail & Conversation */}
180
+ <div className="lg:col-span-4 space-y-6">
181
+ <div className="rounded-3xl border border-slate-800 bg-slate-950 p-6 flex flex-col h-full shadow-2xl">
182
+ <div className="flex items-center gap-3 mb-8 border-b border-slate-800 pb-4">
183
+ <div className="h-2 w-2 rounded-full bg-cyan animate-pulse shadow-[0_0_10px_rgba(6,182,212,0.5)]" />
184
+ <h4 className="text-[10px] font-bold uppercase tracking-widest text-slate-400">Audit Trail & Agents Log</h4>
185
+ </div>
186
+ <div className="flex-1 space-y-6 overflow-y-auto max-h-[600px] pr-2 custom-scrollbar">
187
+ {analysis.audit_log?.map((log, i) => (
188
+ <div key={i} className="flex gap-4 group">
189
+ <div className="flex flex-col items-center">
190
+ <div className="h-6 w-6 rounded-full bg-slate-900 flex items-center justify-center text-[10px] border border-slate-800 group-hover:border-cyan transition">🤖</div>
191
+ {i < (analysis.audit_log?.length ?? 0) - 1 && <div className="w-[1px] flex-1 bg-slate-800 my-2" />}
192
+ </div>
193
+ <div className="pb-4">
194
+ <p className="text-xs text-slate-400 leading-relaxed group-hover:text-slate-200 transition">{log}</p>
195
+ </div>
196
+ </div>
197
+ ))}
198
+ </div>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ )}
203
+
204
+ {!analysis && !isRunning && (
205
+ <div className="rounded-3xl border border-dashed border-slate-800 bg-slate-950/20 p-20 flex flex-col items-center justify-center text-center">
206
+ <div className="text-6xl mb-6 grayscale opacity-20">🤖⚖️👨‍💻🕵️‍♀️</div>
207
+ <h3 className="text-xl font-bold text-slate-500">Mesa de expertos inactiva</h3>
208
+ <p className="mt-2 text-sm text-slate-600 max-w-sm">Selecciona una licitación y carga las bases para iniciar el debate agéntico entre nuestros especialistas.</p>
209
+ </div>
210
+ )}
211
+ </div>
212
+ );
213
+ }
frontend/components/AnalysisHistory.tsx ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { AnalysisHistoryItem } from "../lib/types";
2
+
3
+ type Props = {
4
+ history: AnalysisHistoryItem[];
5
+ };
6
+
7
+ export default function AnalysisHistory({ history }: Props) {
8
+ if (!history.length) {
9
+ return (
10
+ <div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-8 text-slate-400">
11
+ No hay historial de análisis todavía. Ejecuta un análisis para comenzar a guardarlo.
12
+ </div>
13
+ );
14
+ }
15
+
16
+ return (
17
+ <div className="space-y-6">
18
+ <div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-6">
19
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
20
+ <div>
21
+ <h2 className="text-2xl font-semibold text-white">Analysis History</h2>
22
+ <p className="mt-2 text-slate-400">Revisa los análisis previos generados por el agente para auditoría y seguimiento.</p>
23
+ </div>
24
+ </div>
25
+ </div>
26
+
27
+ <div className="space-y-4">
28
+ {history.map((item) => (
29
+ <div key={`${item.tender_code}-${item.analyzed_at}`} className="rounded-3xl border border-slate-800 bg-slate-900/80 p-6">
30
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
31
+ <div>
32
+ <div className="text-sm text-slate-500">Tender</div>
33
+ <div className="text-lg font-semibold text-white">{item.tender_name}</div>
34
+ <div className="text-sm text-slate-400">Código: {item.tender_code}</div>
35
+ </div>
36
+ <div className="text-right">
37
+ <div className="text-sm uppercase tracking-[0.25em] text-cyan-300/80">Fit score</div>
38
+ <div className="mt-2 text-3xl font-semibold text-white">{item.analysis.fit_score}%</div>
39
+ <div className="mt-1 text-sm text-slate-400">{new Date(item.analyzed_at).toLocaleString()}</div>
40
+ </div>
41
+ </div>
42
+ <div className="mt-6 grid gap-4 md:grid-cols-3">
43
+ <div className="rounded-2xl bg-slate-950/80 p-4 text-sm text-slate-300">
44
+ <div className="font-semibold text-white">Decision</div>
45
+ <div className="mt-2">{item.analysis.decision}</div>
46
+ </div>
47
+ <div className="rounded-2xl bg-slate-950/80 p-4 text-sm text-slate-300">
48
+ <div className="font-semibold text-white">Key Requirements</div>
49
+ <div className="mt-2">{item.analysis.key_requirements.length}</div>
50
+ </div>
51
+ <div className="rounded-2xl bg-slate-950/80 p-4 text-sm text-slate-300">
52
+ <div className="font-semibold text-white">Risks</div>
53
+ <div className="mt-2">{item.analysis.risks.length}</div>
54
+ </div>
55
+ </div>
56
+ {item.analysis.audit_log?.length ? (
57
+ <div className="mt-6 rounded-2xl border border-amber-900/40 bg-amber-950/10 p-4 text-sm text-slate-200">
58
+ <div className="font-semibold text-amber-200">Audit trail</div>
59
+ <ul className="mt-3 list-disc space-y-2 pl-5">
60
+ {item.analysis.audit_log.map((entry, index) => (
61
+ <li key={index}>{entry}</li>
62
+ ))}
63
+ </ul>
64
+ </div>
65
+ ) : null}
66
+ </div>
67
+ ))}
68
+ </div>
69
+ </div>
70
+ );
71
+ }
frontend/components/BrandLoader.tsx ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ export default function BrandLoader() {
4
+ return (
5
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-navy/90 backdrop-blur-xl animate-in fade-in duration-500">
6
+ <div className="relative flex flex-col items-center">
7
+ {/* Outer Glowing Ring */}
8
+ <div className="absolute h-64 w-64 rounded-full border-2 border-cyan/20 animate-ping opacity-20" />
9
+
10
+ {/* Rotating Spiral / Radar */}
11
+ <div className="relative h-48 w-48">
12
+ {/* Main Ring */}
13
+ <div className="absolute inset-0 rounded-full border-[3px] border-t-cyan border-r-transparent border-b-sky/30 border-l-transparent animate-spin duration-[1.5s]" />
14
+
15
+ {/* Inner Fast Ring */}
16
+ <div className="absolute inset-4 rounded-full border border-dashed border-cyan/40 animate-spin-reverse duration-[3s]" />
17
+
18
+ {/* The "Mountain" Brand Shape (SVG) */}
19
+ <div className="absolute inset-0 flex items-center justify-center">
20
+ <svg width="60" height="60" viewBox="0 0 100 100" className="drop-shadow-[0_0_15px_rgba(6,182,212,0.8)]">
21
+ <path
22
+ d="M50 15 L90 85 L10 85 Z"
23
+ fill="none"
24
+ stroke="currentColor"
25
+ strokeWidth="4"
26
+ className="text-cyan animate-pulse"
27
+ />
28
+ <path
29
+ d="M50 35 L75 85 L25 85 Z"
30
+ fill="none"
31
+ stroke="currentColor"
32
+ strokeWidth="3"
33
+ className="text-sky/60"
34
+ />
35
+ </svg>
36
+ </div>
37
+
38
+ {/* Scanning Line */}
39
+ <div className="absolute inset-0 rounded-full bg-gradient-to-t from-cyan/20 to-transparent animate-pulse h-1/2 top-0 origin-bottom" />
40
+ </div>
41
+
42
+ {/* Text Status */}
43
+ <div className="mt-12 text-center space-y-2">
44
+ <h3 className="text-xl font-bold tracking-[0.2em] text-white uppercase italic">
45
+ Neural Syncing
46
+ </h3>
47
+ <div className="flex items-center gap-1 justify-center">
48
+ <span className="h-1 w-1 bg-cyan rounded-full animate-bounce delay-75" />
49
+ <span className="h-1 w-1 bg-cyan rounded-full animate-bounce delay-150" />
50
+ <span className="h-1 w-1 bg-cyan rounded-full animate-bounce delay-300" />
51
+ </div>
52
+ <p className="text-[10px] text-slate-500 uppercase tracking-widest mt-4">
53
+ Connecting to Mercado Público Real-Time API...
54
+ </p>
55
+ </div>
56
+ </div>
57
+
58
+ <style jsx>{`
59
+ @keyframes spin-reverse {
60
+ from { transform: rotate(360deg); }
61
+ to { transform: rotate(0deg); }
62
+ }
63
+ .animate-spin-reverse {
64
+ animation: spin-reverse 3s linear infinite;
65
+ }
66
+ `}</style>
67
+ </div>
68
+ );
69
+ }
frontend/components/CompanyProfile.tsx ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import type { CompanyProfile as CompanyProfileType } from "../lib/types";
5
+
6
+ type Props = {
7
+ profile: CompanyProfileType;
8
+ onSave: (profile: CompanyProfileType) => void;
9
+ };
10
+
11
+ export default function CompanyProfile({ profile, onSave }: Props) {
12
+ const [form, setForm] = useState(profile);
13
+
14
+ const serviceValue = useMemo(() => form.services.join(", "), [form.services]);
15
+ const certValue = useMemo(() => form.certifications?.join(", "), [form.certifications]);
16
+ const regionValue = useMemo(() => form.regions?.join(", "), [form.regions]);
17
+ const docsValue = useMemo(() => form.documents_available?.join(", "), [form.documents_available]);
18
+
19
+ const handleSave = () => {
20
+ onSave({
21
+ ...form,
22
+ services: serviceValue.split(",").map((item) => item.trim()).filter(Boolean),
23
+ certifications: certValue.split(",").map((item) => item.trim()).filter(Boolean),
24
+ regions: regionValue.split(",").map((item) => item.trim()).filter(Boolean),
25
+ documents_available: docsValue.split(",").map((item) => item.trim()).filter(Boolean),
26
+ });
27
+ };
28
+
29
+ return (
30
+ <div className="space-y-6">
31
+ <div className="grid gap-6 lg:grid-cols-2">
32
+ <label className="block rounded-3xl border border-slate-800 bg-slate-950/80 p-5">
33
+ <span className="text-sm text-slate-400">Company name</span>
34
+ <input
35
+ value={form.name}
36
+ onChange={(event) => setForm({ ...form, name: event.target.value })}
37
+ className="mt-3 w-full rounded-2xl border border-slate-800 bg-slate-900 px-4 py-3 text-white outline-none"
38
+ />
39
+ </label>
40
+ <label className="block rounded-3xl border border-slate-800 bg-slate-950/80 p-5">
41
+ <span className="text-sm text-slate-400">Industry</span>
42
+ <input
43
+ value={form.industry}
44
+ onChange={(event) => setForm({ ...form, industry: event.target.value })}
45
+ className="mt-3 w-full rounded-2xl border border-slate-800 bg-slate-900 px-4 py-3 text-white outline-none"
46
+ />
47
+ </label>
48
+ </div>
49
+
50
+ <div className="grid gap-6 lg:grid-cols-2">
51
+ <label className="block rounded-3xl border border-slate-800 bg-slate-950/80 p-5">
52
+ <span className="text-sm text-slate-400">Services</span>
53
+ <input
54
+ value={serviceValue}
55
+ onChange={(event) => setForm({ ...form, services: event.target.value.split(",").map((item) => item.trim()).filter(Boolean) })}
56
+ placeholder="AI automation, web apps, data dashboards"
57
+ className="mt-3 w-full rounded-2xl border border-slate-800 bg-slate-900 px-4 py-3 text-white outline-none"
58
+ />
59
+ </label>
60
+ <label className="block rounded-3xl border border-slate-800 bg-slate-950/80 p-5">
61
+ <span className="text-sm text-slate-400">Experience</span>
62
+ <input
63
+ value={form.experience}
64
+ onChange={(event) => setForm({ ...form, experience: event.target.value })}
65
+ className="mt-3 w-full rounded-2xl border border-slate-800 bg-slate-900 px-4 py-3 text-white outline-none"
66
+ />
67
+ </label>
68
+ </div>
69
+
70
+ <div className="grid gap-6 lg:grid-cols-2">
71
+ <label className="block rounded-3xl border border-slate-800 bg-slate-950/80 p-5">
72
+ <span className="text-sm text-slate-400">Certifications</span>
73
+ <input
74
+ value={certValue}
75
+ onChange={(event) => setForm({ ...form, certifications: event.target.value.split(",").map((item) => item.trim()).filter(Boolean) })}
76
+ placeholder="ISO 9001, ISO 27001"
77
+ className="mt-3 w-full rounded-2xl border border-slate-800 bg-slate-900 px-4 py-3 text-white outline-none"
78
+ />
79
+ </label>
80
+ <label className="block rounded-3xl border border-slate-800 bg-slate-950/80 p-5">
81
+ <span className="text-sm text-slate-400">Regions</span>
82
+ <input
83
+ value={regionValue}
84
+ onChange={(event) => setForm({ ...form, regions: event.target.value.split(",").map((item) => item.trim()).filter(Boolean) })}
85
+ placeholder="Metropolitana, Valparaíso"
86
+ className="mt-3 w-full rounded-2xl border border-slate-800 bg-slate-900 px-4 py-3 text-white outline-none"
87
+ />
88
+ </label>
89
+ </div>
90
+
91
+ <label className="block rounded-3xl border border-slate-800 bg-slate-950/80 p-5">
92
+ <span className="text-sm text-slate-400">Documents available</span>
93
+ <input
94
+ value={docsValue}
95
+ onChange={(event) => setForm({ ...form, documents_available: event.target.value.split(",").map((item) => item.trim()).filter(Boolean) })}
96
+ placeholder="RUT, Portfolio, Financial statements"
97
+ className="mt-3 w-full rounded-2xl border border-slate-800 bg-slate-900 px-4 py-3 text-white outline-none"
98
+ />
99
+ </label>
100
+
101
+ <button
102
+ type="button"
103
+ onClick={handleSave}
104
+ className="rounded-3xl bg-cyan-500 px-6 py-4 font-semibold text-slate-950 transition hover:bg-cyan-400"
105
+ >
106
+ Save Profile
107
+ </button>
108
+ </div>
109
+ );
110
+ }
frontend/components/Dashboard.tsx ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import StatCard from "./StatCard";
2
+ import { Tender } from "../lib/types";
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import BrandLoader from "./BrandLoader";
5
+ import { searchTenders, fetchDbStatus } from "../lib/api";
6
+
7
+ type Props = {
8
+ tendersFound: number;
9
+ recommendedOpportunities: number;
10
+ highRiskItems: number;
11
+ reportsGenerated: number;
12
+ tenders: Tender[];
13
+ };
14
+
15
+ export default function Dashboard({
16
+ tendersFound,
17
+ recommendedOpportunities,
18
+ highRiskItems,
19
+ reportsGenerated,
20
+ tenders
21
+ }: Props) {
22
+ const [isSyncing, setIsSyncing] = useState(false);
23
+ const [dbStatus, setDbStatus] = useState<any>(null);
24
+
25
+ useEffect(() => {
26
+ async function loadStatus() {
27
+ const status = await fetchDbStatus();
28
+ setDbStatus(status);
29
+ }
30
+ loadStatus();
31
+ }, [tenders]);
32
+
33
+ const handleGlobalSync = async () => {
34
+ setIsSyncing(true);
35
+ try {
36
+ // We trigger a search for "software" or empty to populate initial DB
37
+ await searchTenders({ keyword: "" });
38
+ // Small delay for the "wow" effect duration
39
+ await new Promise(r => setTimeout(r, 2500));
40
+ window.location.reload(); // Refresh to show new counts
41
+ } catch (e) {
42
+ console.error(e);
43
+ } finally {
44
+ setIsSyncing(false);
45
+ }
46
+ };
47
+
48
+ const sectorDistribution = useMemo(() => {
49
+ const counts: Record<string, number> = {};
50
+ tenders.forEach(t => {
51
+ const sector = t.sector || "General";
52
+ counts[sector] = (counts[sector] || 0) + 1;
53
+ });
54
+ return Object.entries(counts)
55
+ .sort((a, b) => b[1] - a[1])
56
+ .slice(0, 5);
57
+ }, [tenders]);
58
+
59
+ const regionDistribution = useMemo(() => {
60
+ const counts: Record<string, number> = {};
61
+ tenders.forEach(t => {
62
+ const region = t.region || "Sin Región";
63
+ counts[region] = (counts[region] || 0) + 1;
64
+ });
65
+ return Object.entries(counts)
66
+ .sort((a, b) => b[1] - a[1])
67
+ .slice(0, 5);
68
+ }, [tenders]);
69
+
70
+ const deadlineStatus = useMemo(() => {
71
+ const now = new Date();
72
+ const status = {
73
+ urgent: 0,
74
+ near: 0,
75
+ far: 0
76
+ };
77
+ tenders.forEach(t => {
78
+ if (!t.closing_date) return;
79
+ const closing = new Date(t.closing_date);
80
+ const diffDays = Math.ceil((closing.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
81
+ if (diffDays < 7) status.urgent++;
82
+ else if (diffDays < 21) status.near++;
83
+ else status.far++;
84
+ });
85
+ return status;
86
+ }, [tenders]);
87
+
88
+ const totalAmount = useMemo(() => {
89
+ return tenders.reduce((acc, t) => acc + (t.estimated_amount || 0), 0);
90
+ }, [tenders]);
91
+
92
+ const formatAmount = (amount: number) => {
93
+ if (amount >= 1_000_000_000) {
94
+ return `$${(amount / 1_000_000_000).toFixed(1)}B`;
95
+ }
96
+ if (amount >= 1_000_000) {
97
+ return `$${(amount / 1_000_000).toFixed(1)}M`;
98
+ }
99
+ return new Intl.NumberFormat("es-CL", {
100
+ style: "currency",
101
+ currency: "CLP",
102
+ maximumFractionDigits: 0
103
+ }).format(amount);
104
+ };
105
+
106
+ return (
107
+ <div className="space-y-8">
108
+ {isSyncing && <BrandLoader />}
109
+ <div className="flex items-center justify-between gap-4">
110
+ <div>
111
+ <p className="text-sm uppercase tracking-[0.35em] text-cyan/80">Resumen ejecutivo</p>
112
+ <h2 className="mt-3 text-4xl font-semibold text-white">AndesOps AI</h2>
113
+ <p className="mt-4 max-w-2xl text-slate-300">
114
+ Inteligencia de mercado y análisis agéntico para licitaciones públicas en Chile.
115
+ </p>
116
+ </div>
117
+ <button
118
+ onClick={handleGlobalSync}
119
+ className="group relative flex items-center gap-3 overflow-hidden rounded-2xl bg-cyan px-6 py-4 font-bold text-slate-950 transition hover:bg-sky hover:scale-[1.02] active:scale-[0.98]"
120
+ >
121
+ <span className="relative z-10">Sync Global Pipeline</span>
122
+ <span className="text-xl group-hover:rotate-180 transition-transform duration-700">🔄</span>
123
+ <div className="absolute inset-0 bg-gradient-to-r from-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
124
+ </button>
125
+ </div>
126
+
127
+ <div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
128
+ <StatCard title="Tenders Found" value={tendersFound} subtitle="Oportunidades activas" />
129
+ <StatCard title="Recommended" value={recommendedOpportunities} subtitle="Fit score > 80%" />
130
+ <StatCard title="High Risk" value={highRiskItems} subtitle="Riesgos críticos" />
131
+ <StatCard title="Total Pipeline" value={formatAmount(totalAmount)} subtitle="Monto total proyectado" />
132
+ </div>
133
+
134
+ <div className="grid gap-6 lg:grid-cols-3">
135
+ {/* Sector Distribution */}
136
+ <div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-6">
137
+ <h3 className="text-sm uppercase tracking-widest text-slate-400 mb-6 font-semibold">Sectores (Rubros)</h3>
138
+ <div className="space-y-4">
139
+ {sectorDistribution.length > 0 ? (
140
+ sectorDistribution.map(([sector, count]) => (
141
+ <div key={sector}>
142
+ <div className="flex justify-between text-xs mb-1">
143
+ <span className="text-slate-300">{sector}</span>
144
+ <span className="text-cyan font-semibold">{count}</span>
145
+ </div>
146
+ <div className="h-1.5 w-full bg-slate-900 rounded-full overflow-hidden">
147
+ <div
148
+ className="h-full bg-cyan transition-all duration-500"
149
+ style={{ width: `${(count / tenders.length) * 100}%` }}
150
+ />
151
+ </div>
152
+ </div>
153
+ ))
154
+ ) : (
155
+ <p className="text-slate-500 text-xs italic">Sin datos disponibles.</p>
156
+ )}
157
+ </div>
158
+ </div>
159
+
160
+ {/* Region Distribution */}
161
+ <div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-6">
162
+ <h3 className="text-sm uppercase tracking-widest text-slate-400 mb-6 font-semibold">Distribución Regional</h3>
163
+ <div className="space-y-4">
164
+ {regionDistribution.length > 0 ? (
165
+ regionDistribution.map(([region, count]) => (
166
+ <div key={region}>
167
+ <div className="flex justify-between text-xs mb-1">
168
+ <span className="text-slate-300">{region}</span>
169
+ <span className="text-sky font-semibold">{count}</span>
170
+ </div>
171
+ <div className="h-1.5 w-full bg-slate-900 rounded-full overflow-hidden">
172
+ <div
173
+ className="h-full bg-sky transition-all duration-500"
174
+ style={{ width: `${(count / tenders.length) * 100}%` }}
175
+ />
176
+ </div>
177
+ </div>
178
+ ))
179
+ ) : (
180
+ <p className="text-slate-500 text-xs italic">Sin datos disponibles.</p>
181
+ )}
182
+ </div>
183
+ </div>
184
+
185
+ {/* Deadline Status */}
186
+ <div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-6">
187
+ <h3 className="text-sm uppercase tracking-widest text-slate-400 mb-6 font-semibold">Estado de Plazos</h3>
188
+ <div className="space-y-6 pt-2">
189
+ <div className="flex items-center gap-4">
190
+ <div className="h-12 w-1.5 bg-red-500 rounded-full" />
191
+ <div>
192
+ <div className="text-2xl font-bold text-white">{deadlineStatus.urgent}</div>
193
+ <div className="text-xs text-slate-500">Cierre en menos de 7 días</div>
194
+ </div>
195
+ </div>
196
+ <div className="flex items-center gap-4">
197
+ <div className="h-12 w-1.5 bg-amber-500 rounded-full" />
198
+ <div>
199
+ <div className="text-2xl font-bold text-white">{deadlineStatus.near}</div>
200
+ <div className="text-xs text-slate-500">Cierre en 7-21 días</div>
201
+ </div>
202
+ </div>
203
+ <div className="flex items-center gap-4">
204
+ <div className="h-12 w-1.5 bg-green-500 rounded-full" />
205
+ <div>
206
+ <div className="text-2xl font-bold text-white">{deadlineStatus.far}</div>
207
+ <div className="text-xs text-slate-500">Más de 21 días</div>
208
+ </div>
209
+ </div>
210
+ </div>
211
+ </div>
212
+
213
+ {/* Database Status Table (New) */}
214
+ <div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-6 flex flex-col justify-between">
215
+ <div>
216
+ <h3 className="text-sm uppercase tracking-widest text-slate-400 mb-6 font-semibold">Data Integrity Monitor</h3>
217
+ <div className="overflow-hidden rounded-2xl border border-slate-800 bg-slate-900/30">
218
+ <table className="w-full text-left text-[10px]">
219
+ <thead className="bg-slate-800/50 text-slate-500 uppercase font-bold">
220
+ <tr>
221
+ <th className="px-4 py-2">Organismo Local</th>
222
+ <th className="px-4 py-2 text-right">Qty</th>
223
+ </tr>
224
+ </thead>
225
+ <tbody className="divide-y divide-slate-800/50">
226
+ {dbStatus?.top_buyers?.map((b: any, i: number) => (
227
+ <tr key={i} className="hover:bg-slate-800/30">
228
+ <td className="px-4 py-3 text-slate-300 truncate max-w-[120px]">{b.name}</td>
229
+ <td className="px-4 py-3 text-right text-cyan font-mono">{b.count}</td>
230
+ </tr>
231
+ ))}
232
+ {!dbStatus?.top_buyers?.length && (
233
+ <tr>
234
+ <td colSpan={2} className="px-4 py-6 text-center text-slate-600 italic">No local data found.</td>
235
+ </tr>
236
+ )}
237
+ </tbody>
238
+ </table>
239
+ </div>
240
+ </div>
241
+
242
+ <div className="mt-6 pt-6 border-t border-slate-800/50">
243
+ <div className="flex justify-between items-center text-[10px]">
244
+ <span className="text-slate-500 font-bold uppercase tracking-tighter">Total Local Tenders:</span>
245
+ <span className="text-white font-mono">{dbStatus?.total_records || 0}</span>
246
+ </div>
247
+ <div className="flex justify-between items-center text-[10px] mt-2">
248
+ <span className="text-slate-500 font-bold uppercase tracking-tighter">Last Pulse:</span>
249
+ <span className="text-cyan font-mono">{dbStatus?.last_sync ? new Date(dbStatus.last_sync).toLocaleTimeString() : 'Never'}</span>
250
+ </div>
251
+ </div>
252
+ </div>
253
+ </div>
254
+
255
+ <div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-6">
256
+ <h3 className="text-sm uppercase tracking-widest text-slate-400 mb-6 font-semibold">Actividad Reciente en Pipeline</h3>
257
+ <div className="space-y-3">
258
+ {tenders.length > 0 ? (
259
+ tenders.slice(0, 5).map((t) => (
260
+ // ... existing map logic ...
261
+ <div key={t.code} className="flex items-center justify-between p-4 rounded-2xl bg-slate-900/40 border border-slate-800/50 hover:bg-slate-900/60 transition group">
262
+ <div className="flex items-center gap-4 overflow-hidden">
263
+ <div className="h-10 w-10 flex-shrink-0 rounded-full bg-slate-800 flex items-center justify-center text-cyan group-hover:scale-110 transition">
264
+ {t.sector?.charAt(0) || "T"}
265
+ </div>
266
+ <div className="overflow-hidden">
267
+ <div className="text-sm font-medium text-white truncate">{t.name}</div>
268
+ <div className="text-xs text-slate-500 truncate">{t.buyer}</div>
269
+ </div>
270
+ </div>
271
+ <div className="flex items-center gap-6 text-right">
272
+ <div className="hidden sm:block">
273
+ <div className="text-xs text-slate-500">Región</div>
274
+ <div className="text-xs text-slate-300">{t.region || "N/A"}</div>
275
+ </div>
276
+ <div className="min-w-[80px]">
277
+ <div className="text-xs text-slate-500">Código</div>
278
+ <div className="text-xs font-mono text-cyan">{t.code}</div>
279
+ </div>
280
+ </div>
281
+ </div>
282
+ ))
283
+ ) : (
284
+ <div className="flex flex-col items-center justify-center py-10 text-center">
285
+ <p className="text-slate-500 text-sm italic mb-4">La base de datos local está vacía.</p>
286
+ <button
287
+ onClick={handleGlobalSync}
288
+ className="text-xs font-bold text-cyan border border-cyan/20 px-4 py-2 rounded-xl hover:bg-cyan/10 transition"
289
+ >
290
+ 📥 Sincronizar Data Real Ahora
291
+ </button>
292
+ </div>
293
+ )}
294
+ </div>
295
+ </div>
296
+ </div>
297
+ );
298
+ }
frontend/components/ProposalDraft.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ type Props = {
2
+ proposal: string;
3
+ };
4
+
5
+ export default function ProposalDraft({ proposal }: Props) {
6
+ return (
7
+ <div className="space-y-6">
8
+ <div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-6">
9
+ <h2 className="text-2xl font-semibold text-white">Proposal Draft</h2>
10
+ <p className="mt-3 text-slate-400">Este borrador se genera automáticamente una vez que se completa el análisis del tender.</p>
11
+ </div>
12
+ <div className="rounded-3xl border border-slate-800 bg-slate-900/80 p-6 text-slate-200">
13
+ <pre className="whitespace-pre-wrap break-words text-slate-100">{proposal || "Run agent analysis to generate a proposal draft."}</pre>
14
+ </div>
15
+ </div>
16
+ );
17
+ }
frontend/components/Reports.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+
5
+ type Props = {
6
+ reportMarkdown: string;
7
+ };
8
+
9
+ export default function Reports({ reportMarkdown }: Props) {
10
+ const [message, setMessage] = useState("");
11
+
12
+ const handleCopy = async () => {
13
+ try {
14
+ await navigator.clipboard.writeText(reportMarkdown);
15
+ setMessage("Report copied to clipboard.");
16
+ } catch {
17
+ setMessage("Unable to copy report.");
18
+ }
19
+ window.setTimeout(() => setMessage(""), 2000);
20
+ };
21
+
22
+ return (
23
+ <div className="space-y-6">
24
+ <div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
25
+ <div>
26
+ <h2 className="text-2xl font-semibold text-white">Export & Reports</h2>
27
+ <p className="mt-2 text-slate-400">Exporta el análisis completo en formato Markdown para tu equipo legal y comercial.</p>
28
+ </div>
29
+ <button
30
+ type="button"
31
+ onClick={handleCopy}
32
+ className="rounded-3xl bg-cyan px-8 py-4 font-semibold text-slate-950 transition hover:bg-sky shadow-lg shadow-cyan/10"
33
+ >
34
+ Copy Markdown Report
35
+ </button>
36
+ </div>
37
+
38
+ <div className="rounded-3xl border border-slate-800 bg-slate-900/40 p-8 text-slate-200 min-h-[400px] shadow-inner">
39
+ {reportMarkdown ? (
40
+ <div className="prose prose-invert max-w-none">
41
+ <pre className="whitespace-pre-wrap break-words text-slate-100 font-mono text-sm leading-relaxed">{reportMarkdown}</pre>
42
+ </div>
43
+ ) : (
44
+ <div className="flex flex-col items-center justify-center h-full text-slate-500 py-20">
45
+ <div className="text-4xl mb-4">📋</div>
46
+ <p>Generate an analysis in 'Agent Analysis' to preview the report here.</p>
47
+ </div>
48
+ )}
49
+ </div>
50
+ {message && (
51
+ <div className="fixed bottom-8 right-8 rounded-2xl bg-cyan px-6 py-3 text-sm font-semibold text-slate-950 shadow-xl transition-all animate-bounce">
52
+ {message}
53
+ </div>
54
+ )}
55
+ </div>
56
+ );
57
+ }
frontend/components/Sidebar.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import type { Dispatch, SetStateAction } from "react";
4
+
5
+ type SidebarTab =
6
+ | "Dashboard"
7
+ | "Tender Search"
8
+ | "Company Profile"
9
+ | "Agent Analysis"
10
+ | "Proposal Draft"
11
+ | "Reports"
12
+ | "History";
13
+
14
+ type Props = {
15
+ tabs: readonly SidebarTab[];
16
+ activeTab: SidebarTab;
17
+ onTabSelect: Dispatch<SetStateAction<SidebarTab>>;
18
+ status: string;
19
+ };
20
+
21
+ export default function Sidebar({ tabs, activeTab, onTabSelect, status }: Props) {
22
+ return (
23
+ <aside className="w-80 rounded-3xl border border-slate-800 bg-[#081822] p-6 text-slate-100 shadow-xl shadow-slate-900/20">
24
+ <div className="mb-8">
25
+ <div className="text-sm uppercase tracking-[0.3em] text-cyan-300/80">AndesOps AI</div>
26
+ <h1 className="mt-4 text-3xl font-semibold text-white">Enterprise Tender Intelligence</h1>
27
+ <p className="mt-3 text-slate-400">Chile-focused procurement insights for enterprise teams.</p>
28
+ </div>
29
+ <div className="space-y-2">
30
+ {tabs.map((tab) => {
31
+ const tabSlug = tab.toLowerCase().replace(/ /g, "_");
32
+ return (
33
+ <a
34
+ key={tab}
35
+ href={`?tab=${tabSlug}`}
36
+ onClick={(e) => {
37
+ e.preventDefault();
38
+ onTabSelect(tab);
39
+ window.history.pushState({}, '', `?tab=${tabSlug}`);
40
+ }}
41
+ className={`flex w-full items-center justify-between rounded-2xl px-4 py-3 text-left transition ${
42
+ activeTab === tab
43
+ ? "bg-gradient-to-r from-cyan/20 to-slate-700 text-white shadow-inner"
44
+ : "text-slate-300 hover:bg-slate-800/80"
45
+ }`}
46
+ >
47
+ <span>{tab}</span>
48
+ {activeTab === tab && <span className="text-cyan-300">●</span>}
49
+ </a>
50
+ );
51
+ })}
52
+ </div>
53
+ <div className="mt-8 rounded-2xl bg-slate-900/70 p-4 text-sm text-slate-300">
54
+ <div className="font-semibold text-slate-100">Conexión</div>
55
+ <div className="mt-2 text-cyan-300">{status === "connected" ? "Backend disponible" : "Conectando..."}</div>
56
+ </div>
57
+ </aside>
58
+ );
59
+ }
frontend/components/StatCard.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ type Props = {
2
+ title: string;
3
+ value: string | number;
4
+ subtitle: string;
5
+ };
6
+
7
+ export default function StatCard({ title, value, subtitle }: Props) {
8
+ return (
9
+ <div className="rounded-3xl border border-slate-800 bg-slate-950/80 p-6 shadow-lg shadow-slate-900/10 hover:border-slate-700 transition-colors group">
10
+ <div className="text-[10px] uppercase tracking-[0.3em] font-bold text-slate-500 group-hover:text-cyan transition-colors">{title}</div>
11
+ <div className="mt-4 text-3xl xl:text-4xl font-bold text-white tracking-tight truncate">
12
+ {value}
13
+ </div>
14
+ <p className="mt-2 text-xs font-medium text-slate-400">{subtitle}</p>
15
+ </div>
16
+ );
17
+ }
frontend/components/TenderSearch.tsx ADDED
@@ -0,0 +1,414 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Fragment, useMemo, useState, useEffect } from "react";
4
+ import BrandLoader from "./BrandLoader";
5
+ import type { Tender } from "../lib/types";
6
+
7
+ type Props = {
8
+ tenders: Tender[];
9
+ onSearch: (params: { keyword?: string; buyer_code?: string; provider_code?: string; date?: string }) => void;
10
+ onAnalyze: (tender: Tender) => void;
11
+ forceShowFollowed?: boolean;
12
+ };
13
+
14
+ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFollowed = false }: Props) {
15
+ const [keyword, setKeyword] = useState("");
16
+ const [buyerCode, setBuyerCode] = useState("");
17
+ const [providerCode, setProviderCode] = useState("");
18
+ const [searchDate, setSearchDate] = useState("");
19
+
20
+ const [searchMode, setSearchMode] = useState<"keyword" | "intelligence">("keyword");
21
+ const [expandedTenderCodes, setExpandedTenderCodes] = useState<string[]>([]);
22
+ const [followedCodes, setFollowedCodes] = useState<string[]>(() => {
23
+ if (typeof window !== 'undefined') {
24
+ const saved = localStorage.getItem('andes_followed_codes');
25
+ return saved ? JSON.parse(saved) : [];
26
+ }
27
+ return [];
28
+ });
29
+ const [showOnlyFollowed, setShowOnlyFollowed] = useState(forceShowFollowed);
30
+ const [isLoading, setIsLoading] = useState(false);
31
+
32
+ // Sync with forceShowFollowed prop
33
+ useEffect(() => {
34
+ if (forceShowFollowed) setShowOnlyFollowed(true);
35
+ }, [forceShowFollowed]);
36
+
37
+ // Persistence effect
38
+ useEffect(() => {
39
+ localStorage.setItem('andes_followed_codes', JSON.stringify(followedCodes));
40
+ }, [followedCodes]);
41
+
42
+ const toggleExpanded = (code: string) => {
43
+ setExpandedTenderCodes((current) =>
44
+ current.includes(code) ? current.filter((value) => value !== code) : [...current, code]
45
+ );
46
+ };
47
+
48
+ const toggleFollow = (code: string) => {
49
+ setFollowedCodes(prev =>
50
+ prev.includes(code) ? prev.filter(c => c !== code) : [...prev, code]
51
+ );
52
+ };
53
+
54
+ const handleSearchClick = async (page = 1) => {
55
+ setShowOnlyFollowed(false); // Reset filter on new search
56
+ setIsLoading(true);
57
+ setCurrentPage(page);
58
+ try {
59
+ if (searchMode === "keyword") {
60
+ await onSearch({ keyword, skip: (page - 1) * itemsPerPage, limit: itemsPerPage });
61
+ } else {
62
+ await onSearch({
63
+ buyer_code: buyerCode,
64
+ provider_code: providerCode,
65
+ date: searchDate,
66
+ skip: (page - 1) * itemsPerPage,
67
+ limit: itemsPerPage
68
+ });
69
+ }
70
+ } finally {
71
+ setIsLoading(false);
72
+ }
73
+ };
74
+
75
+ const filteredTenders = useMemo(() => {
76
+ if (showOnlyFollowed) {
77
+ return tenders.filter(t => followedCodes.includes(t.code));
78
+ }
79
+ return tenders;
80
+ }, [tenders, showOnlyFollowed, followedCodes]);
81
+
82
+ return (
83
+ <div className="space-y-6">
84
+ {/* Header & Modes */}
85
+ <div className="flex items-center justify-between border-b border-slate-800 pb-4">
86
+ <div>
87
+ <h2 className="text-2xl font-bold text-white">Discovery & Intelligence</h2>
88
+ <p className="text-sm text-slate-400">Encuentra oportunidades reales o analiza a tu competencia.</p>
89
+ </div>
90
+ <div className="flex rounded-2xl bg-slate-900 p-1">
91
+ <button
92
+ onClick={() => setSearchMode("keyword")}
93
+ className={`rounded-xl px-4 py-2 text-xs font-semibold transition ${
94
+ searchMode === "keyword" ? "bg-cyan text-slate-950 shadow-lg shadow-cyan/20" : "text-slate-400 hover:text-slate-200"
95
+ }`}
96
+ >
97
+ Búsqueda General
98
+ </button>
99
+ <button
100
+ onClick={() => setSearchMode("intelligence")}
101
+ className={`rounded-xl px-4 py-2 text-xs font-semibold transition ${
102
+ searchMode === "intelligence" ? "bg-cyan text-slate-950 shadow-lg shadow-cyan/20" : "text-slate-400 hover:text-slate-200"
103
+ }`}
104
+ >
105
+ Market Intelligence
106
+ </button>
107
+ </div>
108
+ </div>
109
+
110
+ {/* Search Forms */}
111
+ <div className="rounded-3xl border border-slate-800 bg-slate-900/40 p-6">
112
+ {searchMode === "keyword" ? (
113
+ <div className="flex flex-wrap items-end gap-4">
114
+ <div className="flex-1 min-w-[300px]">
115
+ <label className="block text-[10px] uppercase tracking-widest text-slate-500 mb-2 ml-1">Palabra clave o código</label>
116
+ <input
117
+ value={keyword}
118
+ onChange={(event) => setKeyword(event.target.value)}
119
+ onKeyDown={(event) => event.key === "Enter" && handleSearchClick()}
120
+ placeholder="Ej: software, 7210-24-LE23"
121
+ className="w-full rounded-2xl border border-slate-800 bg-slate-950 px-5 py-4 text-white outline-none focus:border-cyan transition shadow-inner"
122
+ />
123
+ </div>
124
+ <button
125
+ onClick={() => handleSearchClick()}
126
+ disabled={isLoading}
127
+ className="rounded-2xl bg-cyan px-8 py-4 font-bold text-slate-950 transition hover:bg-sky hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50"
128
+ >
129
+ {isLoading ? "Buscando..." : "Search"}
130
+ </button>
131
+ </div>
132
+ ) : (
133
+ <div className="grid gap-4 md:grid-cols-4 items-end">
134
+ <div>
135
+ <label className="block text-[10px] uppercase tracking-widest text-slate-500 mb-2 ml-1">RUT Proveedor (Competencia)</label>
136
+ <input
137
+ value={providerCode}
138
+ onChange={(event) => setProviderCode(event.target.value)}
139
+ placeholder="Ej: 17793"
140
+ className="w-full rounded-2xl border border-slate-800 bg-slate-950 px-4 py-3 text-white text-sm outline-none focus:border-cyan"
141
+ />
142
+ </div>
143
+ <div>
144
+ <label className="block text-[10px] uppercase tracking-widest text-slate-500 mb-2 ml-1">ID Organismo (Comprador)</label>
145
+ <input
146
+ value={buyerCode}
147
+ onChange={(event) => setBuyerCode(event.target.value)}
148
+ placeholder="Ej: 6945"
149
+ className="w-full rounded-2xl border border-slate-800 bg-slate-950 px-4 py-3 text-white text-sm outline-none focus:border-cyan"
150
+ />
151
+ </div>
152
+ <div>
153
+ <label className="block text-[10px] uppercase tracking-widest text-slate-500 mb-2 ml-1">Fecha (ddmmaaaa)</label>
154
+ <input
155
+ value={searchDate}
156
+ onChange={(event) => setSearchDate(event.target.value)}
157
+ placeholder="Ej: 29042024"
158
+ className="w-full rounded-2xl border border-slate-800 bg-slate-950 px-4 py-3 text-white text-sm outline-none focus:border-cyan"
159
+ />
160
+ </div>
161
+ <button
162
+ onClick={() => handleSearchClick()}
163
+ className="rounded-2xl bg-sky px-6 py-3 font-bold text-slate-950 transition hover:bg-cyan"
164
+ >
165
+ Get Insights
166
+ </button>
167
+ </div>
168
+ )}
169
+ </div>
170
+
171
+ {/* Results Header & Follow Filter */}
172
+ <div className="flex items-center justify-between px-2">
173
+ <h3 className="text-lg font-bold text-white flex items-center gap-2">
174
+ {showOnlyFollowed ? "Mis Seguimientos" : "Resultados Encontrados"}
175
+ <span className="text-xs bg-slate-800 text-slate-500 px-2 py-0.5 rounded-full font-mono">
176
+ {filteredTenders.length}
177
+ </span>
178
+ </h3>
179
+ {followedCodes.length > 0 && (
180
+ <button
181
+ onClick={() => setShowOnlyFollowed(!showOnlyFollowed)}
182
+ className={`flex items-center gap-2 rounded-xl px-4 py-2 text-xs font-bold transition border ${
183
+ showOnlyFollowed
184
+ ? "bg-amber-400/10 border-amber-400/30 text-amber-400"
185
+ : "bg-slate-900 border-slate-800 text-slate-400 hover:border-slate-700"
186
+ }`}
187
+ >
188
+ {showOnlyFollowed ? "★ Viendo Seguimientos" : "☆ Ver solo Seguidos"}
189
+ </button>
190
+ )}
191
+ </div>
192
+
193
+ {/* Results Table */}
194
+ <div className="space-y-4">
195
+ {filteredTenders.length === 0 ? (
196
+ <div className="flex flex-col items-center justify-center rounded-3xl border border-slate-800 bg-slate-950/30 p-20 text-center">
197
+ <div className="text-5xl mb-4 opacity-50">{showOnlyFollowed ? "🌟" : "📡"}</div>
198
+ <p className="text-slate-400">
199
+ {showOnlyFollowed
200
+ ? "No tienes licitaciones marcadas como favoritas todavía."
201
+ : "Inicia una búsqueda para conectar con la API de Mercado Público."}
202
+ </p>
203
+ </div>
204
+ ) : (
205
+ <div className="overflow-hidden rounded-3xl border border-slate-800 bg-slate-950/80 shadow-2xl">
206
+ <table className="w-full min-w-[900px] border-collapse text-left text-sm">
207
+ <thead className="bg-slate-900 text-slate-400 uppercase text-[10px] tracking-widest font-bold">
208
+ <tr>
209
+ <th className="px-6 py-5">Identificador</th>
210
+ <th className="px-6 py-5">Oportunidad</th>
211
+ <th className="px-6 py-5">Entidad Compradora</th>
212
+ <th className="px-6 py-5">Cierre</th>
213
+ <th className="px-6 py-5 text-center">Estado</th>
214
+ <th className="px-6 py-5">Monto</th>
215
+ <th className="px-6 py-5 text-right pr-10">Acciones</th>
216
+ </tr>
217
+ </thead>
218
+ <tbody>
219
+ {filteredTenders.map((tender) => (
220
+ <Fragment key={tender.code}>
221
+ <tr className="border-t border-slate-800 hover:bg-slate-900/50 transition-colors group">
222
+ <td className="px-6 py-5">
223
+ <div className="flex items-center gap-3">
224
+ <button
225
+ onClick={() => toggleFollow(tender.code)}
226
+ className={`text-lg transition-transform hover:scale-125 ${followedCodes.includes(tender.code) ? 'text-amber-400' : 'text-slate-600 hover:text-slate-400'}`}
227
+ >
228
+ {followedCodes.includes(tender.code) ? "★" : "☆"}
229
+ </button>
230
+ <div>
231
+ <div className="font-mono text-cyan">{tender.code}</div>
232
+ <div className="text-[10px] text-slate-500">{tender.source}</div>
233
+ </div>
234
+ </div>
235
+ </td>
236
+ <td className="px-6 py-5 max-w-xs">
237
+ <div className="font-semibold text-white group-hover:text-cyan transition-colors truncate">{tender.name}</div>
238
+ <div className="text-xs text-slate-500">{tender.region || "Multiregional"}</div>
239
+ </td>
240
+ <td className="px-6 py-5 text-slate-300">{tender.buyer}</td>
241
+ <td className="px-6 py-5">
242
+ <div className={`text-xs font-mono ${
243
+ tender.closing_date && new Date(tender.closing_date).getTime() - new Date().getTime() < 3 * 24 * 60 * 60 * 1000
244
+ ? "text-red-400 font-bold"
245
+ : "text-slate-400"
246
+ }`}>
247
+ {tender.closing_date ? new Date(tender.closing_date).toLocaleDateString() : "---"}
248
+ </div>
249
+ </td>
250
+ <td className="px-6 py-5 text-center">
251
+ <span className={`rounded-full px-3 py-1 text-[10px] font-bold ${
252
+ tender.status.toLowerCase().includes('abierto') || tender.status.toLowerCase().includes('publicada')
253
+ ? 'bg-green-500/10 text-green-400 border border-green-500/20'
254
+ : 'bg-slate-800 text-slate-400'
255
+ }`}>
256
+ {tender.status}
257
+ </span>
258
+ </td>
259
+ <td className="px-6 py-5 font-semibold text-slate-200">
260
+ {tender.estimated_amount
261
+ ? new Intl.NumberFormat("es-CL", { style: "currency", currency: "CLP", maximumFractionDigits: 0 }).format(tender.estimated_amount)
262
+ : "---"}
263
+ </td>
264
+ <td className="px-6 py-5 text-right pr-10">
265
+ <div className="flex items-center justify-end gap-3">
266
+ <button
267
+ onClick={() => toggleExpanded(tender.code)}
268
+ className="rounded-xl border border-slate-700 bg-slate-900 px-5 py-2.5 text-xs font-bold text-slate-200 hover:border-cyan hover:text-cyan transition whitespace-nowrap"
269
+ >
270
+ {expandedTenderCodes.includes(tender.code) ? "Cerrar" : "Detalle"}
271
+ </button>
272
+ <button
273
+ onClick={() => onAnalyze(tender)}
274
+ className="rounded-xl bg-cyan px-5 py-2.5 text-xs font-bold text-slate-950 hover:bg-sky transition shadow-lg shadow-cyan/10 whitespace-nowrap"
275
+ >
276
+ Analyze
277
+ </button>
278
+ </div>
279
+ </td>
280
+ </tr>
281
+ {expandedTenderCodes.includes(tender.code) && (
282
+ <tr className="bg-slate-950/90 animate-in fade-in duration-300">
283
+ <td colSpan={7} className="px-10 py-8 border-t border-slate-800">
284
+ <div className="grid gap-10 lg:grid-cols-2">
285
+ {/* Left Col: Info */}
286
+ <div className="space-y-8">
287
+ <div>
288
+ <h4 className="text-xs font-bold uppercase tracking-widest text-cyan mb-4">Descripción del Proyecto</h4>
289
+ <p className="text-slate-300 leading-relaxed text-sm">{tender.description}</p>
290
+ </div>
291
+
292
+ <div>
293
+ <h4 className="text-xs font-bold uppercase tracking-widest text-cyan mb-4">Ítems Solicitados</h4>
294
+ <div className="space-y-2 max-h-[200px] overflow-y-auto pr-2 custom-scrollbar">
295
+ {tender.items?.length ? tender.items.map((it, i) => (
296
+ <div key={i} className="flex items-center justify-between p-3 rounded-xl bg-slate-900/50 border border-slate-800">
297
+ <span className="text-xs text-slate-200">{it.name}</span>
298
+ <span className="text-xs font-mono text-cyan">{it.quantity} {it.unit}</span>
299
+ </div>
300
+ )) : <p className="text-xs text-slate-600 italic">No hay ítems detallados disponibles.</p>}
301
+ </div>
302
+ </div>
303
+ </div>
304
+
305
+ {/* Right Col: Documents & Meta */}
306
+ <div className="space-y-8">
307
+ <div>
308
+ <h4 className="text-xs font-bold uppercase tracking-widest text-cyan mb-4">Bases y Documentos</h4>
309
+ <div className="grid gap-3">
310
+ {tender.attachments && tender.attachments.length > 0 ? (
311
+ tender.attachments.map((att, i) => (
312
+ <a key={i} href={att.url} target="_blank" className="flex items-center gap-3 p-4 rounded-2xl bg-slate-900 hover:bg-slate-800 border border-slate-800 transition group">
313
+ <div className="text-2xl group-hover:scale-110 transition">📄</div>
314
+ <div className="overflow-hidden">
315
+ <div className="text-xs font-semibold text-slate-200 truncate group-hover:text-cyan">{att.name}</div>
316
+ <div className="text-[10px] text-slate-500">Descargar desde Mercado Público</div>
317
+ </div>
318
+ </a>
319
+ ))
320
+ ) : (
321
+ <div className="space-y-3">
322
+ <div className="rounded-2xl border border-dashed border-slate-800 p-6 text-center">
323
+ <p className="text-xs text-slate-500">No hay enlaces directos.</p>
324
+ <p className="mt-2 text-xs text-cyan">Usa 'Upload PDF' en el análisis agéntico.</p>
325
+ </div>
326
+ <a
327
+ href={`https://www.mercadopublico.cl/fichaLicitacion.html?code=${tender.code}`}
328
+ target="_blank"
329
+ rel="noopener noreferrer"
330
+ className="flex items-center justify-center gap-2 w-full p-4 rounded-2xl bg-cyan/10 border border-cyan/20 text-cyan text-xs font-bold hover:bg-cyan/20 transition"
331
+ >
332
+ 🌐 Ir al Portal de Mercado Público
333
+ </a>
334
+ </div>
335
+ )}
336
+ </div>
337
+ </div>
338
+
339
+ <div className="grid grid-cols-2 gap-4">
340
+ <div className="p-4 rounded-2xl bg-slate-900/50 border border-slate-800">
341
+ <div className="text-[10px] uppercase text-slate-500 font-bold mb-1">Cierre Licitación</div>
342
+ <div className="text-xs text-slate-100">{tender.closing_date}</div>
343
+ </div>
344
+ <div className="p-4 rounded-2xl bg-slate-900/50 border border-slate-800">
345
+ <div className="text-[10px] uppercase text-slate-500 font-bold mb-1">Rubro / Sector</div>
346
+ <div className="text-xs text-slate-100">{tender.sector || "General"}</div>
347
+ </div>
348
+ </div>
349
+ </div>
350
+ </div>
351
+ </td>
352
+ </tr>
353
+ )}
354
+ </Fragment>
355
+ ))}
356
+ </tbody>
357
+ </table>
358
+ {/* Pagination Controls */}
359
+ {!showOnlyFollowed && tenders.length > 0 && (
360
+ <div className="flex items-center justify-between p-6 border-t border-slate-800">
361
+ <div className="text-xs text-slate-500">
362
+ Mostrando <span className="text-white font-bold">{tenders.length}</span> resultados (Página {currentPage})
363
+ </div>
364
+ <div className="flex gap-2">
365
+ <button
366
+ onClick={() => handleSearchClick(currentPage - 1)}
367
+ disabled={currentPage === 1 || isLoading}
368
+ className="px-4 py-2 rounded-xl border border-slate-700 text-xs font-bold text-white hover:bg-slate-800 disabled:opacity-30 transition"
369
+ >
370
+ ← Anterior
371
+ </button>
372
+ <button
373
+ onClick={() => handleSearchClick(currentPage + 1)}
374
+ disabled={tenders.length < itemsPerPage || isLoading}
375
+ className="px-4 py-2 rounded-xl border border-slate-700 text-xs font-bold text-white hover:bg-slate-800 disabled:opacity-30 transition"
376
+ >
377
+ Siguiente →
378
+ </button>
379
+ </div>
380
+ </div>
381
+ )}
382
+ </div>
383
+ )}
384
+ </div>
385
+
386
+ {/* Agent Documentation Section */}
387
+ <div className="mt-12 rounded-3xl border border-slate-800 bg-slate-900/20 p-8">
388
+ <h3 className="text-xl font-bold text-white mb-6 flex items-center gap-2">
389
+ <span className="text-cyan">◈</span> Intelligent Agent Orchestration
390
+ </h3>
391
+ <div className="flex items-center gap-3 mb-6">
392
+ <span className="text-cyan">◈</span>
393
+ <h3 className="text-sm font-bold uppercase tracking-widest text-white">Intelligent Agent Orchestration</h3>
394
+ </div>
395
+ <div className="grid gap-4 md:grid-cols-3">
396
+ <div className="rounded-2xl border border-slate-800/50 bg-slate-950/50 p-4">
397
+ <div className="text-[10px] font-bold text-cyan mb-2 uppercase">Legal Agent</div>
398
+ <p className="text-[11px] text-slate-400">Analiza bases administrativas y riesgos de cumplimiento legal en tiempo real.</p>
399
+ </div>
400
+ <div className="rounded-2xl border border-slate-800/50 bg-slate-950/50 p-4">
401
+ <div className="text-[10px] font-bold text-cyan mb-2 uppercase">Technical Agent</div>
402
+ <p className="text-[11px] text-slate-400">Evalúa especificaciones técnicas y determina la factibilidad del proyecto.</p>
403
+ </div>
404
+ <div className="rounded-2xl border border-slate-800/50 bg-slate-950/50 p-4">
405
+ <div className="text-[10px] font-bold text-cyan mb-2 uppercase">Strategy Agent</div>
406
+ <p className="text-[11px] text-slate-400">Calcula el ROI proyectado y define la mejor táctica comercial para ganar.</p>
407
+ </div>
408
+ </div>
409
+ </div>
410
+
411
+ {isLoading && <BrandLoader />}
412
+ </div>
413
+ );
414
+ }
frontend/globals.css ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ color-scheme: dark;
7
+ }
8
+
9
+ html,
10
+ body {
11
+ min-height: 100%;
12
+ }
13
+
14
+ body {
15
+ margin: 0;
16
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
17
+ background: radial-gradient(circle at top, rgba(34, 211, 238, 0.14), transparent 30%),
18
+ linear-gradient(180deg, #06121d 0%, #081727 40%, #081a29 100%);
19
+ }
20
+
21
+ * {
22
+ box-sizing: border-box;
23
+ }
24
+
25
+ ::selection {
26
+ background: rgba(34, 211, 238, 0.3);
27
+ }
28
+
29
+ button,
30
+ input,
31
+ textarea,
32
+ select {
33
+ font: inherit;
34
+ }
frontend/lib/api.ts ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { AnalysisHistoryItem, AnalysisResult, CompanyProfile, Tender } from "./types";
2
+
3
+ const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8000";
4
+
5
+ const jsonHeaders = {
6
+ "Content-Type": "application/json",
7
+ };
8
+
9
+ export async function healthCheck() {
10
+ const res = await fetch(`${API_BASE}/health`);
11
+ if (!res.ok) {
12
+ throw new Error("Health check failed");
13
+ }
14
+ return res.json();
15
+ }
16
+
17
+ export async function fetchDbStatus() {
18
+ const res = await fetch(`${API_BASE}/api/health/db-status`);
19
+ if (!res.ok) return null;
20
+ return res.json();
21
+ }
22
+
23
+ export async function searchTenders(params: {
24
+ keyword?: string;
25
+ buyer_code?: string;
26
+ provider_code?: string;
27
+ date?: string;
28
+ skip?: number;
29
+ limit?: number;
30
+ }): Promise<Tender[]> {
31
+ const query = new URLSearchParams();
32
+ if (params.keyword) query.append("keyword", params.keyword);
33
+ if (params.buyer_code) query.append("buyer_code", params.buyer_code);
34
+ if (params.provider_code) query.append("provider_code", params.provider_code);
35
+ if (params.date) query.append("date", params.date);
36
+ if (params.skip !== undefined) query.append("skip", params.skip.toString());
37
+ if (params.limit !== undefined) query.append("limit", params.limit.toString());
38
+
39
+ const res = await fetch(`${API_BASE}/api/tenders?${query.toString()}`);
40
+ if (!res.ok) {
41
+ throw new Error("Error searching tenders");
42
+ }
43
+ return res.json();
44
+ }
45
+
46
+ export async function analyzeTender(
47
+ tender: Tender,
48
+ companyProfile: CompanyProfile,
49
+ documentText?: string
50
+ ): Promise<AnalysisResult> {
51
+ const res = await fetch(`${API_BASE}/api/analyze`, {
52
+ method: "POST",
53
+ headers: jsonHeaders,
54
+ body: JSON.stringify({
55
+ tender,
56
+ company_profile: companyProfile,
57
+ document_text: documentText
58
+ }),
59
+ });
60
+ if (!res.ok) {
61
+ throw new Error("Error analyzing tender");
62
+ }
63
+ return res.json();
64
+ }
65
+
66
+ export async function uploadDocument(file: File): Promise<{ text: string; filename: string }> {
67
+ const formData = new FormData();
68
+ formData.append("file", file);
69
+
70
+ const res = await fetch(`${API_BASE}/api/upload-document`, {
71
+ method: "POST",
72
+ body: formData,
73
+ });
74
+ if (!res.ok) {
75
+ throw new Error("Error uploading document");
76
+ }
77
+ return res.json();
78
+ }
79
+
80
+ export async function saveCompanyProfile(profile: CompanyProfile): Promise<CompanyProfile> {
81
+ const res = await fetch(`${API_BASE}/api/company-profile`, {
82
+ method: "POST",
83
+ headers: jsonHeaders,
84
+ body: JSON.stringify(profile),
85
+ });
86
+ if (!res.ok) {
87
+ throw new Error("Error saving company profile");
88
+ }
89
+ return res.json();
90
+ }
91
+
92
+ export async function fetchCompanyProfile(): Promise<CompanyProfile> {
93
+ const res = await fetch(`${API_BASE}/api/company-profile`);
94
+ if (!res.ok) {
95
+ throw new Error("No company profile available");
96
+ }
97
+ return res.json();
98
+ }
99
+
100
+ export async function fetchAnalysisHistory(): Promise<AnalysisHistoryItem[]> {
101
+ const res = await fetch(`${API_BASE}/api/analysis-history`);
102
+ if (!res.ok) {
103
+ throw new Error("Error fetching analysis history");
104
+ }
105
+ return res.json();
106
+ }
frontend/lib/types.ts ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type TenderItem = {
2
+ name: str;
3
+ quantity: number;
4
+ unit: str;
5
+ };
6
+
7
+ export type TenderAttachment = {
8
+ name: str;
9
+ url: str;
10
+ };
11
+
12
+ export type Tender = {
13
+ code: string;
14
+ name: string;
15
+ buyer: string;
16
+ status: string;
17
+ closing_date: string;
18
+ description: string;
19
+ estimated_amount: number | null;
20
+ source: string;
21
+ region?: string;
22
+ sector?: string;
23
+ items?: TenderItem[];
24
+ attachments?: TenderAttachment[];
25
+ };
26
+
27
+ export type CompanyProfile = {
28
+ name: string;
29
+ industry: string;
30
+ services: string[];
31
+ experience: string;
32
+ certifications: string[];
33
+ regions: string[];
34
+ documents_available: string[];
35
+ };
36
+
37
+ export type RiskItem = {
38
+ title: string;
39
+ severity: "High" | "Medium" | "Low";
40
+ explanation: string;
41
+ };
42
+
43
+ export type ActionItem = {
44
+ task: string;
45
+ priority: string;
46
+ owner: string;
47
+ timeline: string;
48
+ };
49
+
50
+ export type AnalysisResult = {
51
+ fit_score: number;
52
+ decision: string;
53
+ executive_summary: string;
54
+ key_requirements: string[];
55
+ risks: RiskItem[];
56
+ compliance_gaps: string[];
57
+ action_plan: ActionItem[];
58
+ proposal_draft: string;
59
+ report_markdown: string;
60
+ audit_log: string[];
61
+ };
62
+
63
+ export type AnalysisHistoryItem = {
64
+ tender_code: string;
65
+ tender_name: string;
66
+ analyzed_at: string;
67
+ analysis: AnalysisResult;
68
+ };
frontend/next-env.d.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/basic-features/typescript for more information.
frontend/next.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ reactStrictMode: true,
4
+ };
5
+
6
+ module.exports = nextConfig;
frontend/package-lock.json ADDED
@@ -0,0 +1,1662 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "andesops-ai-frontend",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "andesops-ai-frontend",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "next": "14.2.5",
12
+ "react": "18.3.1",
13
+ "react-dom": "18.3.1"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "20.14.2",
17
+ "@types/react": "18.3.3",
18
+ "@types/react-dom": "18.3.0",
19
+ "autoprefixer": "10.4.19",
20
+ "postcss": "8.4.35",
21
+ "tailwindcss": "3.4.4",
22
+ "typescript": "5.6.3"
23
+ }
24
+ },
25
+ "node_modules/@alloc/quick-lru": {
26
+ "version": "5.2.0",
27
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
28
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
29
+ "dev": true,
30
+ "license": "MIT",
31
+ "engines": {
32
+ "node": ">=10"
33
+ },
34
+ "funding": {
35
+ "url": "https://github.com/sponsors/sindresorhus"
36
+ }
37
+ },
38
+ "node_modules/@jridgewell/gen-mapping": {
39
+ "version": "0.3.13",
40
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
41
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
42
+ "dev": true,
43
+ "license": "MIT",
44
+ "dependencies": {
45
+ "@jridgewell/sourcemap-codec": "^1.5.0",
46
+ "@jridgewell/trace-mapping": "^0.3.24"
47
+ }
48
+ },
49
+ "node_modules/@jridgewell/resolve-uri": {
50
+ "version": "3.1.2",
51
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
52
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
53
+ "dev": true,
54
+ "license": "MIT",
55
+ "engines": {
56
+ "node": ">=6.0.0"
57
+ }
58
+ },
59
+ "node_modules/@jridgewell/sourcemap-codec": {
60
+ "version": "1.5.5",
61
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
62
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
63
+ "dev": true,
64
+ "license": "MIT"
65
+ },
66
+ "node_modules/@jridgewell/trace-mapping": {
67
+ "version": "0.3.31",
68
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
69
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
70
+ "dev": true,
71
+ "license": "MIT",
72
+ "dependencies": {
73
+ "@jridgewell/resolve-uri": "^3.1.0",
74
+ "@jridgewell/sourcemap-codec": "^1.4.14"
75
+ }
76
+ },
77
+ "node_modules/@next/env": {
78
+ "version": "14.2.5",
79
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz",
80
+ "integrity": "sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==",
81
+ "license": "MIT"
82
+ },
83
+ "node_modules/@next/swc-darwin-arm64": {
84
+ "version": "14.2.5",
85
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz",
86
+ "integrity": "sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==",
87
+ "cpu": [
88
+ "arm64"
89
+ ],
90
+ "license": "MIT",
91
+ "optional": true,
92
+ "os": [
93
+ "darwin"
94
+ ],
95
+ "engines": {
96
+ "node": ">= 10"
97
+ }
98
+ },
99
+ "node_modules/@next/swc-darwin-x64": {
100
+ "version": "14.2.5",
101
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz",
102
+ "integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==",
103
+ "cpu": [
104
+ "x64"
105
+ ],
106
+ "license": "MIT",
107
+ "optional": true,
108
+ "os": [
109
+ "darwin"
110
+ ],
111
+ "engines": {
112
+ "node": ">= 10"
113
+ }
114
+ },
115
+ "node_modules/@next/swc-linux-arm64-gnu": {
116
+ "version": "14.2.5",
117
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz",
118
+ "integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==",
119
+ "cpu": [
120
+ "arm64"
121
+ ],
122
+ "license": "MIT",
123
+ "optional": true,
124
+ "os": [
125
+ "linux"
126
+ ],
127
+ "engines": {
128
+ "node": ">= 10"
129
+ }
130
+ },
131
+ "node_modules/@next/swc-linux-arm64-musl": {
132
+ "version": "14.2.5",
133
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz",
134
+ "integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==",
135
+ "cpu": [
136
+ "arm64"
137
+ ],
138
+ "license": "MIT",
139
+ "optional": true,
140
+ "os": [
141
+ "linux"
142
+ ],
143
+ "engines": {
144
+ "node": ">= 10"
145
+ }
146
+ },
147
+ "node_modules/@next/swc-linux-x64-gnu": {
148
+ "version": "14.2.5",
149
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz",
150
+ "integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==",
151
+ "cpu": [
152
+ "x64"
153
+ ],
154
+ "license": "MIT",
155
+ "optional": true,
156
+ "os": [
157
+ "linux"
158
+ ],
159
+ "engines": {
160
+ "node": ">= 10"
161
+ }
162
+ },
163
+ "node_modules/@next/swc-linux-x64-musl": {
164
+ "version": "14.2.5",
165
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz",
166
+ "integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==",
167
+ "cpu": [
168
+ "x64"
169
+ ],
170
+ "license": "MIT",
171
+ "optional": true,
172
+ "os": [
173
+ "linux"
174
+ ],
175
+ "engines": {
176
+ "node": ">= 10"
177
+ }
178
+ },
179
+ "node_modules/@next/swc-win32-arm64-msvc": {
180
+ "version": "14.2.5",
181
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz",
182
+ "integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==",
183
+ "cpu": [
184
+ "arm64"
185
+ ],
186
+ "license": "MIT",
187
+ "optional": true,
188
+ "os": [
189
+ "win32"
190
+ ],
191
+ "engines": {
192
+ "node": ">= 10"
193
+ }
194
+ },
195
+ "node_modules/@next/swc-win32-ia32-msvc": {
196
+ "version": "14.2.5",
197
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz",
198
+ "integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==",
199
+ "cpu": [
200
+ "ia32"
201
+ ],
202
+ "license": "MIT",
203
+ "optional": true,
204
+ "os": [
205
+ "win32"
206
+ ],
207
+ "engines": {
208
+ "node": ">= 10"
209
+ }
210
+ },
211
+ "node_modules/@next/swc-win32-x64-msvc": {
212
+ "version": "14.2.5",
213
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz",
214
+ "integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==",
215
+ "cpu": [
216
+ "x64"
217
+ ],
218
+ "license": "MIT",
219
+ "optional": true,
220
+ "os": [
221
+ "win32"
222
+ ],
223
+ "engines": {
224
+ "node": ">= 10"
225
+ }
226
+ },
227
+ "node_modules/@nodelib/fs.scandir": {
228
+ "version": "2.1.5",
229
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
230
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
231
+ "dev": true,
232
+ "license": "MIT",
233
+ "dependencies": {
234
+ "@nodelib/fs.stat": "2.0.5",
235
+ "run-parallel": "^1.1.9"
236
+ },
237
+ "engines": {
238
+ "node": ">= 8"
239
+ }
240
+ },
241
+ "node_modules/@nodelib/fs.stat": {
242
+ "version": "2.0.5",
243
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
244
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
245
+ "dev": true,
246
+ "license": "MIT",
247
+ "engines": {
248
+ "node": ">= 8"
249
+ }
250
+ },
251
+ "node_modules/@nodelib/fs.walk": {
252
+ "version": "1.2.8",
253
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
254
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
255
+ "dev": true,
256
+ "license": "MIT",
257
+ "dependencies": {
258
+ "@nodelib/fs.scandir": "2.1.5",
259
+ "fastq": "^1.6.0"
260
+ },
261
+ "engines": {
262
+ "node": ">= 8"
263
+ }
264
+ },
265
+ "node_modules/@swc/counter": {
266
+ "version": "0.1.3",
267
+ "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
268
+ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
269
+ "license": "Apache-2.0"
270
+ },
271
+ "node_modules/@swc/helpers": {
272
+ "version": "0.5.5",
273
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
274
+ "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
275
+ "license": "Apache-2.0",
276
+ "dependencies": {
277
+ "@swc/counter": "^0.1.3",
278
+ "tslib": "^2.4.0"
279
+ }
280
+ },
281
+ "node_modules/@types/node": {
282
+ "version": "20.14.2",
283
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz",
284
+ "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==",
285
+ "dev": true,
286
+ "license": "MIT",
287
+ "dependencies": {
288
+ "undici-types": "~5.26.4"
289
+ }
290
+ },
291
+ "node_modules/@types/prop-types": {
292
+ "version": "15.7.15",
293
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
294
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
295
+ "dev": true,
296
+ "license": "MIT"
297
+ },
298
+ "node_modules/@types/react": {
299
+ "version": "18.3.3",
300
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
301
+ "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
302
+ "dev": true,
303
+ "license": "MIT",
304
+ "dependencies": {
305
+ "@types/prop-types": "*",
306
+ "csstype": "^3.0.2"
307
+ }
308
+ },
309
+ "node_modules/@types/react-dom": {
310
+ "version": "18.3.0",
311
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
312
+ "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
313
+ "dev": true,
314
+ "license": "MIT",
315
+ "dependencies": {
316
+ "@types/react": "*"
317
+ }
318
+ },
319
+ "node_modules/any-promise": {
320
+ "version": "1.3.0",
321
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
322
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
323
+ "dev": true,
324
+ "license": "MIT"
325
+ },
326
+ "node_modules/anymatch": {
327
+ "version": "3.1.3",
328
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
329
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
330
+ "dev": true,
331
+ "license": "ISC",
332
+ "dependencies": {
333
+ "normalize-path": "^3.0.0",
334
+ "picomatch": "^2.0.4"
335
+ },
336
+ "engines": {
337
+ "node": ">= 8"
338
+ }
339
+ },
340
+ "node_modules/arg": {
341
+ "version": "5.0.2",
342
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
343
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
344
+ "dev": true,
345
+ "license": "MIT"
346
+ },
347
+ "node_modules/autoprefixer": {
348
+ "version": "10.4.19",
349
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
350
+ "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==",
351
+ "dev": true,
352
+ "funding": [
353
+ {
354
+ "type": "opencollective",
355
+ "url": "https://opencollective.com/postcss/"
356
+ },
357
+ {
358
+ "type": "tidelift",
359
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
360
+ },
361
+ {
362
+ "type": "github",
363
+ "url": "https://github.com/sponsors/ai"
364
+ }
365
+ ],
366
+ "license": "MIT",
367
+ "dependencies": {
368
+ "browserslist": "^4.23.0",
369
+ "caniuse-lite": "^1.0.30001599",
370
+ "fraction.js": "^4.3.7",
371
+ "normalize-range": "^0.1.2",
372
+ "picocolors": "^1.0.0",
373
+ "postcss-value-parser": "^4.2.0"
374
+ },
375
+ "bin": {
376
+ "autoprefixer": "bin/autoprefixer"
377
+ },
378
+ "engines": {
379
+ "node": "^10 || ^12 || >=14"
380
+ },
381
+ "peerDependencies": {
382
+ "postcss": "^8.1.0"
383
+ }
384
+ },
385
+ "node_modules/baseline-browser-mapping": {
386
+ "version": "2.10.24",
387
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.24.tgz",
388
+ "integrity": "sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==",
389
+ "dev": true,
390
+ "license": "Apache-2.0",
391
+ "bin": {
392
+ "baseline-browser-mapping": "dist/cli.cjs"
393
+ },
394
+ "engines": {
395
+ "node": ">=6.0.0"
396
+ }
397
+ },
398
+ "node_modules/binary-extensions": {
399
+ "version": "2.3.0",
400
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
401
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
402
+ "dev": true,
403
+ "license": "MIT",
404
+ "engines": {
405
+ "node": ">=8"
406
+ },
407
+ "funding": {
408
+ "url": "https://github.com/sponsors/sindresorhus"
409
+ }
410
+ },
411
+ "node_modules/braces": {
412
+ "version": "3.0.3",
413
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
414
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
415
+ "dev": true,
416
+ "license": "MIT",
417
+ "dependencies": {
418
+ "fill-range": "^7.1.1"
419
+ },
420
+ "engines": {
421
+ "node": ">=8"
422
+ }
423
+ },
424
+ "node_modules/browserslist": {
425
+ "version": "4.28.2",
426
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
427
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
428
+ "dev": true,
429
+ "funding": [
430
+ {
431
+ "type": "opencollective",
432
+ "url": "https://opencollective.com/browserslist"
433
+ },
434
+ {
435
+ "type": "tidelift",
436
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
437
+ },
438
+ {
439
+ "type": "github",
440
+ "url": "https://github.com/sponsors/ai"
441
+ }
442
+ ],
443
+ "license": "MIT",
444
+ "dependencies": {
445
+ "baseline-browser-mapping": "^2.10.12",
446
+ "caniuse-lite": "^1.0.30001782",
447
+ "electron-to-chromium": "^1.5.328",
448
+ "node-releases": "^2.0.36",
449
+ "update-browserslist-db": "^1.2.3"
450
+ },
451
+ "bin": {
452
+ "browserslist": "cli.js"
453
+ },
454
+ "engines": {
455
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
456
+ }
457
+ },
458
+ "node_modules/busboy": {
459
+ "version": "1.6.0",
460
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
461
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
462
+ "dependencies": {
463
+ "streamsearch": "^1.1.0"
464
+ },
465
+ "engines": {
466
+ "node": ">=10.16.0"
467
+ }
468
+ },
469
+ "node_modules/camelcase-css": {
470
+ "version": "2.0.1",
471
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
472
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
473
+ "dev": true,
474
+ "license": "MIT",
475
+ "engines": {
476
+ "node": ">= 6"
477
+ }
478
+ },
479
+ "node_modules/caniuse-lite": {
480
+ "version": "1.0.30001791",
481
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz",
482
+ "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==",
483
+ "funding": [
484
+ {
485
+ "type": "opencollective",
486
+ "url": "https://opencollective.com/browserslist"
487
+ },
488
+ {
489
+ "type": "tidelift",
490
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
491
+ },
492
+ {
493
+ "type": "github",
494
+ "url": "https://github.com/sponsors/ai"
495
+ }
496
+ ],
497
+ "license": "CC-BY-4.0"
498
+ },
499
+ "node_modules/chokidar": {
500
+ "version": "3.6.0",
501
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
502
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
503
+ "dev": true,
504
+ "license": "MIT",
505
+ "dependencies": {
506
+ "anymatch": "~3.1.2",
507
+ "braces": "~3.0.2",
508
+ "glob-parent": "~5.1.2",
509
+ "is-binary-path": "~2.1.0",
510
+ "is-glob": "~4.0.1",
511
+ "normalize-path": "~3.0.0",
512
+ "readdirp": "~3.6.0"
513
+ },
514
+ "engines": {
515
+ "node": ">= 8.10.0"
516
+ },
517
+ "funding": {
518
+ "url": "https://paulmillr.com/funding/"
519
+ },
520
+ "optionalDependencies": {
521
+ "fsevents": "~2.3.2"
522
+ }
523
+ },
524
+ "node_modules/chokidar/node_modules/glob-parent": {
525
+ "version": "5.1.2",
526
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
527
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
528
+ "dev": true,
529
+ "license": "ISC",
530
+ "dependencies": {
531
+ "is-glob": "^4.0.1"
532
+ },
533
+ "engines": {
534
+ "node": ">= 6"
535
+ }
536
+ },
537
+ "node_modules/client-only": {
538
+ "version": "0.0.1",
539
+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
540
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
541
+ "license": "MIT"
542
+ },
543
+ "node_modules/commander": {
544
+ "version": "4.1.1",
545
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
546
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
547
+ "dev": true,
548
+ "license": "MIT",
549
+ "engines": {
550
+ "node": ">= 6"
551
+ }
552
+ },
553
+ "node_modules/cssesc": {
554
+ "version": "3.0.0",
555
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
556
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
557
+ "dev": true,
558
+ "license": "MIT",
559
+ "bin": {
560
+ "cssesc": "bin/cssesc"
561
+ },
562
+ "engines": {
563
+ "node": ">=4"
564
+ }
565
+ },
566
+ "node_modules/csstype": {
567
+ "version": "3.2.3",
568
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
569
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
570
+ "dev": true,
571
+ "license": "MIT"
572
+ },
573
+ "node_modules/didyoumean": {
574
+ "version": "1.2.2",
575
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
576
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
577
+ "dev": true,
578
+ "license": "Apache-2.0"
579
+ },
580
+ "node_modules/dlv": {
581
+ "version": "1.1.3",
582
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
583
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
584
+ "dev": true,
585
+ "license": "MIT"
586
+ },
587
+ "node_modules/electron-to-chromium": {
588
+ "version": "1.5.344",
589
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
590
+ "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==",
591
+ "dev": true,
592
+ "license": "ISC"
593
+ },
594
+ "node_modules/es-errors": {
595
+ "version": "1.3.0",
596
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
597
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
598
+ "dev": true,
599
+ "license": "MIT",
600
+ "engines": {
601
+ "node": ">= 0.4"
602
+ }
603
+ },
604
+ "node_modules/escalade": {
605
+ "version": "3.2.0",
606
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
607
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
608
+ "dev": true,
609
+ "license": "MIT",
610
+ "engines": {
611
+ "node": ">=6"
612
+ }
613
+ },
614
+ "node_modules/fast-glob": {
615
+ "version": "3.3.3",
616
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
617
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
618
+ "dev": true,
619
+ "license": "MIT",
620
+ "dependencies": {
621
+ "@nodelib/fs.stat": "^2.0.2",
622
+ "@nodelib/fs.walk": "^1.2.3",
623
+ "glob-parent": "^5.1.2",
624
+ "merge2": "^1.3.0",
625
+ "micromatch": "^4.0.8"
626
+ },
627
+ "engines": {
628
+ "node": ">=8.6.0"
629
+ }
630
+ },
631
+ "node_modules/fast-glob/node_modules/glob-parent": {
632
+ "version": "5.1.2",
633
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
634
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
635
+ "dev": true,
636
+ "license": "ISC",
637
+ "dependencies": {
638
+ "is-glob": "^4.0.1"
639
+ },
640
+ "engines": {
641
+ "node": ">= 6"
642
+ }
643
+ },
644
+ "node_modules/fastq": {
645
+ "version": "1.20.1",
646
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
647
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
648
+ "dev": true,
649
+ "license": "ISC",
650
+ "dependencies": {
651
+ "reusify": "^1.0.4"
652
+ }
653
+ },
654
+ "node_modules/fill-range": {
655
+ "version": "7.1.1",
656
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
657
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
658
+ "dev": true,
659
+ "license": "MIT",
660
+ "dependencies": {
661
+ "to-regex-range": "^5.0.1"
662
+ },
663
+ "engines": {
664
+ "node": ">=8"
665
+ }
666
+ },
667
+ "node_modules/fraction.js": {
668
+ "version": "4.3.7",
669
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
670
+ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
671
+ "dev": true,
672
+ "license": "MIT",
673
+ "engines": {
674
+ "node": "*"
675
+ },
676
+ "funding": {
677
+ "type": "patreon",
678
+ "url": "https://github.com/sponsors/rawify"
679
+ }
680
+ },
681
+ "node_modules/fsevents": {
682
+ "version": "2.3.3",
683
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
684
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
685
+ "dev": true,
686
+ "hasInstallScript": true,
687
+ "license": "MIT",
688
+ "optional": true,
689
+ "os": [
690
+ "darwin"
691
+ ],
692
+ "engines": {
693
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
694
+ }
695
+ },
696
+ "node_modules/function-bind": {
697
+ "version": "1.1.2",
698
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
699
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
700
+ "dev": true,
701
+ "license": "MIT",
702
+ "funding": {
703
+ "url": "https://github.com/sponsors/ljharb"
704
+ }
705
+ },
706
+ "node_modules/glob-parent": {
707
+ "version": "6.0.2",
708
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
709
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
710
+ "dev": true,
711
+ "license": "ISC",
712
+ "dependencies": {
713
+ "is-glob": "^4.0.3"
714
+ },
715
+ "engines": {
716
+ "node": ">=10.13.0"
717
+ }
718
+ },
719
+ "node_modules/graceful-fs": {
720
+ "version": "4.2.11",
721
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
722
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
723
+ "license": "ISC"
724
+ },
725
+ "node_modules/hasown": {
726
+ "version": "2.0.3",
727
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
728
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
729
+ "dev": true,
730
+ "license": "MIT",
731
+ "dependencies": {
732
+ "function-bind": "^1.1.2"
733
+ },
734
+ "engines": {
735
+ "node": ">= 0.4"
736
+ }
737
+ },
738
+ "node_modules/is-binary-path": {
739
+ "version": "2.1.0",
740
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
741
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
742
+ "dev": true,
743
+ "license": "MIT",
744
+ "dependencies": {
745
+ "binary-extensions": "^2.0.0"
746
+ },
747
+ "engines": {
748
+ "node": ">=8"
749
+ }
750
+ },
751
+ "node_modules/is-core-module": {
752
+ "version": "2.16.1",
753
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
754
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
755
+ "dev": true,
756
+ "license": "MIT",
757
+ "dependencies": {
758
+ "hasown": "^2.0.2"
759
+ },
760
+ "engines": {
761
+ "node": ">= 0.4"
762
+ },
763
+ "funding": {
764
+ "url": "https://github.com/sponsors/ljharb"
765
+ }
766
+ },
767
+ "node_modules/is-extglob": {
768
+ "version": "2.1.1",
769
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
770
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
771
+ "dev": true,
772
+ "license": "MIT",
773
+ "engines": {
774
+ "node": ">=0.10.0"
775
+ }
776
+ },
777
+ "node_modules/is-glob": {
778
+ "version": "4.0.3",
779
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
780
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
781
+ "dev": true,
782
+ "license": "MIT",
783
+ "dependencies": {
784
+ "is-extglob": "^2.1.1"
785
+ },
786
+ "engines": {
787
+ "node": ">=0.10.0"
788
+ }
789
+ },
790
+ "node_modules/is-number": {
791
+ "version": "7.0.0",
792
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
793
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
794
+ "dev": true,
795
+ "license": "MIT",
796
+ "engines": {
797
+ "node": ">=0.12.0"
798
+ }
799
+ },
800
+ "node_modules/jiti": {
801
+ "version": "1.21.7",
802
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
803
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
804
+ "dev": true,
805
+ "license": "MIT",
806
+ "bin": {
807
+ "jiti": "bin/jiti.js"
808
+ }
809
+ },
810
+ "node_modules/js-tokens": {
811
+ "version": "4.0.0",
812
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
813
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
814
+ "license": "MIT"
815
+ },
816
+ "node_modules/lilconfig": {
817
+ "version": "2.1.0",
818
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
819
+ "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
820
+ "dev": true,
821
+ "license": "MIT",
822
+ "engines": {
823
+ "node": ">=10"
824
+ }
825
+ },
826
+ "node_modules/lines-and-columns": {
827
+ "version": "1.2.4",
828
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
829
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
830
+ "dev": true,
831
+ "license": "MIT"
832
+ },
833
+ "node_modules/loose-envify": {
834
+ "version": "1.4.0",
835
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
836
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
837
+ "license": "MIT",
838
+ "dependencies": {
839
+ "js-tokens": "^3.0.0 || ^4.0.0"
840
+ },
841
+ "bin": {
842
+ "loose-envify": "cli.js"
843
+ }
844
+ },
845
+ "node_modules/merge2": {
846
+ "version": "1.4.1",
847
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
848
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
849
+ "dev": true,
850
+ "license": "MIT",
851
+ "engines": {
852
+ "node": ">= 8"
853
+ }
854
+ },
855
+ "node_modules/micromatch": {
856
+ "version": "4.0.8",
857
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
858
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
859
+ "dev": true,
860
+ "license": "MIT",
861
+ "dependencies": {
862
+ "braces": "^3.0.3",
863
+ "picomatch": "^2.3.1"
864
+ },
865
+ "engines": {
866
+ "node": ">=8.6"
867
+ }
868
+ },
869
+ "node_modules/mz": {
870
+ "version": "2.7.0",
871
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
872
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
873
+ "dev": true,
874
+ "license": "MIT",
875
+ "dependencies": {
876
+ "any-promise": "^1.0.0",
877
+ "object-assign": "^4.0.1",
878
+ "thenify-all": "^1.0.0"
879
+ }
880
+ },
881
+ "node_modules/nanoid": {
882
+ "version": "3.3.11",
883
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
884
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
885
+ "funding": [
886
+ {
887
+ "type": "github",
888
+ "url": "https://github.com/sponsors/ai"
889
+ }
890
+ ],
891
+ "license": "MIT",
892
+ "bin": {
893
+ "nanoid": "bin/nanoid.cjs"
894
+ },
895
+ "engines": {
896
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
897
+ }
898
+ },
899
+ "node_modules/next": {
900
+ "version": "14.2.5",
901
+ "resolved": "https://registry.npmjs.org/next/-/next-14.2.5.tgz",
902
+ "integrity": "sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==",
903
+ "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.",
904
+ "license": "MIT",
905
+ "dependencies": {
906
+ "@next/env": "14.2.5",
907
+ "@swc/helpers": "0.5.5",
908
+ "busboy": "1.6.0",
909
+ "caniuse-lite": "^1.0.30001579",
910
+ "graceful-fs": "^4.2.11",
911
+ "postcss": "8.4.31",
912
+ "styled-jsx": "5.1.1"
913
+ },
914
+ "bin": {
915
+ "next": "dist/bin/next"
916
+ },
917
+ "engines": {
918
+ "node": ">=18.17.0"
919
+ },
920
+ "optionalDependencies": {
921
+ "@next/swc-darwin-arm64": "14.2.5",
922
+ "@next/swc-darwin-x64": "14.2.5",
923
+ "@next/swc-linux-arm64-gnu": "14.2.5",
924
+ "@next/swc-linux-arm64-musl": "14.2.5",
925
+ "@next/swc-linux-x64-gnu": "14.2.5",
926
+ "@next/swc-linux-x64-musl": "14.2.5",
927
+ "@next/swc-win32-arm64-msvc": "14.2.5",
928
+ "@next/swc-win32-ia32-msvc": "14.2.5",
929
+ "@next/swc-win32-x64-msvc": "14.2.5"
930
+ },
931
+ "peerDependencies": {
932
+ "@opentelemetry/api": "^1.1.0",
933
+ "@playwright/test": "^1.41.2",
934
+ "react": "^18.2.0",
935
+ "react-dom": "^18.2.0",
936
+ "sass": "^1.3.0"
937
+ },
938
+ "peerDependenciesMeta": {
939
+ "@opentelemetry/api": {
940
+ "optional": true
941
+ },
942
+ "@playwright/test": {
943
+ "optional": true
944
+ },
945
+ "sass": {
946
+ "optional": true
947
+ }
948
+ }
949
+ },
950
+ "node_modules/next/node_modules/postcss": {
951
+ "version": "8.4.31",
952
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
953
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
954
+ "funding": [
955
+ {
956
+ "type": "opencollective",
957
+ "url": "https://opencollective.com/postcss/"
958
+ },
959
+ {
960
+ "type": "tidelift",
961
+ "url": "https://tidelift.com/funding/github/npm/postcss"
962
+ },
963
+ {
964
+ "type": "github",
965
+ "url": "https://github.com/sponsors/ai"
966
+ }
967
+ ],
968
+ "license": "MIT",
969
+ "dependencies": {
970
+ "nanoid": "^3.3.6",
971
+ "picocolors": "^1.0.0",
972
+ "source-map-js": "^1.0.2"
973
+ },
974
+ "engines": {
975
+ "node": "^10 || ^12 || >=14"
976
+ }
977
+ },
978
+ "node_modules/node-releases": {
979
+ "version": "2.0.38",
980
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
981
+ "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
982
+ "dev": true,
983
+ "license": "MIT"
984
+ },
985
+ "node_modules/normalize-path": {
986
+ "version": "3.0.0",
987
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
988
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
989
+ "dev": true,
990
+ "license": "MIT",
991
+ "engines": {
992
+ "node": ">=0.10.0"
993
+ }
994
+ },
995
+ "node_modules/normalize-range": {
996
+ "version": "0.1.2",
997
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
998
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
999
+ "dev": true,
1000
+ "license": "MIT",
1001
+ "engines": {
1002
+ "node": ">=0.10.0"
1003
+ }
1004
+ },
1005
+ "node_modules/object-assign": {
1006
+ "version": "4.1.1",
1007
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1008
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1009
+ "dev": true,
1010
+ "license": "MIT",
1011
+ "engines": {
1012
+ "node": ">=0.10.0"
1013
+ }
1014
+ },
1015
+ "node_modules/object-hash": {
1016
+ "version": "3.0.0",
1017
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
1018
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
1019
+ "dev": true,
1020
+ "license": "MIT",
1021
+ "engines": {
1022
+ "node": ">= 6"
1023
+ }
1024
+ },
1025
+ "node_modules/path-parse": {
1026
+ "version": "1.0.7",
1027
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
1028
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
1029
+ "dev": true,
1030
+ "license": "MIT"
1031
+ },
1032
+ "node_modules/picocolors": {
1033
+ "version": "1.1.1",
1034
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1035
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1036
+ "license": "ISC"
1037
+ },
1038
+ "node_modules/picomatch": {
1039
+ "version": "2.3.2",
1040
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
1041
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
1042
+ "dev": true,
1043
+ "license": "MIT",
1044
+ "engines": {
1045
+ "node": ">=8.6"
1046
+ },
1047
+ "funding": {
1048
+ "url": "https://github.com/sponsors/jonschlinkert"
1049
+ }
1050
+ },
1051
+ "node_modules/pify": {
1052
+ "version": "2.3.0",
1053
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
1054
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
1055
+ "dev": true,
1056
+ "license": "MIT",
1057
+ "engines": {
1058
+ "node": ">=0.10.0"
1059
+ }
1060
+ },
1061
+ "node_modules/pirates": {
1062
+ "version": "4.0.7",
1063
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
1064
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
1065
+ "dev": true,
1066
+ "license": "MIT",
1067
+ "engines": {
1068
+ "node": ">= 6"
1069
+ }
1070
+ },
1071
+ "node_modules/postcss": {
1072
+ "version": "8.4.35",
1073
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
1074
+ "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
1075
+ "dev": true,
1076
+ "funding": [
1077
+ {
1078
+ "type": "opencollective",
1079
+ "url": "https://opencollective.com/postcss/"
1080
+ },
1081
+ {
1082
+ "type": "tidelift",
1083
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1084
+ },
1085
+ {
1086
+ "type": "github",
1087
+ "url": "https://github.com/sponsors/ai"
1088
+ }
1089
+ ],
1090
+ "license": "MIT",
1091
+ "dependencies": {
1092
+ "nanoid": "^3.3.7",
1093
+ "picocolors": "^1.0.0",
1094
+ "source-map-js": "^1.0.2"
1095
+ },
1096
+ "engines": {
1097
+ "node": "^10 || ^12 || >=14"
1098
+ }
1099
+ },
1100
+ "node_modules/postcss-import": {
1101
+ "version": "15.1.0",
1102
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
1103
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
1104
+ "dev": true,
1105
+ "license": "MIT",
1106
+ "dependencies": {
1107
+ "postcss-value-parser": "^4.0.0",
1108
+ "read-cache": "^1.0.0",
1109
+ "resolve": "^1.1.7"
1110
+ },
1111
+ "engines": {
1112
+ "node": ">=14.0.0"
1113
+ },
1114
+ "peerDependencies": {
1115
+ "postcss": "^8.0.0"
1116
+ }
1117
+ },
1118
+ "node_modules/postcss-js": {
1119
+ "version": "4.1.0",
1120
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
1121
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
1122
+ "dev": true,
1123
+ "funding": [
1124
+ {
1125
+ "type": "opencollective",
1126
+ "url": "https://opencollective.com/postcss/"
1127
+ },
1128
+ {
1129
+ "type": "github",
1130
+ "url": "https://github.com/sponsors/ai"
1131
+ }
1132
+ ],
1133
+ "license": "MIT",
1134
+ "dependencies": {
1135
+ "camelcase-css": "^2.0.1"
1136
+ },
1137
+ "engines": {
1138
+ "node": "^12 || ^14 || >= 16"
1139
+ },
1140
+ "peerDependencies": {
1141
+ "postcss": "^8.4.21"
1142
+ }
1143
+ },
1144
+ "node_modules/postcss-nested": {
1145
+ "version": "6.2.0",
1146
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
1147
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
1148
+ "dev": true,
1149
+ "funding": [
1150
+ {
1151
+ "type": "opencollective",
1152
+ "url": "https://opencollective.com/postcss/"
1153
+ },
1154
+ {
1155
+ "type": "github",
1156
+ "url": "https://github.com/sponsors/ai"
1157
+ }
1158
+ ],
1159
+ "license": "MIT",
1160
+ "dependencies": {
1161
+ "postcss-selector-parser": "^6.1.1"
1162
+ },
1163
+ "engines": {
1164
+ "node": ">=12.0"
1165
+ },
1166
+ "peerDependencies": {
1167
+ "postcss": "^8.2.14"
1168
+ }
1169
+ },
1170
+ "node_modules/postcss-selector-parser": {
1171
+ "version": "6.1.2",
1172
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
1173
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
1174
+ "dev": true,
1175
+ "license": "MIT",
1176
+ "dependencies": {
1177
+ "cssesc": "^3.0.0",
1178
+ "util-deprecate": "^1.0.2"
1179
+ },
1180
+ "engines": {
1181
+ "node": ">=4"
1182
+ }
1183
+ },
1184
+ "node_modules/postcss-value-parser": {
1185
+ "version": "4.2.0",
1186
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
1187
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
1188
+ "dev": true,
1189
+ "license": "MIT"
1190
+ },
1191
+ "node_modules/queue-microtask": {
1192
+ "version": "1.2.3",
1193
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
1194
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
1195
+ "dev": true,
1196
+ "funding": [
1197
+ {
1198
+ "type": "github",
1199
+ "url": "https://github.com/sponsors/feross"
1200
+ },
1201
+ {
1202
+ "type": "patreon",
1203
+ "url": "https://www.patreon.com/feross"
1204
+ },
1205
+ {
1206
+ "type": "consulting",
1207
+ "url": "https://feross.org/support"
1208
+ }
1209
+ ],
1210
+ "license": "MIT"
1211
+ },
1212
+ "node_modules/react": {
1213
+ "version": "18.3.1",
1214
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1215
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1216
+ "license": "MIT",
1217
+ "dependencies": {
1218
+ "loose-envify": "^1.1.0"
1219
+ },
1220
+ "engines": {
1221
+ "node": ">=0.10.0"
1222
+ }
1223
+ },
1224
+ "node_modules/react-dom": {
1225
+ "version": "18.3.1",
1226
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
1227
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1228
+ "license": "MIT",
1229
+ "dependencies": {
1230
+ "loose-envify": "^1.1.0",
1231
+ "scheduler": "^0.23.2"
1232
+ },
1233
+ "peerDependencies": {
1234
+ "react": "^18.3.1"
1235
+ }
1236
+ },
1237
+ "node_modules/read-cache": {
1238
+ "version": "1.0.0",
1239
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
1240
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
1241
+ "dev": true,
1242
+ "license": "MIT",
1243
+ "dependencies": {
1244
+ "pify": "^2.3.0"
1245
+ }
1246
+ },
1247
+ "node_modules/readdirp": {
1248
+ "version": "3.6.0",
1249
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1250
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1251
+ "dev": true,
1252
+ "license": "MIT",
1253
+ "dependencies": {
1254
+ "picomatch": "^2.2.1"
1255
+ },
1256
+ "engines": {
1257
+ "node": ">=8.10.0"
1258
+ }
1259
+ },
1260
+ "node_modules/resolve": {
1261
+ "version": "1.22.12",
1262
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
1263
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
1264
+ "dev": true,
1265
+ "license": "MIT",
1266
+ "dependencies": {
1267
+ "es-errors": "^1.3.0",
1268
+ "is-core-module": "^2.16.1",
1269
+ "path-parse": "^1.0.7",
1270
+ "supports-preserve-symlinks-flag": "^1.0.0"
1271
+ },
1272
+ "bin": {
1273
+ "resolve": "bin/resolve"
1274
+ },
1275
+ "engines": {
1276
+ "node": ">= 0.4"
1277
+ },
1278
+ "funding": {
1279
+ "url": "https://github.com/sponsors/ljharb"
1280
+ }
1281
+ },
1282
+ "node_modules/reusify": {
1283
+ "version": "1.1.0",
1284
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
1285
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
1286
+ "dev": true,
1287
+ "license": "MIT",
1288
+ "engines": {
1289
+ "iojs": ">=1.0.0",
1290
+ "node": ">=0.10.0"
1291
+ }
1292
+ },
1293
+ "node_modules/run-parallel": {
1294
+ "version": "1.2.0",
1295
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
1296
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
1297
+ "dev": true,
1298
+ "funding": [
1299
+ {
1300
+ "type": "github",
1301
+ "url": "https://github.com/sponsors/feross"
1302
+ },
1303
+ {
1304
+ "type": "patreon",
1305
+ "url": "https://www.patreon.com/feross"
1306
+ },
1307
+ {
1308
+ "type": "consulting",
1309
+ "url": "https://feross.org/support"
1310
+ }
1311
+ ],
1312
+ "license": "MIT",
1313
+ "dependencies": {
1314
+ "queue-microtask": "^1.2.2"
1315
+ }
1316
+ },
1317
+ "node_modules/scheduler": {
1318
+ "version": "0.23.2",
1319
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
1320
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
1321
+ "license": "MIT",
1322
+ "dependencies": {
1323
+ "loose-envify": "^1.1.0"
1324
+ }
1325
+ },
1326
+ "node_modules/source-map-js": {
1327
+ "version": "1.2.1",
1328
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1329
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1330
+ "license": "BSD-3-Clause",
1331
+ "engines": {
1332
+ "node": ">=0.10.0"
1333
+ }
1334
+ },
1335
+ "node_modules/streamsearch": {
1336
+ "version": "1.1.0",
1337
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
1338
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
1339
+ "engines": {
1340
+ "node": ">=10.0.0"
1341
+ }
1342
+ },
1343
+ "node_modules/styled-jsx": {
1344
+ "version": "5.1.1",
1345
+ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
1346
+ "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
1347
+ "license": "MIT",
1348
+ "dependencies": {
1349
+ "client-only": "0.0.1"
1350
+ },
1351
+ "engines": {
1352
+ "node": ">= 12.0.0"
1353
+ },
1354
+ "peerDependencies": {
1355
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
1356
+ },
1357
+ "peerDependenciesMeta": {
1358
+ "@babel/core": {
1359
+ "optional": true
1360
+ },
1361
+ "babel-plugin-macros": {
1362
+ "optional": true
1363
+ }
1364
+ }
1365
+ },
1366
+ "node_modules/sucrase": {
1367
+ "version": "3.35.1",
1368
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
1369
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
1370
+ "dev": true,
1371
+ "license": "MIT",
1372
+ "dependencies": {
1373
+ "@jridgewell/gen-mapping": "^0.3.2",
1374
+ "commander": "^4.0.0",
1375
+ "lines-and-columns": "^1.1.6",
1376
+ "mz": "^2.7.0",
1377
+ "pirates": "^4.0.1",
1378
+ "tinyglobby": "^0.2.11",
1379
+ "ts-interface-checker": "^0.1.9"
1380
+ },
1381
+ "bin": {
1382
+ "sucrase": "bin/sucrase",
1383
+ "sucrase-node": "bin/sucrase-node"
1384
+ },
1385
+ "engines": {
1386
+ "node": ">=16 || 14 >=14.17"
1387
+ }
1388
+ },
1389
+ "node_modules/supports-preserve-symlinks-flag": {
1390
+ "version": "1.0.0",
1391
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
1392
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
1393
+ "dev": true,
1394
+ "license": "MIT",
1395
+ "engines": {
1396
+ "node": ">= 0.4"
1397
+ },
1398
+ "funding": {
1399
+ "url": "https://github.com/sponsors/ljharb"
1400
+ }
1401
+ },
1402
+ "node_modules/tailwindcss": {
1403
+ "version": "3.4.4",
1404
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz",
1405
+ "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==",
1406
+ "dev": true,
1407
+ "license": "MIT",
1408
+ "dependencies": {
1409
+ "@alloc/quick-lru": "^5.2.0",
1410
+ "arg": "^5.0.2",
1411
+ "chokidar": "^3.5.3",
1412
+ "didyoumean": "^1.2.2",
1413
+ "dlv": "^1.1.3",
1414
+ "fast-glob": "^3.3.0",
1415
+ "glob-parent": "^6.0.2",
1416
+ "is-glob": "^4.0.3",
1417
+ "jiti": "^1.21.0",
1418
+ "lilconfig": "^2.1.0",
1419
+ "micromatch": "^4.0.5",
1420
+ "normalize-path": "^3.0.0",
1421
+ "object-hash": "^3.0.0",
1422
+ "picocolors": "^1.0.0",
1423
+ "postcss": "^8.4.23",
1424
+ "postcss-import": "^15.1.0",
1425
+ "postcss-js": "^4.0.1",
1426
+ "postcss-load-config": "^4.0.1",
1427
+ "postcss-nested": "^6.0.1",
1428
+ "postcss-selector-parser": "^6.0.11",
1429
+ "resolve": "^1.22.2",
1430
+ "sucrase": "^3.32.0"
1431
+ },
1432
+ "bin": {
1433
+ "tailwind": "lib/cli.js",
1434
+ "tailwindcss": "lib/cli.js"
1435
+ },
1436
+ "engines": {
1437
+ "node": ">=14.0.0"
1438
+ }
1439
+ },
1440
+ "node_modules/tailwindcss/node_modules/postcss-load-config": {
1441
+ "version": "4.0.2",
1442
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
1443
+ "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
1444
+ "dev": true,
1445
+ "funding": [
1446
+ {
1447
+ "type": "opencollective",
1448
+ "url": "https://opencollective.com/postcss/"
1449
+ },
1450
+ {
1451
+ "type": "github",
1452
+ "url": "https://github.com/sponsors/ai"
1453
+ }
1454
+ ],
1455
+ "license": "MIT",
1456
+ "dependencies": {
1457
+ "lilconfig": "^3.0.0",
1458
+ "yaml": "^2.3.4"
1459
+ },
1460
+ "engines": {
1461
+ "node": ">= 14"
1462
+ },
1463
+ "peerDependencies": {
1464
+ "postcss": ">=8.0.9",
1465
+ "ts-node": ">=9.0.0"
1466
+ },
1467
+ "peerDependenciesMeta": {
1468
+ "postcss": {
1469
+ "optional": true
1470
+ },
1471
+ "ts-node": {
1472
+ "optional": true
1473
+ }
1474
+ }
1475
+ },
1476
+ "node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": {
1477
+ "version": "3.1.3",
1478
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
1479
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
1480
+ "dev": true,
1481
+ "license": "MIT",
1482
+ "engines": {
1483
+ "node": ">=14"
1484
+ },
1485
+ "funding": {
1486
+ "url": "https://github.com/sponsors/antonk52"
1487
+ }
1488
+ },
1489
+ "node_modules/thenify": {
1490
+ "version": "3.3.1",
1491
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
1492
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
1493
+ "dev": true,
1494
+ "license": "MIT",
1495
+ "dependencies": {
1496
+ "any-promise": "^1.0.0"
1497
+ }
1498
+ },
1499
+ "node_modules/thenify-all": {
1500
+ "version": "1.6.0",
1501
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
1502
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
1503
+ "dev": true,
1504
+ "license": "MIT",
1505
+ "dependencies": {
1506
+ "thenify": ">= 3.1.0 < 4"
1507
+ },
1508
+ "engines": {
1509
+ "node": ">=0.8"
1510
+ }
1511
+ },
1512
+ "node_modules/tinyglobby": {
1513
+ "version": "0.2.16",
1514
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
1515
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
1516
+ "dev": true,
1517
+ "license": "MIT",
1518
+ "dependencies": {
1519
+ "fdir": "^6.5.0",
1520
+ "picomatch": "^4.0.4"
1521
+ },
1522
+ "engines": {
1523
+ "node": ">=12.0.0"
1524
+ },
1525
+ "funding": {
1526
+ "url": "https://github.com/sponsors/SuperchupuDev"
1527
+ }
1528
+ },
1529
+ "node_modules/tinyglobby/node_modules/fdir": {
1530
+ "version": "6.5.0",
1531
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1532
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1533
+ "dev": true,
1534
+ "license": "MIT",
1535
+ "engines": {
1536
+ "node": ">=12.0.0"
1537
+ },
1538
+ "peerDependencies": {
1539
+ "picomatch": "^3 || ^4"
1540
+ },
1541
+ "peerDependenciesMeta": {
1542
+ "picomatch": {
1543
+ "optional": true
1544
+ }
1545
+ }
1546
+ },
1547
+ "node_modules/tinyglobby/node_modules/picomatch": {
1548
+ "version": "4.0.4",
1549
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
1550
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
1551
+ "dev": true,
1552
+ "license": "MIT",
1553
+ "engines": {
1554
+ "node": ">=12"
1555
+ },
1556
+ "funding": {
1557
+ "url": "https://github.com/sponsors/jonschlinkert"
1558
+ }
1559
+ },
1560
+ "node_modules/to-regex-range": {
1561
+ "version": "5.0.1",
1562
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1563
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1564
+ "dev": true,
1565
+ "license": "MIT",
1566
+ "dependencies": {
1567
+ "is-number": "^7.0.0"
1568
+ },
1569
+ "engines": {
1570
+ "node": ">=8.0"
1571
+ }
1572
+ },
1573
+ "node_modules/ts-interface-checker": {
1574
+ "version": "0.1.13",
1575
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
1576
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
1577
+ "dev": true,
1578
+ "license": "Apache-2.0"
1579
+ },
1580
+ "node_modules/tslib": {
1581
+ "version": "2.8.1",
1582
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
1583
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
1584
+ "license": "0BSD"
1585
+ },
1586
+ "node_modules/typescript": {
1587
+ "version": "5.6.3",
1588
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
1589
+ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
1590
+ "dev": true,
1591
+ "license": "Apache-2.0",
1592
+ "bin": {
1593
+ "tsc": "bin/tsc",
1594
+ "tsserver": "bin/tsserver"
1595
+ },
1596
+ "engines": {
1597
+ "node": ">=14.17"
1598
+ }
1599
+ },
1600
+ "node_modules/undici-types": {
1601
+ "version": "5.26.5",
1602
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
1603
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
1604
+ "dev": true,
1605
+ "license": "MIT"
1606
+ },
1607
+ "node_modules/update-browserslist-db": {
1608
+ "version": "1.2.3",
1609
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
1610
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
1611
+ "dev": true,
1612
+ "funding": [
1613
+ {
1614
+ "type": "opencollective",
1615
+ "url": "https://opencollective.com/browserslist"
1616
+ },
1617
+ {
1618
+ "type": "tidelift",
1619
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1620
+ },
1621
+ {
1622
+ "type": "github",
1623
+ "url": "https://github.com/sponsors/ai"
1624
+ }
1625
+ ],
1626
+ "license": "MIT",
1627
+ "dependencies": {
1628
+ "escalade": "^3.2.0",
1629
+ "picocolors": "^1.1.1"
1630
+ },
1631
+ "bin": {
1632
+ "update-browserslist-db": "cli.js"
1633
+ },
1634
+ "peerDependencies": {
1635
+ "browserslist": ">= 4.21.0"
1636
+ }
1637
+ },
1638
+ "node_modules/util-deprecate": {
1639
+ "version": "1.0.2",
1640
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
1641
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
1642
+ "dev": true,
1643
+ "license": "MIT"
1644
+ },
1645
+ "node_modules/yaml": {
1646
+ "version": "2.8.3",
1647
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
1648
+ "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
1649
+ "dev": true,
1650
+ "license": "ISC",
1651
+ "bin": {
1652
+ "yaml": "bin.mjs"
1653
+ },
1654
+ "engines": {
1655
+ "node": ">= 14.6"
1656
+ },
1657
+ "funding": {
1658
+ "url": "https://github.com/sponsors/eemeli"
1659
+ }
1660
+ }
1661
+ }
1662
+ }
frontend/package.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "andesops-ai-frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "next": "14.2.5",
13
+ "react": "18.3.1",
14
+ "react-dom": "18.3.1"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "20.14.2",
18
+ "@types/react": "18.3.3",
19
+ "@types/react-dom": "18.3.0",
20
+ "autoprefixer": "10.4.19",
21
+ "postcss": "8.4.35",
22
+ "tailwindcss": "3.4.4",
23
+ "typescript": "5.6.3"
24
+ }
25
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
frontend/tailwind.config.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Config } from "tailwindcss";
2
+
3
+ const config: Config = {
4
+ content: ["./app/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"],
5
+ theme: {
6
+ extend: {
7
+ colors: {
8
+ navy: "#0b1420",
9
+ cyan: "#22d3ee",
10
+ sky: "#38bdf8",
11
+ surface: "#112530",
12
+ },
13
+ },
14
+ },
15
+ plugins: [],
16
+ };
17
+
18
+ export default config;
frontend/tsconfig.json ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2020",
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "es2020"
8
+ ],
9
+ "allowJs": false,
10
+ "skipLibCheck": true,
11
+ "strict": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "noEmit": true,
14
+ "esModuleInterop": true,
15
+ "module": "esnext",
16
+ "moduleResolution": "node",
17
+ "resolveJsonModule": true,
18
+ "isolatedModules": true,
19
+ "jsx": "preserve",
20
+ "incremental": true,
21
+ "types": [
22
+ "node"
23
+ ],
24
+ "plugins": [
25
+ {
26
+ "name": "next"
27
+ }
28
+ ]
29
+ },
30
+ "include": [
31
+ "next-env.d.ts",
32
+ "**/*.ts",
33
+ "**/*.tsx",
34
+ ".next/types/**/*.ts"
35
+ ],
36
+ "exclude": [
37
+ "node_modules"
38
+ ]
39
+ }