diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..22f513cf94f22bdae6704a5d2314e6099fd85142
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,58 @@
+# Python
+__pycache__/
+*.pyc
+*.pyo
+*.pyd
+*.egg-info/
+*.egg
+dist/
+build/
+
+# Virtual environments
+venv/
+.venv/
+env/
+
+# Environment variables β never commit real credentials
+.env
+
+# Databases
+*.db
+*.sqlite3
+
+# FAISS vector stores β user data, never commit
+vector_store/
+*.index
+*.pkl
+
+# Uploaded files β user data, never commit
+temp/
+
+# Node
+node_modules/
+
+# Vite build output
+dist/
+.vite/
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
+
+# IDE
+.vscode/
+.idea/
+
+# React
+coverage/
+
+# Docker overrides
+*.override.yml
+
+
+
+hf_home/
+.cache/
\ No newline at end of file
diff --git a/DocPilot/backend/app/api/auth.py b/DocPilot/backend/app/api/auth.py
new file mode 100644
index 0000000000000000000000000000000000000000..ca7ca6953c884426c7b67cc64bbdb7b801570bd0
--- /dev/null
+++ b/DocPilot/backend/app/api/auth.py
@@ -0,0 +1,166 @@
+reset_tokens = {}
+
+import secrets
+
+from fastapi import (
+ APIRouter,
+ Depends,
+ HTTPException,
+)
+
+from fastapi.security import (
+ OAuth2PasswordRequestForm,
+)
+
+from sqlalchemy.orm import Session
+
+from app.db.session import get_db
+
+from app.models.user import User
+
+from app.schemas.auth import (
+ UserCreate,
+)
+
+from app.schemas.password import (
+ ForgotPasswordRequest,
+ ResetPasswordRequest,
+)
+
+from app.core.security import (
+ hash_password,
+ verify_password,
+ create_access_token,
+)
+
+from app.core.dependencies import (
+ get_current_user,
+)
+
+router = APIRouter()
+
+
+@router.post("/signup")
+def signup(
+ user: UserCreate,
+ db: Session = Depends(get_db),
+):
+
+ existing_user = db.query(User).filter(User.email == user.email).first()
+
+ if existing_user:
+
+ raise HTTPException(
+ status_code=400,
+ detail="Email already registered",
+ )
+
+ new_user = User(
+ username=user.username,
+ email=user.email,
+ hashed_password=hash_password(user.password),
+ )
+
+ db.add(new_user)
+
+ db.commit()
+
+ db.refresh(new_user)
+
+ return {"message": "User created successfully"}
+
+
+@router.post("/login")
+def login(
+ form_data: OAuth2PasswordRequestForm = Depends(),
+ db: Session = Depends(get_db),
+):
+
+ db_user = db.query(User).filter(User.email == form_data.username).first()
+
+ if not db_user:
+
+ raise HTTPException(
+ status_code=401,
+ detail="Invalid credentials",
+ )
+
+ if not verify_password(
+ form_data.password,
+ db_user.hashed_password,
+ ):
+
+ raise HTTPException(
+ status_code=401,
+ detail="Invalid credentials",
+ )
+
+ access_token = create_access_token(data={"sub": db_user.email})
+
+ return {
+ "access_token": access_token,
+ "token_type": "bearer",
+ }
+
+
+@router.get("/me")
+def get_me(current_user=Depends(get_current_user)):
+
+ return {
+ "id": current_user.id,
+ "username": current_user.username,
+ "email": current_user.email,
+ "plan": current_user.plan,
+ }
+
+
+@router.post("/forgot-password")
+def forgot_password(
+ email_data: ForgotPasswordRequest,
+ db: Session = Depends(get_db),
+):
+
+ user = db.query(User).filter(User.email == email_data.email).first()
+
+ if not user:
+
+ raise HTTPException(
+ status_code=404,
+ detail="User not found",
+ )
+
+ token = secrets.token_hex(16)
+
+ reset_tokens[token] = user.email
+
+ return {"reset_token": token}
+
+
+@router.post("/reset-password")
+def reset_password(
+ data: ResetPasswordRequest,
+ db: Session = Depends(get_db),
+):
+
+ token = data.token
+
+ new_password = data.new_password
+
+ if token not in reset_tokens:
+
+ raise HTTPException(
+ status_code=400,
+ detail="Invalid token",
+ )
+
+ email = reset_tokens[token]
+
+ user = db.query(User).filter(User.email == email).first()
+
+ user.hashed_password = hash_password(new_password)
+
+ db.commit()
+
+ del reset_tokens[token]
+
+ return {"message": "Password reset successful"}
diff --git a/DocPilot/backend/app/api/billing.py b/DocPilot/backend/app/api/billing.py
new file mode 100644
index 0000000000000000000000000000000000000000..988a361ebab5fd9d6da7b8e614a46da12e8d8096
--- /dev/null
+++ b/DocPilot/backend/app/api/billing.py
@@ -0,0 +1,61 @@
+from fastapi import (
+ APIRouter,
+ Depends,
+)
+
+from sqlalchemy.orm import Session
+
+from app.db.session import get_db
+
+from app.core.dependencies import (
+ get_current_user,
+)
+
+router = APIRouter()
+
+
+@router.get("/me")
+def get_plan(
+ current_user=Depends(get_current_user),
+):
+
+ return {
+ "username": current_user.username,
+ "plan": current_user.plan,
+ }
+
+
+@router.post("/upgrade")
+def upgrade_plan(
+ current_user=Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+
+ current_user.plan = "pro"
+
+ db.commit()
+
+ db.refresh(current_user)
+
+ return {
+ "message": "Upgraded to pro plan.",
+ "plan": current_user.plan,
+ }
+
+
+@router.post("/downgrade")
+def downgrade_plan(
+ current_user=Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+
+ current_user.plan = "free"
+
+ db.commit()
+
+ db.refresh(current_user)
+
+ return {
+ "message": "Downgraded to free plan.",
+ "plan": current_user.plan,
+ }
diff --git a/DocPilot/backend/app/api/chat.py b/DocPilot/backend/app/api/chat.py
new file mode 100644
index 0000000000000000000000000000000000000000..c9286dc64665e1143fb865b071d7d5dd27f7d45c
--- /dev/null
+++ b/DocPilot/backend/app/api/chat.py
@@ -0,0 +1,91 @@
+from fastapi import (
+ APIRouter,
+ Depends,
+ HTTPException,
+)
+
+from groq import AuthenticationError
+
+from sqlalchemy.orm import Session
+
+from app.services.rag import (
+ ask_question,
+)
+
+from app.core.dependencies import (
+ get_current_user,
+)
+
+from app.db.session import get_db
+
+from app.schemas.chat import ChatRequest
+
+from app.models.chat import (
+ ChatSession,
+ ChatMessage,
+)
+
+router = APIRouter()
+
+
+@router.post("/ask")
+def ask(
+ query: ChatRequest,
+ current_user=Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+
+ session_id = query.session_id
+
+ if not session_id:
+
+ session = ChatSession(
+ owner_id=current_user.id,
+ title=query.question.strip()[:40],
+ )
+
+ db.add(session)
+
+ db.commit()
+
+ db.refresh(session)
+
+ session_id = session.id
+
+ user_message = ChatMessage(
+ session_id=session_id,
+ role="user",
+ content=query.question,
+ )
+
+ db.add(user_message)
+
+ db.commit()
+
+ try:
+ rag_response = ask_question(
+ question=query.question,
+ user_id=current_user.id,
+ source=query.source,
+ )
+ except AuthenticationError as exc:
+ raise HTTPException(
+ status_code=502,
+ detail="Groq authentication failed. Check GROQ_API_KEY in .env and restart the server.",
+ ) from exc
+
+ assistant_message = ChatMessage(
+ session_id=session_id,
+ role="assistant",
+ content=rag_response["answer"],
+ )
+
+ db.add(assistant_message)
+
+ db.commit()
+
+ return {
+ "session_id": session_id,
+ "answer": rag_response["answer"],
+ "sources": rag_response["sources"],
+ }
diff --git a/DocPilot/backend/app/api/documents.py b/DocPilot/backend/app/api/documents.py
new file mode 100644
index 0000000000000000000000000000000000000000..3a1b483916e225f1556f183059fce6df95971a81
--- /dev/null
+++ b/DocPilot/backend/app/api/documents.py
@@ -0,0 +1,181 @@
+from fastapi import (
+ APIRouter,
+ UploadFile,
+ File,
+ Depends,
+ HTTPException,
+)
+
+from sqlalchemy.orm import Session
+
+import shutil
+import os
+
+from app.services.ingestion import (
+ process_document,
+)
+
+from pilotcore.retrieval.vector_store import (
+ reset_vector_store,
+ rebuild_index_without_document,
+)
+
+from app.core.dependencies import (
+ get_current_user,
+)
+
+from app.db.session import get_db
+
+from app.models.document import Document
+
+from app.schemas.document import (
+ DocumentResponse,
+)
+
+router = APIRouter()
+
+
+@router.post("/upload")
+async def upload_document(
+ file: UploadFile = File(...),
+ current_user=Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+
+ document_count = (
+ db.query(Document).filter(Document.owner_id == current_user.id).count()
+ )
+
+ if current_user.plan == "free" and document_count >= 3:
+
+ raise HTTPException(
+ status_code=403,
+ detail="Free plan upload limit reached.",
+ )
+
+ os.makedirs(
+ "temp",
+ exist_ok=True,
+ )
+
+ allowed_extensions = [
+ ".pdf",
+ ".docx",
+ ".txt",
+ ".md",
+ ".csv",
+ ".xlsx",
+ ".png",
+ ".jpg",
+ ".jpeg",
+ ]
+
+ file_ext = os.path.splitext(file.filename)[1].lower()
+
+ if file_ext not in allowed_extensions:
+
+ raise HTTPException(
+ status_code=400,
+ detail="Unsupported file type.",
+ )
+
+ file_path = f"temp/{file.filename}"
+
+ with open(
+ file_path,
+ "wb",
+ ) as buffer:
+
+ shutil.copyfileobj(
+ file.file,
+ buffer,
+ )
+
+ document = Document(
+ owner_id=current_user.id,
+ filename=file.filename,
+ filepath=file_path,
+ file_size=os.path.getsize(file_path),
+ )
+
+ db.add(document)
+
+ db.commit()
+
+ db.refresh(document)
+
+ process_document(
+ file_path,
+ current_user.id,
+ document.id,
+ )
+
+ return {
+ "message": "Document uploaded",
+ "document_id": document.id,
+ }
+
+
+@router.get(
+ "/",
+ response_model=list[DocumentResponse],
+)
+def get_documents(
+ db: Session = Depends(get_db),
+ current_user=Depends(get_current_user),
+):
+
+ documents = db.query(Document).filter(Document.owner_id == current_user.id).all()
+
+ return documents
+
+
+@router.delete("/reset")
+def reset_documents(
+ current_user=Depends(get_current_user),
+):
+
+ reset_vector_store(current_user.id)
+
+ return {"message": "Vector store cleared."}
+
+
+@router.delete("/{document_id}")
+def delete_document(
+ document_id: int,
+ db: Session = Depends(get_db),
+ current_user=Depends(get_current_user),
+):
+
+ document = (
+ db.query(Document)
+ .filter(
+ Document.id == document_id,
+ Document.owner_id == current_user.id,
+ )
+ .first()
+ )
+
+ if not document:
+
+ raise HTTPException(
+ status_code=404,
+ detail="Document not found",
+ )
+
+ if os.path.exists(document.filepath):
+
+ os.remove(document.filepath)
+
+ rebuild_index_without_document(
+ current_user.id,
+ document.id,
+ )
+
+ db.delete(document)
+
+ db.commit()
+
+ return {
+ "message": "Document deleted",
+ }
diff --git a/DocPilot/backend/app/api/history.py b/DocPilot/backend/app/api/history.py
new file mode 100644
index 0000000000000000000000000000000000000000..5a98acfdc28f6610b4c51513c868f7edb2a38be5
--- /dev/null
+++ b/DocPilot/backend/app/api/history.py
@@ -0,0 +1,89 @@
+from fastapi import (
+ APIRouter,
+ Depends,
+)
+
+from sqlalchemy.orm import Session
+
+from app.db.session import get_db
+
+from app.models.chat import (
+ ChatSession,
+ ChatMessage,
+)
+
+from app.core.dependencies import (
+ get_current_user,
+)
+
+router = APIRouter()
+
+
+@router.get("/sessions")
+def get_sessions(
+ db: Session = Depends(get_db),
+ current_user=Depends(get_current_user),
+):
+
+ sessions = (
+ db.query(ChatSession)
+ .filter(ChatSession.owner_id == current_user.id)
+ .order_by(ChatSession.id.desc())
+ .all()
+ )
+
+ return sessions
+
+
+@router.get("/{session_id}")
+def get_session_messages(
+ session_id: int,
+ db: Session = Depends(get_db),
+ current_user=Depends(get_current_user),
+):
+
+ session = (
+ db.query(ChatSession)
+ .filter(
+ ChatSession.id == session_id,
+ ChatSession.owner_id == current_user.id,
+ )
+ .first()
+ )
+
+ if not session:
+
+ return {"detail": "Session not found"}
+
+ messages = db.query(ChatMessage).filter(ChatMessage.session_id == session_id).all()
+
+ return messages
+
+
+@router.delete("/{session_id}")
+def delete_session(
+ session_id: int,
+ db: Session = Depends(get_db),
+ current_user=Depends(get_current_user),
+):
+
+ session = (
+ db.query(ChatSession)
+ .filter(
+ ChatSession.id == session_id,
+ ChatSession.owner_id == current_user.id,
+ )
+ .first()
+ )
+
+ if not session:
+
+ return {"detail": "Session not found"}
+
+ db.query(ChatMessage).filter(ChatMessage.session_id == session_id).delete()
+
+ db.delete(session)
+
+ db.commit()
+
+ return {"message": "Chat deleted"}
diff --git a/DocPilot/backend/app/core/config.py b/DocPilot/backend/app/core/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..128206b48bf9d0e9ca464e5d9580e584fe993fe7
--- /dev/null
+++ b/DocPilot/backend/app/core/config.py
@@ -0,0 +1 @@
+from pilotcore.config import DATABASE_URL, SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
diff --git a/DocPilot/backend/app/core/dependencies.py b/DocPilot/backend/app/core/dependencies.py
new file mode 100644
index 0000000000000000000000000000000000000000..8d36d6982021fc25a40869996b89f122fb8e98b7
--- /dev/null
+++ b/DocPilot/backend/app/core/dependencies.py
@@ -0,0 +1,43 @@
+from fastapi import Depends, HTTPException
+
+from fastapi.security import OAuth2PasswordBearer
+
+from jose import JWTError, jwt
+
+from sqlalchemy.orm import Session
+
+from app.db.session import get_db
+
+from app.models.user import User
+
+from app.core.security import SECRET_KEY, ALGORITHM
+
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
+
+
+def get_current_user(
+ token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)
+):
+
+ credentials_exception = HTTPException(
+ status_code=401, detail="Could not validate credentials"
+ )
+
+ try:
+
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
+
+ email = payload.get("sub")
+
+ if email is None:
+ raise credentials_exception
+
+ except JWTError:
+ raise credentials_exception
+
+ user = db.query(User).filter(User.email == email).first()
+
+ if user is None:
+ raise credentials_exception
+
+ return user
diff --git a/DocPilot/backend/app/core/security.py b/DocPilot/backend/app/core/security.py
new file mode 100644
index 0000000000000000000000000000000000000000..ddd66cdd575f5ba2bc53a8791505ae801df0125c
--- /dev/null
+++ b/DocPilot/backend/app/core/security.py
@@ -0,0 +1,21 @@
+from datetime import datetime, timedelta
+from jose import jwt
+from passlib.context import CryptContext
+from pilotcore.config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
+
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+
+
+def hash_password(password: str):
+ return pwd_context.hash(password)
+
+
+def verify_password(plain_password, hashed_password):
+ return pwd_context.verify(plain_password, hashed_password)
+
+
+def create_access_token(data: dict):
+ to_encode = data.copy()
+ expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+ to_encode.update({"exp": expire})
+ return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
diff --git a/DocPilot/backend/app/db/database.py b/DocPilot/backend/app/db/database.py
new file mode 100644
index 0000000000000000000000000000000000000000..e18a157076e0ed3dc7ade0917fa1581cb453f1bf
--- /dev/null
+++ b/DocPilot/backend/app/db/database.py
@@ -0,0 +1,9 @@
+from sqlalchemy import create_engine
+from sqlalchemy.orm import declarative_base, sessionmaker
+from pilotcore.config import DATABASE_URL
+
+engine = create_engine(DATABASE_URL)
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+Base = declarative_base()
+
+from app.models import *
diff --git a/DocPilot/backend/app/db/session.py b/DocPilot/backend/app/db/session.py
new file mode 100644
index 0000000000000000000000000000000000000000..52f852bee4d1d7e38deb2da9a7667d0807877f1e
--- /dev/null
+++ b/DocPilot/backend/app/db/session.py
@@ -0,0 +1,11 @@
+from app.db.database import SessionLocal
+
+
+def get_db():
+ db = SessionLocal()
+
+ try:
+ yield db
+
+ finally:
+ db.close()
diff --git a/DocPilot/backend/app/main.py b/DocPilot/backend/app/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..b8a90864add0be87ea7fde8793dc25b0a804adea
--- /dev/null
+++ b/DocPilot/backend/app/main.py
@@ -0,0 +1,52 @@
+from fastapi import FastAPI
+
+from fastapi.middleware.cors import CORSMiddleware
+
+from app.api import (
+ chat,
+ documents,
+ auth,
+ billing,
+)
+
+from app.db.database import (
+ engine,
+ Base,
+)
+
+from app.models import (
+ User,
+ Document,
+ ChatSession,
+ ChatMessage,
+)
+from app.api import history
+
+Base.metadata.create_all(bind=engine)
+
+app = FastAPI()
+
+app.include_router(chat.router, prefix="/chat")
+
+app.include_router(documents.router, prefix="/docs")
+
+app.include_router(auth.router, prefix="/auth")
+
+app.include_router(billing.router, prefix="/billing")
+app.include_router(history.router, prefix="/history")
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+@app.get("/")
+def root():
+
+ return {
+ "status": "running",
+ }
diff --git a/DocPilot/backend/app/models/__init__.py b/DocPilot/backend/app/models/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..39491a73f92e67a254070754e573b396fa454476
--- /dev/null
+++ b/DocPilot/backend/app/models/__init__.py
@@ -0,0 +1,3 @@
+from .user import User
+from .document import Document
+from .chat import ChatSession, ChatMessage
diff --git a/DocPilot/backend/app/models/chat.py b/DocPilot/backend/app/models/chat.py
new file mode 100644
index 0000000000000000000000000000000000000000..972bc7beeb0c00da40d778ccf0b2596889fbc90d
--- /dev/null
+++ b/DocPilot/backend/app/models/chat.py
@@ -0,0 +1,69 @@
+from sqlalchemy import (
+ Column,
+ Integer,
+ Text,
+ String,
+ ForeignKey,
+ DateTime,
+)
+
+from sqlalchemy.sql import func
+
+from app.db.database import Base
+
+
+class ChatSession(Base):
+ __tablename__ = "chat_sessions"
+
+ id = Column(
+ Integer,
+ primary_key=True,
+ index=True,
+ )
+
+ owner_id = Column(
+ Integer,
+ ForeignKey("users.id"),
+ nullable=False,
+ )
+
+ title = Column(
+ String,
+ default="New Chat",
+ )
+
+ created_at = Column(
+ DateTime(timezone=True),
+ server_default=func.now(),
+ )
+
+
+class ChatMessage(Base):
+ __tablename__ = "chat_messages"
+
+ id = Column(
+ Integer,
+ primary_key=True,
+ index=True,
+ )
+
+ session_id = Column(
+ Integer,
+ ForeignKey("chat_sessions.id"),
+ nullable=False,
+ )
+
+ role = Column(
+ Text,
+ nullable=False,
+ )
+
+ content = Column(
+ Text,
+ nullable=False,
+ )
+
+ created_at = Column(
+ DateTime(timezone=True),
+ server_default=func.now(),
+ )
diff --git a/DocPilot/backend/app/models/document.py b/DocPilot/backend/app/models/document.py
new file mode 100644
index 0000000000000000000000000000000000000000..8d322052101d865f9b2f641cdb7085d7b90a08f1
--- /dev/null
+++ b/DocPilot/backend/app/models/document.py
@@ -0,0 +1,29 @@
+from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
+
+from sqlalchemy.sql import func
+
+from app.db.database import Base
+
+
+class Document(Base):
+ __tablename__ = "documents"
+
+ id = Column(Integer, primary_key=True, index=True)
+
+ owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
+
+ filename = Column(String, nullable=False)
+
+ filepath = Column(String, nullable=False)
+
+ file_size = Column(Integer)
+
+ page_count = Column(Integer)
+
+ chunk_count = Column(Integer)
+
+ ocr_used = Column(Boolean, default=False)
+
+ status = Column(String, default="processed")
+
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
diff --git a/DocPilot/backend/app/models/user.py b/DocPilot/backend/app/models/user.py
new file mode 100644
index 0000000000000000000000000000000000000000..4c868ed6e1e235dce2593ef71d33debb18b89716
--- /dev/null
+++ b/DocPilot/backend/app/models/user.py
@@ -0,0 +1,17 @@
+from sqlalchemy import Column, Integer, String
+
+from app.db.database import Base
+
+
+class User(Base):
+ __tablename__ = "users"
+
+ id = Column(Integer, primary_key=True, index=True)
+
+ username = Column(String, unique=True, nullable=False)
+
+ email = Column(String, unique=True, nullable=False)
+
+ hashed_password = Column(String, nullable=False)
+
+ plan = Column(String, default="free")
diff --git a/DocPilot/backend/app/schemas/auth.py b/DocPilot/backend/app/schemas/auth.py
new file mode 100644
index 0000000000000000000000000000000000000000..34b2bf767c4742cf6aca222412a61ff32360a8e5
--- /dev/null
+++ b/DocPilot/backend/app/schemas/auth.py
@@ -0,0 +1,19 @@
+from pydantic import BaseModel
+
+from typing import Optional
+
+
+class UserCreate(BaseModel):
+ username: str
+ email: str
+ password: str
+
+
+class UserLogin(BaseModel):
+ email: str
+ password: str
+
+
+class Token(BaseModel):
+ access_token: str
+ token_type: str
diff --git a/DocPilot/backend/app/schemas/chat.py b/DocPilot/backend/app/schemas/chat.py
new file mode 100644
index 0000000000000000000000000000000000000000..3d65171298f43bbe832aa1349c16968c4f2765bc
--- /dev/null
+++ b/DocPilot/backend/app/schemas/chat.py
@@ -0,0 +1,10 @@
+from pydantic import BaseModel
+
+
+class ChatRequest(BaseModel):
+
+ question: str
+
+ source: str | None = None
+
+ session_id: int | None = None
diff --git a/DocPilot/backend/app/schemas/document.py b/DocPilot/backend/app/schemas/document.py
new file mode 100644
index 0000000000000000000000000000000000000000..ff7452a0338a8ae33e7970d136dc6073ceb6500b
--- /dev/null
+++ b/DocPilot/backend/app/schemas/document.py
@@ -0,0 +1,25 @@
+from pydantic import BaseModel
+from datetime import datetime
+
+
+class DocumentResponse(BaseModel):
+ id: int
+
+ filename: str
+
+ filepath: str
+
+ file_size: int | None = None
+
+ page_count: int | None = None
+
+ chunk_count: int | None = None
+
+ ocr_used: bool
+
+ status: str
+
+ created_at: datetime
+
+ class Config:
+ from_attributes = True
diff --git a/DocPilot/backend/app/schemas/password.py b/DocPilot/backend/app/schemas/password.py
new file mode 100644
index 0000000000000000000000000000000000000000..abcfa75de0ce5a66614a7cc66a441269fad77365
--- /dev/null
+++ b/DocPilot/backend/app/schemas/password.py
@@ -0,0 +1,10 @@
+from pydantic import BaseModel
+
+
+class ForgotPasswordRequest(BaseModel):
+ email: str
+
+
+class ResetPasswordRequest(BaseModel):
+ token: str
+ new_password: str
diff --git a/DocPilot/backend/app/services/ingestion.py b/DocPilot/backend/app/services/ingestion.py
new file mode 100644
index 0000000000000000000000000000000000000000..7804828228c489c25d06637170f5f6c9905a0fc1
--- /dev/null
+++ b/DocPilot/backend/app/services/ingestion.py
@@ -0,0 +1,242 @@
+from pypdf import PdfReader
+
+from pdf2image import convert_from_path
+
+from docx import Document
+
+import pandas as pd
+
+import markdown
+
+import pytesseract
+
+from PIL import Image
+
+import os
+import time
+import requests
+
+from app.services.rag import add_chunks
+from pilotcore.config import TRACEPILOT_URL
+
+pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
+POPPLER_PATH = r"C:\Users\Adhi\Desktop\poppler-26.02.0\Library\bin"
+
+
+def chunk_text(
+ text,
+ chunk_size=500,
+ overlap=50,
+):
+
+ chunks = []
+
+ start = 0
+
+ while start < len(text):
+
+ end = start + chunk_size
+
+ chunks.append(text[start:end])
+
+ start += chunk_size - overlap
+
+ return chunks
+
+
+def extract_pdf_text(
+ file_path,
+):
+
+ reader = PdfReader(file_path)
+
+ full_text = ""
+
+ for page in reader.pages:
+
+ text = page.extract_text() or ""
+
+ full_text += text + "\n"
+
+ return full_text
+
+
+def extract_pdf_ocr(
+ file_path,
+):
+
+ images = convert_from_path(
+ file_path,
+ poppler_path=POPPLER_PATH,
+ )
+
+ text = ""
+
+ for image in images:
+
+ text += pytesseract.image_to_string(image)
+
+ return text
+
+
+def extract_docx_text(
+ file_path,
+):
+
+ doc = Document(file_path)
+
+ return "\n".join(para.text for para in doc.paragraphs)
+
+
+def extract_txt_text(
+ file_path,
+):
+
+ with open(
+ file_path,
+ "r",
+ encoding="utf-8",
+ ) as f:
+
+ return f.read()
+
+
+def extract_md_text(
+ file_path,
+):
+
+ with open(
+ file_path,
+ "r",
+ encoding="utf-8",
+ ) as f:
+
+ return markdown.markdown(f.read())
+
+
+def extract_csv_text(
+ file_path,
+):
+
+ df = pd.read_csv(file_path)
+
+ return df.to_string()
+
+
+def extract_xlsx_text(
+ file_path,
+):
+
+ df = pd.read_excel(file_path)
+
+ return df.to_string()
+
+
+def extract_image_text(
+ file_path,
+):
+
+ image = Image.open(file_path)
+
+ return pytesseract.image_to_string(image)
+
+
+def extract_text(
+ file_path,
+):
+
+ ext = os.path.splitext(file_path)[1].lower()
+
+ if ext == ".pdf":
+
+ text = extract_pdf_text(file_path)
+
+ if len(text.strip()) < 10:
+
+ print("OCR triggered")
+
+ text = extract_pdf_ocr(file_path)
+
+ return text
+
+ elif ext == ".docx":
+
+ return extract_docx_text(file_path)
+
+ elif ext == ".txt":
+
+ return extract_txt_text(file_path)
+
+ elif ext == ".md":
+
+ return extract_md_text(file_path)
+
+ elif ext == ".csv":
+
+ return extract_csv_text(file_path)
+
+ elif ext == ".xlsx":
+
+ return extract_xlsx_text(file_path)
+
+ elif ext in [
+ ".png",
+ ".jpg",
+ ".jpeg",
+ ]:
+
+ return extract_image_text(file_path)
+
+ else:
+
+ raise Exception("Unsupported file type")
+
+
+def process_document(
+ file_path,
+ user_id,
+ document_id,
+):
+
+ start_time = time.perf_counter()
+
+ text = extract_text(file_path)
+
+ text = " ".join(text.split())
+
+ if len(text.split()) < 5:
+ return
+
+ chunks = chunk_text(text)
+
+ all_chunks = [
+ {
+ "document_id": document_id,
+ "text": chunk,
+ "source": os.path.basename(file_path),
+ "page": 1,
+ "chunk_id": i,
+ }
+ for i, chunk in enumerate(chunks)
+ ]
+
+ add_chunks(all_chunks, user_id)
+
+ latency_ms = (time.perf_counter() - start_time) * 1000
+
+ try:
+ requests.post(
+ f"{TRACEPILOT_URL}/tracepilot/ingest/document",
+ json={
+ "document_id": str(document_id),
+ "user_id": str(user_id),
+ "filename": os.path.basename(file_path),
+ "chunk_count": len(all_chunks),
+ "char_count": len(text),
+ "latency_ms": round(latency_ms, 2),
+ "status": "success",
+ },
+ timeout=2,
+ )
+ except Exception:
+ pass
diff --git a/DocPilot/backend/app/services/rag.py b/DocPilot/backend/app/services/rag.py
new file mode 100644
index 0000000000000000000000000000000000000000..d97ea269e4c4158dd6f4224d2f68ebd67295823e
--- /dev/null
+++ b/DocPilot/backend/app/services/rag.py
@@ -0,0 +1,43 @@
+from pilotcore.retrieval.embeddings import get_embedding
+from pilotcore.retrieval.vector_store import add_vector
+from pilotcore.runtime.pipeline import run_pipeline
+
+
+def add_chunks(chunks, user_id):
+ for chunk in chunks:
+ embedding = get_embedding(chunk["text"])
+ add_vector(
+ user_id=user_id,
+ embedding=embedding,
+ text=chunk["text"],
+ source=chunk["source"],
+ page=chunk["page"],
+ chunk_id=chunk["chunk_id"],
+ document_id=chunk["document_id"],
+ )
+
+
+def ask_question(question, user_id, source=None):
+ trace = run_pipeline(
+ query=question,
+ user_id=user_id,
+ source=source,
+ )
+
+ retrieved = trace.retrieval_result.retrieved_chunks
+
+ if not retrieved:
+ return {"answer": "No relevant context found.", "sources": []}
+
+ sources = []
+ seen = set()
+ for item in retrieved:
+ key = (item.chunk.source, item.chunk.page_number)
+ if key not in seen:
+ seen.add(key)
+ sources.append({"source": item.chunk.source, "page": item.chunk.page_number})
+
+ return {
+ "answer": trace.final_response,
+ "sources": sources,
+ }
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..74a5ef7b437faad0d08447a2d1c279ab2d089af7
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,15 @@
+FROM python:3.11
+
+WORKDIR /app
+
+RUN apt-get update && apt-get install -y \
+ tesseract-ocr \
+ poppler-utils
+
+COPY . .
+
+RUN pip install --no-cache-dir -r requirements.txt
+
+EXPOSE 7860
+
+CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..38464a0a8e424a904bf003e793261e4fcb627c91
--- /dev/null
+++ b/README.md
@@ -0,0 +1,273 @@
+---
+title: PilotMaster Backend
+emoji: π
+colorFrom: blue
+colorTo: indigo
+sdk: docker
+app_file: Dockerfile
+app_port: 7860
+pinned: false
+---
+
+# PilotMaster
+
+PilotMaster is an observable AI execution ecosystem built around a single idea: when an AI answers a question, every step of that process should be visible, measurable, and inspectable.
+
+Most RAG applications are black boxes. You upload a document, ask a question, and get an answer. You have no idea which chunks were retrieved, whether the model stayed grounded in the evidence, or whether the response was faithful to the source material. PilotMaster changes that.
+
+---
+
+## What it is
+
+PilotMaster is made up of three layers that work together:
+
+### PilotCore β the execution kernel
+
+The brain of the system. PilotCore owns everything that happens at runtime: embedding documents, searching the vector store, building prompts, calling the LLM, timing spans, and emitting traces. Neither DocPilot nor TracePilot execute RAG themselves β they both delegate to PilotCore. This means the execution logic lives in exactly one place.
+
+### DocPilot β the user-facing product
+
+The application a user actually interacts with. Upload a PDF, DOCX, TXT, CSV, XLSX, or image. Ask questions about it. Get answers with source citations. Manage chat history. DocPilot is the product layer β it handles auth, billing, document management, and chat sessions. It calls PilotCore for everything AI-related.
+
+### TracePilot β the observability layer
+
+The tool an AI engineer opens while DocPilot is running. Every time a user asks a question in DocPilot, TracePilot automatically receives the full execution trace: which chunks were retrieved, what scores they had, what prompt was built, what the model said, how long each step took, and a four-dimensional evaluation of the response quality. TracePilot makes the AI's reasoning visible in real time.
+
+---
+
+## How a single question flows through the system
+
+1. User types a question in DocPilot
+2. DocPilot calls `run_pipeline(query, user_id, source)` in PilotCore
+3. PilotCore embeds the query using `all-mpnet-base-v2` (sentence-transformers, runs locally)
+4. PilotCore searches the user's FAISS vector store for relevant chunks
+5. Irrelevant chunks (L2 distance > 1.4) are filtered out before the LLM sees them
+6. PilotCore builds a prompt β QA-style for direct questions, summarization-style for broad requests
+7. PilotCore calls Groq (`llama-3.1-8b-instant`) for generation
+8. PilotCore runs four-dimensional evaluation on the response
+9. PilotCore emits the full trace to TracePilot via HTTP
+10. User sees the answer in DocPilot. AI engineer sees the trace in TracePilot.
+
+---
+
+## The evaluation system
+
+Every response is judged across four independent dimensions:
+
+**Retrieval Relevance** β did the vector store find chunks that are actually related to the query? Measured by L2 embedding distance. Scores below 0.8 are high, 0.8β1.2 are moderate, above 1.2 are low.
+
+**Grounding Confidence** β did the model answer using the retrieved evidence? Measured by word overlap between the response and the chunks, with stopwords excluded and a length penalty applied to longer responses. If the model correctly says "I don't have enough information", it is rewarded with high grounding.
+
+**Answerability** β did the document actually contain enough to answer this question? Measured by how many query keywords appear in the retrieved chunks. Broad queries like "explain the document" are capped at partial β no document can fully answer an open-ended request.
+
+**Hallucination Risk** β how much of the response went beyond the evidence? Derived from faithfulness score with a length penalty. A short, accurate answer scores low risk. A long response that introduces unsupported facts scores high.
+
+---
+
+## Tech stack
+
+| Layer | Technology |
+| ------------ | ------------------------------------------------------------------------ |
+| LLM | Groq β llama-3.1-8b-instant |
+| Embeddings | sentence-transformers β all-mpnet-base-v2 (local, no server needed) |
+| Vector store | FAISS (per-user, persisted to disk) |
+| Backend | FastAPI (unified server, DocPilot + TracePilot mounted as sub-apps) |
+| Database | PostgreSQL (DocPilot users/docs/chat), SQLite (TracePilot traces) |
+| Frontend | React + Vite (unified app, tab-switched between DocPilot and TracePilot) |
+
+---
+
+## Running it
+
+### Prerequisites
+
+- Python 3.10+
+- PostgreSQL running locally (`rag_saas` database)
+- A Groq API key
+
+### Setup
+
+```bash
+# Install Python dependencies
+pip install -r DocPilot/backend/requirements.txt
+pip install -e .
+
+# Install frontend dependencies
+cd frontend && npm install
+```
+
+### Configure
+
+Copy `.env.example` to `.env` and fill in your values:
+
+```bash
+cp .env.example .env
+```
+
+```env
+GROQ_API_KEY=your_groq_api_key_here
+DATABASE_URL=postgresql://postgres:yourpassword@localhost:5432/rag_saas
+SECRET_KEY=your_secret_key_here
+```
+
+Get a free Groq API key at [console.groq.com](https://console.groq.com).
+
+### Start
+
+```bash
+# Terminal 1 β unified backend (DocPilot + TracePilot on one server)
+uvicorn main:app --reload --port 8000
+
+# Terminal 2 β unified frontend (DocPilot + TracePilot in one window)
+cd frontend && npm run dev
+```
+
+Open `http://localhost:5173`. Sign up, log in, and you're in PilotMaster.
+
+---
+
+## π Production Cloud Deployment
+
+PilotMaster is architected to decouple cleanly across managed infrastructure tiers for production environments:
+
+### 1. Database Tier (Serverless Postgres)
+
+- **Provider:** Neon.tech or Supabase.
+- **Setup:** Provision a managed PostgreSQL instance and use the provided serverless connection string (`postgresql://...`) to replace your local database URL.
+
+### 2. Backend API Substrate (FastAPI Core)
+
+- **Provider:** Render or Railway (Python Web Service Web Tier).
+- **Build Command:** `pip install -r DocPilot/backend/requirements.txt && pip install -e .`
+- **Start Command:** `uvicorn main:app --host 0.0.0.0 --port $PORT`
+- **Environment Variables Required:**
+ - `GROQ_API_KEY`: Production hardware passkey from console.groq.com.
+ - `DATABASE_URL`: Your live cloud connection string.
+ - `SECRET_KEY`: A secure string for handling application auth state.
+
+### 3. Frontend Application Layer (React + Vite static edge)
+
+- **Provider:** Vercel or Netlify.
+- **Root Directory:** `frontend/`
+- **Build Command:** `npm run build`
+- **Output Directory:** `dist`
+- **Environment Variables Required:**
+ - `VITE_API_BASE_URL`: Point this to your live Render/Railway backend domain.
+
+## Project structure
+
+```
+PilotMaster/
+βββ main.py # Unified backend entry point
+βββ .env # Single source of truth for all config
+βββ frontend/ # Unified React app (DocPilot + TracePilot)
+β βββ src/
+β βββ App.jsx # PilotMaster home, auth, routing
+β βββ docpilot/ # DocPilot workspace
+β βββ tracepilot/ # TracePilot workspace
+βββ pilotcore/ # Execution kernel (shared by both apps)
+β βββ config.py # Central config β all services import from here
+β βββ evaluation/ # Multi-dimensional response evaluation
+β βββ generation/ # Groq LLM client + prompt builder
+β βββ retrieval/ # Embeddings, FAISS vector store, retrieval runtime
+β βββ runtime/ # Pipeline orchestration + trace emission
+β βββ schemas/ # Canonical data contracts (Trace, Chunk, Span, etc.)
+β βββ tracing/ # Span timing, trace creation, telemetry
+βββ DocPilot/backend/ # Product layer β auth, billing, documents, chat
+βββ TracePilot/backend/ # Observability layer β trace storage, evaluation, replay
+```
+
+---
+
+## PilotCore β the execution kernel
+
+PilotCore is not an application. It has no UI, no user-facing endpoints, and no opinions about products. It is the runtime substrate that both DocPilot and TracePilot are built on top of.
+
+Every time a user asks a question in DocPilot, it is PilotCore that actually runs. DocPilot calls `run_pipeline(query, user_id, source)` and waits. Everything that happens between that call and the response β embedding, retrieval, filtering, prompt construction, generation, evaluation, and trace emission β happens inside PilotCore.
+
+**What PilotCore owns:**
+
+- **Embeddings** β converts text to vectors using `all-mpnet-base-v2` running locally via sentence-transformers. No external embedding API, no network dependency.
+- **Vector store** β manages per-user FAISS indexes on disk. Each user's documents live in their own isolated index. Supports add, search, reset, and selective deletion by document.
+- **Retrieval runtime** β searches the vector store, returns the top-k chunks by L2 distance, then filters out anything above the relevance threshold before the LLM sees it. For broad queries where everything gets filtered, it falls back to the top 3 chunks so the LLM always has context.
+- **Prompt builder** β detects the query type and builds the appropriate prompt. Direct fact questions get a strict QA prompt. Broad requests like "explain the document" get a summarization prompt.
+- **Generation** β calls Groq's `llama-3.1-8b-instant` with the constructed prompt and returns the response.
+- **Evaluation** β runs four independent evaluators on every response: retrieval relevance, grounding confidence, answerability, and hallucination risk. These are computed from the query, the response, and the retrieved chunks β not from the LLM.
+- **Tracing** β creates a trace for every pipeline run, times each span (retrieval, generation), and emits the full trace payload to TracePilot via HTTP after execution completes.
+- **Schemas** β defines the canonical data contracts used across the system: Trace, Chunk, RetrievedChunk, RetrievalResult, Span, Citation, DocumentMetadata.
+- **Config** β the single source of truth for all environment variables. Every service in the ecosystem imports from `pilotcore/config.py` instead of reading `.env` directly.
+
+The reason this architecture matters is that DocPilot and TracePilot can evolve independently without touching execution logic. If the retrieval strategy changes, the evaluation thresholds are tuned, or the LLM is swapped out, that change happens in PilotCore once and both applications immediately reflect it.
+
+---
+
+## Using DocPilot
+
+DocPilot is the document intelligence interface. Here's what a typical session looks like:
+
+**Sign up and log in.** Your account is tied to a plan β free users can upload up to 3 documents, pro users have no limit. Plan management lives on the PilotMaster home dashboard.
+
+**Upload a document.** Drag and drop a file into the upload area in the sidebar, or click to browse. Supported formats are PDF, DOCX, TXT, MD, CSV, XLSX, PNG, JPG, and JPEG. The moment you hit Upload, PilotCore takes over β it extracts the text, chunks it into 500-character segments with 50-character overlap, embeds every chunk using `all-mpnet-base-v2`, and stores the vectors in a FAISS index scoped to your user account. For scanned PDFs and images, OCR runs automatically via Tesseract.
+
+**Ask questions.** Type a question in the chat input and hit Send or press Enter. DocPilot calls PilotCore's `run_pipeline`, which embeds your query, searches your vector store, filters out irrelevant chunks, builds a prompt, and calls Groq for generation. The answer appears in the chat with source citations showing which file and page the evidence came from.
+
+**Manage conversations.** Every chat session is saved and listed in the sidebar. Click any session to reload its full message history. Delete sessions you no longer need with the β button. Start a fresh conversation with + New Chat.
+
+**Reset your vector store.** The Reset button in the header clears your entire FAISS index. Use this when you want to start fresh with a new set of documents.
+
+**Jump to TracePilot.** The TracePilot β button in the header takes you directly to the observability dashboard so you can inspect the trace for the question you just asked.
+
+---
+
+## Using TracePilot
+
+TracePilot is the execution intelligence dashboard. It's designed to be open alongside DocPilot β every question asked in DocPilot automatically appears here within seconds.
+
+**The trace list.** The left sidebar shows every query that has run through PilotCore, newest first. Each entry shows the query text, a color-coded retrieval relevance tag (green = high, orange = moderate, red = low), and additional tags if the query was unanswerable or the model abstained. Latency is shown inline on the right.
+
+**Inspecting a trace.** Click any trace to open the full detail view. At the top you'll see the four evaluation dimensions:
+
+- **retrieval** β how semantically close were the retrieved chunks to the query?
+- **grounding** β how much of the response came from the retrieved evidence?
+- **answerability** β did the document actually contain enough to answer this?
+- **hallucination risk** β how much did the response go beyond the evidence?
+
+Below the tags you'll see the full response, the retrieved chunks with their L2 distance scores (color-coded), and a metrics panel showing faithfulness score, query coverage, query type, and chunk count.
+
+**Replay.** Every trace has a Replay button. Clicking it re-runs the exact same query through PilotCore using the same user's vector store, producing a new trace linked to the original via `parent_trace_id`. This lets you compare how the same question performs across different document states or after model changes.
+
+**Execution identity.** Every trace carries a complete version snapshot of the system that produced it β `evaluator_version`, `prompt_version`, and `retriever_version`. This means when you replay a trace and the scores change, you can tell whether the difference came from the AI behaving differently or from the evaluation system itself having changed. A hallucination score from evaluator v1 is not the same thing as a hallucination score from evaluator v2, and TracePilot makes that distinction visible.
+
+**Live updates.** TracePilot polls for new traces every 3 seconds. You don't need to refresh β ask a question in DocPilot, switch to TracePilot, and the trace appears automatically.
+
+**The stats bar.** The header shows live aggregate stats across all traces: total trace count, average latency, grounded count, and ungrounded count. These update as new traces arrive.
+
+**Jump to DocPilot.** The DocPilot β button in the header takes you back to the chat interface without losing your place.
+
+---
+
+## What makes this different
+
+Most RAG demos are single-file scripts. PilotMaster is architected as a production-grade platform with a clear separation between the execution kernel, the product layer, and the observability layer. The same principles that make distributed systems observable β traces, spans, telemetry β are applied to AI execution.
+
+The result is a system where you can watch the AI think. Not just see the answer, but understand exactly how it arrived there, whether it stayed grounded, and where it might have gone wrong.
+
+---
+
+## Architectural honesty β what this is and what it isnβt
+
+PilotMaster is an early-stage AI runtime and observability platform. It is not a production system at scale. Being honest about the current state and the intended direction is part of what makes the architecture credible.
+
+**The evaluators are currently heuristic-based.** Grounding, answerability, and hallucination risk are all computed from lexical overlap β word matching between the response and the retrieved chunks. This is fast, deterministic, and inspectable, which makes it a good foundation. But lexical heuristics have a ceiling. The natural evolution here is toward embedding-based semantic evaluation, LLM-as-a-judge scoring, and eventually evaluator ensembles that combine multiple signals. The current evaluators are v1 β they are designed to be replaced, which is exactly why `evaluator_version` is attached to every trace.
+
+**Retrieval is vector similarity only.** FAISS with L2 distance is the entire retrieval stack right now. This works well for focused factual queries but shows weakness on broad or ambiguous ones β a query like "elaborate the document" has no semantic anchor in the chunk space, so the system falls back to top-k regardless of score. The next retrieval evolution is hybrid search: combining dense vector retrieval with sparse keyword matching (BM25), and adding a cross-encoder reranker to re-score the top candidates before they reach the LLM. That would eliminate the contamination problem where an irrelevant chunk sneaks through because it happened to be the closest vector.
+
+**Trace storage is operationally thin.** TracePilot currently stores traces in SQLite and polls for new ones every 3 seconds. This is fine for a single-user development environment. At scale, this becomes a bottleneck β SQLite doesnβt support concurrent writes, polling is wasteful, and thereβs no indexing on spans or evaluation fields. The intended direction is a proper trace store with queryable spans, aggregation pipelines, trace retention policies, and real-time streaming via WebSockets or Server-Sent Events instead of polling.
+
+**Prompt versioning is manual.** Right now, bumping `prompt_version` in `pilotcore/config.py` is a manual operation. There is no prompt registry, no A/B testing infrastructure, and no way to compare prompt performance across a dataset. The intended direction is a prompt management layer inside PilotCore where prompt templates are named, versioned, and stored β so TracePilot can show not just what the response was, but exactly which prompt template produced it and how that template has performed historically.
+
+**Evaluator versioning exists but isnβt automated.** Version constants live in `pilotcore/config.py` and are attached to every trace. When you change the evaluation logic and bump the version, all future traces carry the new version and old traces retain the old one. What doesnβt exist yet is automated detection β the system wonβt warn you if you change the evaluator without bumping the version. Thatβs a future guardrail.
+
+**Trace lineage is shallow.** `parent_trace_id` links a replay to its original trace, which is the beginning of execution lineage. Whatβs missing is deeper lineage β tracking which document version was active, which evaluator version scored it, which prompt template was used, and how all of those evolved over time. Full execution lineage is what turns a trace store into a genuine audit trail for AI behavior.
+
+These are not oversights. They are the known frontier of the system β the places where the architecture is intentionally designed to grow.
diff --git a/TracePilot/.gitignore b/TracePilot/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..1c3c7eaf496de9e76d9dc1dc6c9ae158b67d7259
--- /dev/null
+++ b/TracePilot/.gitignore
@@ -0,0 +1,5 @@
+venv/
+.env
+__pycache__/
+*.pyc
+tracepilot.db
\ No newline at end of file
diff --git a/TracePilot/backend/Dockerfile b/TracePilot/backend/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/TracePilot/backend/app/__init__.py b/TracePilot/backend/app/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/TracePilot/backend/app/analytics/failure_detector.py b/TracePilot/backend/app/analytics/failure_detector.py
new file mode 100644
index 0000000000000000000000000000000000000000..0551744bc73cf86567e6fb1f13bdcd57f71d1f65
--- /dev/null
+++ b/TracePilot/backend/app/analytics/failure_detector.py
@@ -0,0 +1,30 @@
+from app.tracing.trace_manager import get_traces
+
+
+def detect_failures():
+
+ traces = get_traces()
+
+ failures = []
+
+ for trace in traces:
+
+ reasons = []
+
+ if trace.retrieval_quality == "poor":
+ reasons.append("poor_retrieval")
+
+ if trace.latency > 2000:
+ reasons.append("high_latency")
+
+ if reasons:
+
+ failures.append({
+ "trace_id": trace.trace_id,
+ "query": trace.query,
+ "reasons": reasons,
+ "latency": trace.latency,
+ "retrieval_quality": trace.retrieval_quality
+ })
+
+ return failures
diff --git a/TracePilot/backend/app/api/__init__.py b/TracePilot/backend/app/api/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/TracePilot/backend/app/core/config.py b/TracePilot/backend/app/core/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..c885b3e779a14a2861f34a990812e6eeefe231e0
--- /dev/null
+++ b/TracePilot/backend/app/core/config.py
@@ -0,0 +1 @@
+from pilotcore.config import GROQ_API_KEY, GROQ_MODEL, TRACEPILOT_URL, DOCPILOT_URL
diff --git a/TracePilot/backend/app/core/llm.py b/TracePilot/backend/app/core/llm.py
new file mode 100644
index 0000000000000000000000000000000000000000..06faaf0de7893c803d217cbe19332a4fc7c5b8a6
--- /dev/null
+++ b/TracePilot/backend/app/core/llm.py
@@ -0,0 +1 @@
+from pilotcore.generation.generator import generate_response
diff --git a/TracePilot/backend/app/db/database.py b/TracePilot/backend/app/db/database.py
new file mode 100644
index 0000000000000000000000000000000000000000..1095590aefa5a6c3e14fcd7906306bc030970a5d
--- /dev/null
+++ b/TracePilot/backend/app/db/database.py
@@ -0,0 +1,61 @@
+import sqlite3
+
+DB_PATH = "tracepilot.db"
+
+
+def get_connection():
+ conn = sqlite3.connect(DB_PATH)
+ conn.row_factory = sqlite3.Row
+ return conn
+
+
+def init_db():
+
+ conn = get_connection()
+ cursor = conn.cursor()
+
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS traces (
+ trace_id TEXT PRIMARY KEY,
+ query TEXT,
+ retrieved_chunks TEXT,
+ prompt TEXT,
+ response TEXT,
+ latency REAL,
+ timestamp TEXT,
+ model_name TEXT,
+ retrieval_score_avg REAL,
+ response_length INTEGER,
+ chunk_count INTEGER,
+ parent_trace_id TEXT,
+ retrieval_quality TEXT,
+ grounded BOOLEAN,
+ top_retrieval_score REAL,
+ spans TEXT,
+ failure_types TEXT,
+ prompt_mode TEXT DEFAULT 'strict',
+ evaluation TEXT,
+ user_id TEXT,
+ source TEXT,
+ evaluator_version TEXT DEFAULT '1.0',
+ prompt_version TEXT DEFAULT '1.0',
+ retriever_version TEXT DEFAULT 'vector_v1'
+ )
+ """)
+
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS ingestion_traces (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ document_id TEXT,
+ user_id TEXT,
+ filename TEXT,
+ chunk_count INTEGER,
+ char_count INTEGER,
+ latency_ms REAL,
+ status TEXT,
+ timestamp TEXT
+ )
+ """)
+
+ conn.commit()
+ conn.close()
diff --git a/TracePilot/backend/app/evaluation/evaluator.py b/TracePilot/backend/app/evaluation/evaluator.py
new file mode 100644
index 0000000000000000000000000000000000000000..8f0f4ce6778ccbf3c193dc524a77c8a05d633e7a
--- /dev/null
+++ b/TracePilot/backend/app/evaluation/evaluator.py
@@ -0,0 +1,16 @@
+from pilotcore.evaluation.evaluator import run_evaluation
+
+
+class Evaluator:
+
+ def evaluate(self, query, response, chunks):
+ scores = [c.get("score", 0) if isinstance(c, dict) else c.score for c in chunks]
+ result = run_evaluation(query=query, response=response, chunks=chunks, scores=scores)
+ # Map to legacy keys pipeline_runner still uses
+ return {
+ "grounded": result.get("grounded", False),
+ "hallucination_score": 1.0 - result.get("faithfulness_score", 0.0),
+ "faithfulness_score": result.get("faithfulness_score", 0.0),
+ "abstained": result.get("abstained", False),
+ **result,
+ }
diff --git a/TracePilot/backend/app/main.py b/TracePilot/backend/app/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..63033746988d3d380c91fc8a53131edde372e014
--- /dev/null
+++ b/TracePilot/backend/app/main.py
@@ -0,0 +1,216 @@
+from fastapi import FastAPI, HTTPException
+from fastapi.middleware.cors import CORSMiddleware
+from pydantic import BaseModel
+from typing import List, Optional
+from datetime import datetime
+
+from app.db.database import init_db
+from app.pipelines.pipeline_runner import PipelineRunner
+from app.tracing.trace_manager import get_traces, get_trace_by_id, save_trace
+from app.tracing.replay import replay_trace as run_replay
+from app.analytics.failure_detector import detect_failures
+from app.models.trace import Trace, RetrievedChunk
+
+app = FastAPI()
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+init_db()
+
+
+class QueryRequest(BaseModel):
+ query: str
+ prompt_mode: str = "strict"
+
+
+class IngestChunk(BaseModel):
+ chunk_id: str
+ text: str
+ score: float
+ rank: int
+
+
+class IngestRequest(BaseModel):
+ trace_id: str
+ query: str
+ response: str
+ prompt: str
+ latency: float
+ model_name: str
+ retrieved_chunks: List[IngestChunk]
+ retrieval_score_avg: float
+ top_retrieval_score: float
+ chunk_count: int
+ response_length: int
+ retrieval_quality: str
+ grounded: bool
+ evaluation: Optional[dict] = None
+ spans: list = []
+ failure_types: list = []
+ prompt_mode: str = "strict"
+ parent_trace_id: Optional[str] = None
+ user_id: Optional[str] = None
+ source: Optional[str] = None
+ evaluator_version: Optional[str] = "1.0"
+ prompt_version: Optional[str] = "1.0"
+ retriever_version: Optional[str] = "vector_v1"
+
+
+class EventRequest(BaseModel):
+ event_type: str
+ payload: dict
+
+
+class IngestDocumentRequest(BaseModel):
+ document_id: str
+ user_id: str
+ filename: str
+ chunk_count: int
+ char_count: int
+ latency_ms: float
+ status: str
+
+
+@app.post("/ingest/document")
+def ingest_document(request: IngestDocumentRequest):
+ from app.db.database import get_connection
+ conn = get_connection()
+ cursor = conn.cursor()
+ cursor.execute("""
+ INSERT INTO ingestion_traces
+ (document_id, user_id, filename, chunk_count, char_count, latency_ms, status, timestamp)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ """, (
+ request.document_id,
+ request.user_id,
+ request.filename,
+ request.chunk_count,
+ request.char_count,
+ request.latency_ms,
+ request.status,
+ str(datetime.utcnow()),
+ ))
+ conn.commit()
+ conn.close()
+ return {"status": "ok", "document_id": request.document_id}
+
+
+@app.get("/ingestion-traces")
+def get_ingestion_traces():
+ from app.db.database import get_connection
+ conn = get_connection()
+ cursor = conn.cursor()
+ cursor.execute("SELECT * FROM ingestion_traces ORDER BY timestamp DESC")
+ rows = cursor.fetchall()
+ conn.close()
+ return [dict(row) for row in rows]
+
+
+@app.post("/ingest")
+def ingest_trace(request: IngestRequest):
+ trace = Trace(
+ trace_id=request.trace_id,
+ query=request.query,
+ retrieved_chunks=[
+ RetrievedChunk(**c.dict()) for c in request.retrieved_chunks
+ ],
+ prompt=request.prompt,
+ response=request.response,
+ latency=request.latency,
+ timestamp=datetime.utcnow(),
+ model_name=request.model_name,
+ retrieval_score_avg=request.retrieval_score_avg,
+ response_length=request.response_length,
+ chunk_count=request.chunk_count,
+ parent_trace_id=request.parent_trace_id,
+ retrieval_quality=request.retrieval_quality,
+ grounded=request.grounded,
+ top_retrieval_score=request.top_retrieval_score,
+ spans=request.spans,
+ failure_types=request.failure_types,
+ prompt_mode=request.prompt_mode,
+ evaluation=request.evaluation or {},
+ user_id=request.user_id,
+ source=request.source,
+ evaluator_version=request.evaluator_version or "1.0",
+ prompt_version=request.prompt_version or "1.0",
+ retriever_version=request.retriever_version or "vector_v1",
+ )
+ save_trace(trace)
+ return {"status": "ok", "trace_id": trace.trace_id}
+
+
+@app.post("/events")
+def receive_event(request: EventRequest):
+ return {"status": "ok"}
+
+
+@app.post("/ask")
+def ask_question(request: QueryRequest):
+ runner = PipelineRunner()
+ return runner.run(request.query, prompt_mode=request.prompt_mode)
+
+
+@app.get("/analytics/failures")
+def get_failures():
+ return detect_failures()
+
+
+@app.get("/traces")
+def get_all_traces(retrieval_quality: str | None = None):
+ return get_traces(retrieval_quality)
+
+
+@app.get("/traces/compare")
+def compare_traces(trace_id_1: str, trace_id_2: str):
+ trace_1 = get_trace_by_id(trace_id_1)
+ trace_2 = get_trace_by_id(trace_id_2)
+
+ if isinstance(trace_1, dict):
+ raise HTTPException(status_code=404, detail="First trace not found")
+ if isinstance(trace_2, dict):
+ raise HTTPException(status_code=404, detail="Second trace not found")
+
+ return {
+ "trace_1": {
+ "trace_id": trace_1.trace_id,
+ "model_name": trace_1.model_name,
+ "latency": trace_1.latency,
+ "retrieval_score_avg": trace_1.retrieval_score_avg,
+ "response_length": trace_1.response_length,
+ "chunk_count": trace_1.chunk_count,
+ },
+ "trace_2": {
+ "trace_id": trace_2.trace_id,
+ "model_name": trace_2.model_name,
+ "latency": trace_2.latency,
+ "retrieval_score_avg": trace_2.retrieval_score_avg,
+ "response_length": trace_2.response_length,
+ "chunk_count": trace_2.chunk_count,
+ },
+ "differences": {
+ "latency_delta": round(trace_2.latency - trace_1.latency, 2),
+ "retrieval_score_delta": round(trace_2.retrieval_score_avg - trace_1.retrieval_score_avg, 2),
+ "response_length_delta": trace_2.response_length - trace_1.response_length,
+ "response_changed": trace_1.response != trace_2.response,
+ },
+ }
+
+
+@app.get("/traces/{trace_id}")
+def fetch_trace(trace_id: str):
+ return get_trace_by_id(trace_id)
+
+
+@app.post("/traces/{trace_id}/replay")
+def replay_trace_endpoint(trace_id: str):
+ result = run_replay(trace_id)
+ if "error" in result:
+ raise HTTPException(status_code=404, detail=result["error"])
+ return result
diff --git a/TracePilot/backend/app/models/trace.py b/TracePilot/backend/app/models/trace.py
new file mode 100644
index 0000000000000000000000000000000000000000..ee0eb4a7d8572fd36e4bedb28d4aa6715b7fe4c8
--- /dev/null
+++ b/TracePilot/backend/app/models/trace.py
@@ -0,0 +1,42 @@
+from pydantic import BaseModel
+from typing import List
+from uuid import uuid4
+from datetime import datetime
+
+
+class RetrievedChunk(BaseModel):
+ chunk_id: str
+ text: str
+ score: float
+ rank: int
+
+
+class Trace(BaseModel):
+ trace_id: str
+ query: str
+ retrieved_chunks: List[RetrievedChunk]
+ prompt: str
+ response: str
+ latency: float
+ timestamp: datetime
+ model_name: str
+ retrieval_score_avg: float
+ response_length: int
+ chunk_count: int
+ parent_trace_id: str | None = None
+ retrieval_quality: str
+ grounded: bool
+ top_retrieval_score: float = 0.0
+ prompt_mode: str = "strict"
+ spans: list = []
+ failure_types: list = []
+ evaluation: dict = {}
+ user_id: str | None = None
+ source: str | None = None
+ evaluator_version: str = "1.0"
+ prompt_version: str = "1.0"
+ retriever_version: str = "vector_v1"
+
+ @staticmethod
+ def create_id():
+ return str(uuid4())
diff --git a/TracePilot/backend/app/pipelines/pipeline_runner.py b/TracePilot/backend/app/pipelines/pipeline_runner.py
new file mode 100644
index 0000000000000000000000000000000000000000..6814cafb49db60a5163f195673e1b67b32e1bc85
--- /dev/null
+++ b/TracePilot/backend/app/pipelines/pipeline_runner.py
@@ -0,0 +1,12 @@
+from pilotcore.runtime.pipeline import run_pipeline
+
+
+class PipelineRunner:
+
+ def run(self, query, parent_trace_id=None, prompt_mode="strict"):
+ trace = run_pipeline(query=query)
+ return {
+ "trace_id": trace.trace_id,
+ "response": trace.final_response,
+ "parent_trace_id": parent_trace_id,
+ }
diff --git a/TracePilot/backend/app/tracing/replay.py b/TracePilot/backend/app/tracing/replay.py
new file mode 100644
index 0000000000000000000000000000000000000000..23577b6f76dbec6a900704d9d232a9511d558071
--- /dev/null
+++ b/TracePilot/backend/app/tracing/replay.py
@@ -0,0 +1,24 @@
+from app.tracing.trace_manager import get_trace
+
+
+def replay_trace(trace_id: str):
+
+ original_trace = get_trace(trace_id)
+
+ if not original_trace:
+ return {"error": "Trace not found"}
+
+ from pilotcore.runtime.pipeline import run_pipeline
+
+ trace = run_pipeline(
+ query=original_trace["query"],
+ user_id=original_trace.get("user_id"),
+ source=original_trace.get("source"),
+ )
+
+ return {
+ "trace_id": trace.trace_id,
+ "query": trace.user_query,
+ "response": trace.final_response,
+ "parent_trace_id": trace_id,
+ }
diff --git a/TracePilot/backend/app/tracing/trace_manager.py b/TracePilot/backend/app/tracing/trace_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..7900269ea9b1c87b22efac62ab84e86a42599635
--- /dev/null
+++ b/TracePilot/backend/app/tracing/trace_manager.py
@@ -0,0 +1,137 @@
+import json
+
+from app.db.database import get_connection
+from app.models.trace import Trace, RetrievedChunk
+
+
+def save_trace(trace: Trace):
+
+ conn = get_connection()
+ cursor = conn.cursor()
+
+ cursor.execute("""
+ INSERT INTO traces (
+ trace_id, query, retrieved_chunks, prompt, response, latency,
+ timestamp, model_name, retrieval_score_avg, response_length,
+ chunk_count, parent_trace_id, retrieval_quality, grounded,
+ top_retrieval_score, spans, failure_types, prompt_mode, evaluation,
+ user_id, source, evaluator_version, prompt_version, retriever_version
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """, (
+ trace.trace_id, trace.query,
+ json.dumps([chunk.dict() for chunk in trace.retrieved_chunks]),
+ trace.prompt, trace.response, trace.latency, str(trace.timestamp),
+ trace.model_name, trace.retrieval_score_avg, trace.response_length,
+ trace.chunk_count, trace.parent_trace_id, trace.retrieval_quality,
+ trace.grounded, trace.top_retrieval_score,
+ json.dumps(trace.spans), json.dumps(trace.failure_types),
+ trace.prompt_mode, json.dumps(trace.evaluation),
+ trace.user_id, trace.source,
+ trace.evaluator_version, trace.prompt_version, trace.retriever_version
+ ))
+
+ conn.commit()
+ conn.close()
+
+
+def _row_to_trace(row) -> Trace:
+ return Trace(
+ trace_id=row["trace_id"],
+ query=row["query"],
+ retrieved_chunks=[
+ RetrievedChunk(**chunk)
+ for chunk in json.loads(row["retrieved_chunks"])
+ ],
+ prompt=row["prompt"],
+ response=row["response"],
+ latency=row["latency"],
+ timestamp=row["timestamp"],
+ model_name=row["model_name"],
+ retrieval_score_avg=row["retrieval_score_avg"],
+ response_length=row["response_length"],
+ chunk_count=row["chunk_count"],
+ parent_trace_id=row["parent_trace_id"],
+ retrieval_quality=row["retrieval_quality"],
+ grounded=row["grounded"],
+ top_retrieval_score=row["top_retrieval_score"],
+ spans=json.loads(row["spans"] or "[]"),
+ failure_types=json.loads(row["failure_types"] or "[]"),
+ prompt_mode=row["prompt_mode"] or "strict",
+ evaluation=json.loads(row["evaluation"] or "{}"),
+ user_id=row["user_id"],
+ source=row["source"],
+ evaluator_version=row["evaluator_version"] or "1.0",
+ prompt_version=row["prompt_version"] or "1.0",
+ retriever_version=row["retriever_version"] or "vector_v1",
+ )
+
+
+def get_traces(retrieval_quality=None):
+
+ conn = get_connection()
+ cursor = conn.cursor()
+
+ if retrieval_quality:
+ cursor.execute(
+ "SELECT * FROM traces WHERE retrieval_quality = ? ORDER BY timestamp DESC",
+ (retrieval_quality,)
+ )
+ else:
+ cursor.execute("SELECT * FROM traces ORDER BY timestamp DESC")
+
+ rows = cursor.fetchall()
+ conn.close()
+
+ return [_row_to_trace(row) for row in rows]
+
+
+def get_trace_by_id(trace_id: str):
+
+ conn = get_connection()
+ cursor = conn.cursor()
+ cursor.execute("SELECT * FROM traces WHERE trace_id = ?", (trace_id,))
+ row = cursor.fetchone()
+ conn.close()
+
+ if not row:
+ return {"error": "Trace not found"}
+
+ return _row_to_trace(row)
+
+
+def get_trace(trace_id: str) -> dict | None:
+
+ conn = get_connection()
+ cursor = conn.cursor()
+ cursor.execute("SELECT * FROM traces WHERE trace_id = ?", (trace_id,))
+ row = cursor.fetchone()
+ conn.close()
+
+ if not row:
+ return None
+
+ return {
+ "trace_id": row["trace_id"],
+ "query": row["query"],
+ "retrieved_chunks": json.loads(row["retrieved_chunks"]),
+ "prompt": row["prompt"],
+ "response": row["response"],
+ "latency": row["latency"],
+ "timestamp": row["timestamp"],
+ "model_name": row["model_name"],
+ "retrieval_score_avg": row["retrieval_score_avg"],
+ "response_length": row["response_length"],
+ "chunk_count": row["chunk_count"],
+ "parent_trace_id": row["parent_trace_id"],
+ "retrieval_quality": row["retrieval_quality"],
+ "grounded": row["grounded"],
+ "top_retrieval_score": row["top_retrieval_score"],
+ "spans": json.loads(row["spans"] or "[]"),
+ "failure_types": json.loads(row["failure_types"] or "[]"),
+ "user_id": row["user_id"],
+ "source": row["source"],
+ "evaluator_version": row["evaluator_version"] or "1.0",
+ "prompt_version": row["prompt_version"] or "1.0",
+ "retriever_version": row["retriever_version"] or "vector_v1",
+ }
diff --git a/TracePilot/backend/data/knowledge_base.txt b/TracePilot/backend/data/knowledge_base.txt
new file mode 100644
index 0000000000000000000000000000000000000000..c565e2c610d8b8cd87417e38e0c3ccdf90b61693
--- /dev/null
+++ b/TracePilot/backend/data/knowledge_base.txt
@@ -0,0 +1,15 @@
+Tawang is a town in Arunachal Pradesh in northeastern India.
+
+Tawang Monastery is one of the largest Buddhist monasteries in India.
+
+Visitors usually travel to Tawang from Guwahati via Tezpur.
+
+The best time to visit Tawang is from March to October.
+
+An Inner Line Permit is required for Indian citizens visiting Arunachal Pradesh.
+
+Sela Pass is a famous mountain pass on the route to Tawang.
+
+Madhuri Lake is a popular tourist attraction near Tawang.
+
+Travelers visiting high-altitude areas should prepare for cold weather and altitude sickness.
diff --git a/TracePilot/backend/requirements.txt b/TracePilot/backend/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..bc60d2eaab0e2e0028c1054bb307f7dd91f742ff
Binary files /dev/null and b/TracePilot/backend/requirements.txt differ
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..834d338dce7896325c89e1100a7b9aba9c4103d6
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,61 @@
+services:
+
+ postgres:
+ image: postgres:16
+ environment:
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: root
+ POSTGRES_DB: rag_saas
+ ports:
+ - "5432:5432"
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+
+ docpilot-backend:
+ build: ./DocPilot/backend
+ ports:
+ - "8000:8000"
+ volumes:
+ - ./DocPilot/backend:/app
+ - ./pilotcore:/app/pilotcore
+ - ./.env:/.env
+ env_file:
+ - .env
+ depends_on:
+ postgres:
+ condition: service_healthy
+
+ docpilot-frontend:
+ build: ./DocPilot/frontend
+ ports:
+ - "5173:5173"
+ depends_on:
+ - docpilot-backend
+
+ tracepilot-backend:
+ build: ./TracePilot/backend
+ ports:
+ - "8001:8001"
+ volumes:
+ - ./TracePilot/backend:/app
+ - ./pilotcore:/app/pilotcore
+ - ./.env:/.env
+ env_file:
+ - .env
+ depends_on:
+ - docpilot-backend
+
+ tracepilot-frontend:
+ build: ./TracePilot/frontend
+ ports:
+ - "5174:5174"
+ depends_on:
+ - tracepilot-backend
+
+volumes:
+ postgres_data:
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..34639bc9318338045090151b29d8b4d3899d6ca5
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ PilotMaster
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..e9889bd382f5cd581319bfc41475235bfbdc2fea
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,1914 @@
+{
+ "name": "pilotmaster-frontend",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "pilotmaster-frontend",
+ "version": "1.0.0",
+ "dependencies": {
+ "axios": "^1.6.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-dropzone": "^14.3.8"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^4.2.0",
+ "vite": "^5.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.3",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
+ "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
+ "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
+ "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
+ "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
+ "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
+ "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
+ "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
+ "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
+ "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
+ "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
+ "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
+ "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
+ "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
+ "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
+ "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
+ "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
+ "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
+ "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
+ "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
+ "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
+ "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
+ "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
+ "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
+ "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
+ "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
+ "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
+ "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
+ "node_modules/attr-accept": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
+ "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/axios": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz",
+ "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
+ "dependencies": {
+ "follow-redirects": "^1.16.0",
+ "form-data": "^4.0.5",
+ "https-proxy-agent": "^5.0.1",
+ "proxy-from-env": "^2.1.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.31",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz",
+ "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==",
+ "dev": true,
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001793",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
+ "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ]
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.359",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.359.tgz",
+ "integrity": "sha512-8lPELWuYZIWk7NDvCNthtmMw/7Q5Wu25NpM4djFMHBmk8DubPAtL4YTOp7ou0e7HyJtwkVlWv8XMLURnrtgJQw==",
+ "dev": true
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/file-selector": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
+ "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
+ "dependencies": {
+ "tslib": "^2.7.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.44",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz",
+ "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==",
+ "dev": true
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true
+ },
+ "node_modules/postcss": {
+ "version": "8.5.14",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+ "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-dropzone": {
+ "version": "14.4.1",
+ "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.4.1.tgz",
+ "integrity": "sha512-QDuV76v3uKbHiH34SpwifZ+gOLi1+RdsCO1kl5vxMT4wW8R82+sthjvBw4th3NHF/XX6FBsqDYZVNN+pnhaw0g==",
+ "dependencies": {
+ "attr-accept": "^2.2.4",
+ "file-selector": "^2.1.0",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">= 10.13"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8 || 18.0.0"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
+ "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.4",
+ "@rollup/rollup-android-arm64": "4.60.4",
+ "@rollup/rollup-darwin-arm64": "4.60.4",
+ "@rollup/rollup-darwin-x64": "4.60.4",
+ "@rollup/rollup-freebsd-arm64": "4.60.4",
+ "@rollup/rollup-freebsd-x64": "4.60.4",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.4",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.4",
+ "@rollup/rollup-linux-arm64-musl": "4.60.4",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.4",
+ "@rollup/rollup-linux-loong64-musl": "4.60.4",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.4",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.4",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.4",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.4",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.4",
+ "@rollup/rollup-linux-x64-gnu": "4.60.4",
+ "@rollup/rollup-linux-x64-musl": "4.60.4",
+ "@rollup/rollup-openbsd-x64": "4.60.4",
+ "@rollup/rollup-openharmony-arm64": "4.60.4",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.4",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.4",
+ "@rollup/rollup-win32-x64-gnu": "4.60.4",
+ "@rollup/rollup-win32-x64-msvc": "4.60.4",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..aea636577f04830eec6972348fec7edbe500ce62
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "pilotmaster-frontend",
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "axios": "^1.6.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-dropzone": "^14.3.8"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^4.2.0",
+ "vite": "^5.0.0"
+ }
+}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..344f12f526cf20b849b313bc6deed67ed0f66fd8
--- /dev/null
+++ b/frontend/src/App.jsx
@@ -0,0 +1,313 @@
+import { useEffect, useState } from "react";
+import { apiRequest, loginRequest } from "./docpilot/api.js";
+import DocPilotDashboard from "./docpilot/pages/Dashboard.jsx";
+import TraceExplorer from "./tracepilot/TraceExplorer.jsx";
+
+export default function App() {
+ const [auth, setAuth] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [screen, setScreen] = useState("login");
+ const [username, setUsername] = useState("");
+ const [plan, setPlan] = useState("free");
+
+ useEffect(() => {
+ const validate = async () => {
+ const token = localStorage.getItem("token");
+ if (!token) { setLoading(false); return; }
+ try {
+ const data = await apiRequest("/auth/me");
+ if (data.email) {
+ setAuth(true);
+ setUsername(data.username);
+ setPlan(data.plan);
+ setScreen("home");
+ } else {
+ localStorage.removeItem("token");
+ }
+ } catch {
+ localStorage.removeItem("token");
+ }
+ setLoading(false);
+ };
+ validate();
+ }, []); // runs once only on mount
+
+ const logout = () => {
+ localStorage.removeItem("token");
+ setAuth(false);
+ setScreen("login");
+ setUsername("");
+ };
+
+ const onLogin = async () => {
+ const data = await apiRequest("/auth/me");
+ setUsername(data.username);
+ setPlan(data.plan);
+ setAuth(true);
+ setScreen("home");
+ };
+
+ if (loading) return ;
+
+ if (!auth) {
+ if (screen === "signup") return setScreen("login")} />;
+ if (screen === "forgot") return setScreen("login")} />;
+ return setScreen("signup")} goToForgot={() => setScreen("forgot")} />;
+ }
+
+ if (screen === "docpilot") return (
+ setScreen("home")}
+ onTracePilot={() => setScreen("tracepilot")}
+ />
+ );
+
+ if (screen === "tracepilot") return (
+ setScreen("home")}
+ onDocPilot={() => setScreen("docpilot")}
+ />
+ );
+
+ return ;
+}
+
+// βββ HOME ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function PilotMasterHome({ username, plan, onOpen, onLogout }) {
+ const [currentPlan, setCurrentPlan] = useState(plan);
+
+ const upgradePlan = async () => {
+ try {
+ const data = await apiRequest("/billing/upgrade", "POST");
+ setCurrentPlan(data.plan);
+ } catch { alert("Upgrade failed"); }
+ };
+
+ const downgradePlan = async () => {
+ try {
+ const data = await apiRequest("/billing/downgrade", "POST");
+ setCurrentPlan(data.plan);
+ } catch { alert("Downgrade failed"); }
+ };
+ return (
+
+ {/* TOP BAR */}
+
+
+
PilotMaster
+
observable AI execution ecosystem
+
+
+
+
{username}
+
{currentPlan}
+
+ {currentPlan === "free" ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {/* CENTER */}
+
+
select a workspace
+
+
onOpen("docpilot")}
+ accent="#4caf50"
+ />
+ onOpen("tracepilot")}
+ accent="#7c4dff"
+ />
+
+
+
+ {/* FOOTER */}
+
+
PilotMaster Β· execution kernel: PilotCore
+
llama-3.1-8b-instant Β· all-mpnet-base-v2
+
+
+ );
+}
+
+function ProductCard({ name, description, tags, onClick, accent }) {
+ const [hovered, setHovered] = useState(false);
+ return (
+ setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ style={{
+ width: "320px", padding: "28px", borderRadius: "14px", cursor: "pointer",
+ background: hovered ? "#141414" : "#111",
+ border: `1px solid ${hovered ? "#2a2a2a" : "#1a1a1a"}`,
+ transition: "all 0.15s ease",
+ display: "flex", flexDirection: "column", gap: "14px",
+ boxSizing: "border-box",
+ }}
+ >
+
+
{name}
+ β
+
+
{description}
+
+ {tags.map(tag => (
+ {tag}
+ ))}
+
+
+ );
+}
+
+// βββ AUTH ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function Login({ onLogin, goToSignup, goToForgot }) {
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+
+ const login = async () => {
+ try {
+ const data = await loginRequest(email, password);
+ if (!data.access_token) { alert("Invalid credentials"); return; }
+ localStorage.setItem("token", data.access_token);
+ onLogin();
+ } catch { alert("Wrong email or password"); }
+ };
+
+ return (
+
+ PilotMaster
+ observable AI execution ecosystem
+ setEmail(e.target.value)} style={inputStyle} />
+ setPassword(e.target.value)}
+ onKeyDown={e => e.key === "Enter" && login()} style={inputStyle} />
+
+ Don't have an account? Sign up
+ Forgot password?
+
+ );
+}
+
+function Signup({ goToLogin }) {
+ const [username, setUsername] = useState("");
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+
+ const signup = async () => {
+ try {
+ await apiRequest("/auth/signup", "POST", { username, email, password });
+ alert("Account created. Please login.");
+ goToLogin();
+ } catch { alert("Signup failed"); }
+ };
+
+ return (
+
+ PilotMaster
+ create your account
+ setUsername(e.target.value)} style={inputStyle} />
+ setEmail(e.target.value)} style={inputStyle} />
+ setPassword(e.target.value)} style={inputStyle} />
+
+ Already have an account? Login
+
+ );
+}
+
+function ForgotPassword({ goBack }) {
+ const [email, setEmail] = useState("");
+ const [token, setToken] = useState("");
+ const [newPassword, setNewPassword] = useState("");
+ const [generatedToken, setGeneratedToken] = useState("");
+
+ return (
+
+ Reset Password
+ setEmail(e.target.value)} style={inputStyle} />
+
+ {generatedToken && (
+
+ Token: {generatedToken}
+
+ )}
+ setToken(e.target.value)} style={inputStyle} />
+ setNewPassword(e.target.value)} style={inputStyle} />
+
+ Back to Login
+
+ );
+}
+
+function AuthShell({ children }) {
+ return (
+
+ );
+}
+
+function Splash() {
+ return (
+
+ Loading...
+
+ );
+}
+
+// βββ STYLES ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+const inputStyle = {
+ width: "100%", padding: "20px 22px", marginBottom: "16px", borderRadius: "14px",
+ border: "1px solid #1e1e1e", background: "#141414", color: "white",
+ fontSize: "17px", outline: "none", boxSizing: "border-box",
+};
+
+const primaryBtnStyle = {
+ width: "100%", padding: "20px", borderRadius: "14px", border: "1px solid #2a2a2a",
+ background: "#1a1a1a", color: "white", fontSize: "17px", cursor: "pointer",
+ fontWeight: "600", marginBottom: "8px", boxSizing: "border-box",
+};
+
+const authTitleStyle = {
+ margin: "0 0 8px", fontSize: "64px", fontFamily: "Georgia, serif",
+ fontWeight: "600", letterSpacing: "-3px", color: "white", textAlign: "center", lineHeight: 1,
+};
+
+const linkStyle = {
+ margin: "14px 0 0", color: "#555", textAlign: "center", cursor: "pointer", fontSize: "15px",
+};
+
+const btnStyle = {
+ padding: "10px 20px", background: "#141414", color: "#888",
+ border: "1px solid #222", borderRadius: "10px", cursor: "pointer", fontSize: "13px",
+};
diff --git a/frontend/src/docpilot/App.jsx b/frontend/src/docpilot/App.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..7dd1682c46cfee768cbfab02097d6fec30576279
--- /dev/null
+++ b/frontend/src/docpilot/App.jsx
@@ -0,0 +1,3 @@
+// Auth is handled at PilotMaster root (App.jsx)
+// This file kept for compatibility but is no longer the entry point
+export { default } from "./pages/Dashboard.jsx";
diff --git a/frontend/src/docpilot/api.js b/frontend/src/docpilot/api.js
new file mode 100644
index 0000000000000000000000000000000000000000..e6c5f23760a4ad79ce8787bf7c0980b80420a842
--- /dev/null
+++ b/frontend/src/docpilot/api.js
@@ -0,0 +1,35 @@
+const API_BASE = "http://127.0.0.1:8000/docpilot";
+
+export const apiRequest = async (endpoint, method = "GET", body = null) => {
+ const token = localStorage.getItem("token");
+ const headers = {};
+
+ if (!(body instanceof FormData)) {
+ headers["Content-Type"] = "application/json";
+ }
+ if (token) {
+ headers["Authorization"] = `Bearer ${token}`;
+ }
+
+ const response = await fetch(API_BASE + endpoint, {
+ method,
+ headers,
+ body: body instanceof FormData ? body : body ? JSON.stringify(body) : null,
+ });
+
+ return response.json();
+};
+
+export const loginRequest = async (email, password) => {
+ const formData = new URLSearchParams();
+ formData.append("username", email);
+ formData.append("password", password);
+
+ const response = await fetch(API_BASE + "/auth/login", {
+ method: "POST",
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
+ body: formData,
+ });
+
+ return response.json();
+};
diff --git a/frontend/src/docpilot/pages/Dashboard.jsx b/frontend/src/docpilot/pages/Dashboard.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..41d1959074b1be93b34b6ed956d66f5a0fae18f2
--- /dev/null
+++ b/frontend/src/docpilot/pages/Dashboard.jsx
@@ -0,0 +1,260 @@
+import { useState, useEffect, useRef } from "react";
+import { apiRequest } from "../api";
+import { useDropzone } from "react-dropzone";
+
+function Dashboard({ onLogout, onHome, onTracePilot }) {
+ const [file, setFile] = useState(null);
+ const [question, setQuestion] = useState("");
+ const [source, setSource] = useState("");
+ const [messages, setMessages] = useState([]);
+ const [sessions, setSessions] = useState([]);
+ const [currentSessionId, setCurrentSessionId] = useState(null);
+ const [username, setUsername] = useState("");
+ const [uploading, setUploading] = useState(false);
+ const [asking, setAsking] = useState(false);
+ const messagesEndRef = useRef(null);
+
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+ }, [messages]);
+
+ useEffect(() => {
+ apiRequest("/history/sessions").then(setSessions).catch(() => {});
+ apiRequest("/billing/me").then(d => setUsername(d.username)).catch(() => {});
+ }, []);
+
+ const fetchSessions = () =>
+ apiRequest("/history/sessions").then(setSessions).catch(() => {});
+
+ const loadSession = async (sessionId) => {
+ const data = await apiRequest(`/history/${sessionId}`);
+ // Align keys directly with your backend metrics payload structure
+ setMessages(data.map(m => ({
+ role: m.role,
+ content: m.content,
+ sources: m.sources,
+ timestamp: m.timestamp || m.created_at || new Date().toISOString()
+ })));
+ setCurrentSessionId(sessionId);
+ };
+
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
+ onDrop: (files) => files.length > 0 && setFile(files[0]),
+ accept: {
+ "application/pdf": [".pdf"],
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"],
+ "text/plain": [".txt"],
+ "text/markdown": [".md"],
+ "text/csv": [".csv"],
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"],
+ "image/png": [".png"],
+ "image/jpeg": [".jpg", ".jpeg"],
+ },
+ });
+
+ const uploadFile = async () => {
+ if (!file) return;
+ setUploading(true);
+ const formData = new FormData();
+ formData.append("file", file);
+ try {
+ const data = await apiRequest("/docs/upload", "POST", formData);
+ if (data.detail) { alert(data.detail); }
+ else { alert(data.message); setSource(file.name); }
+ } catch { alert("Upload failed"); }
+ setUploading(false);
+ };
+
+ const askQuestion = async () => {
+ if (!question) return;
+ const q = question;
+ setQuestion("");
+ setMessages(prev => [
+ ...prev,
+ { role: "user", content: q, timestamp: new Date().toISOString() },
+ { role: "assistant", content: "Thinking...", loading: true, timestamp: new Date().toISOString() }
+ ]);
+ setAsking(true);
+ try {
+ const data = await apiRequest("/chat/ask", "POST", { question: q, source, session_id: currentSessionId });
+ if (data.session_id) { setCurrentSessionId(data.session_id); fetchSessions(); }
+ setMessages(prev => {
+ const u = [...prev];
+ u[u.length - 1] = {
+ role: "assistant",
+ content: data.answer,
+ sources: data.sources,
+ timestamp: new Date().toISOString()
+ };
+ return u;
+ });
+ } catch {
+ setMessages(prev => {
+ const u = [...prev];
+ u[u.length - 1] = {
+ role: "assistant",
+ content: "Something went wrong.",
+ timestamp: new Date().toISOString()
+ };
+ return u;
+ });
+ }
+ setAsking(false);
+ };
+
+ const resetMemory = async () => {
+ await apiRequest("/docs/reset", "DELETE");
+ setMessages([]); setQuestion(""); setSource(""); setFile(null); setCurrentSessionId(null);
+ };
+
+ return (
+
+
+ {/* SIDEBAR */}
+
+
+ {/* SIDEBAR HEADER */}
+
+
DocPilot
+
{username}
+
+
+ {/* UPLOAD */}
+
+
+
+ {isDragActive ?
Drop here...
:
Drag & drop or click to upload
}
+ {file &&
{file.name}
}
+
+
+
+
+ {/* NEW CHAT */}
+
+
+
+
+ {/* SESSIONS */}
+
+
Conversations
+ {sessions.map(session => (
+
loadSession(session.id)} style={{
+ padding: "12px 14px", marginBottom: "6px", borderRadius: "10px",
+ background: currentSessionId === session.id ? "#1e1e1e" : "#141414",
+ border: "1px solid #1e1e1e", cursor: "pointer", fontSize: "13px",
+ display: "flex", justifyContent: "space-between", alignItems: "center",
+ }}>
+
+ {session.title || `Chat #${session.id}`}
+
+
+
+ ))}
+
+
+
+ {/* MAIN */}
+
+
+ {/* THIN HEADER */}
+
+
+ Active Document: {source || "None"}
+
+
+ {[
+ { label: "β Home", onClick: onHome, color: "#aaa" },
+ { label: "TracePilot β", onClick: onTracePilot, color: "#7c4dff" },
+ { label: "Reset", onClick: resetMemory, color: "#aaa" },
+ { label: "Logout", onClick: onLogout, color: "#aaa" },
+ ].map(btn => (
+
+ ))}
+
+
{/* CHAT AREA */}
+
+ {messages.length === 0 && (
+
Ask questions about your document...
+ )}
+ {messages
+ .sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp))
+ .map((msg, i) => (
+
+ {msg.role === "user" ? (
+
+ {msg.content}
+
+ ) : (
+
+
+ {/* REMOVED CONTAINER BUBBLE STYLE HERE β JUST RAW TEXT CONTENT */}
+
+ {msg.content}
+
+
+ {/* Sources section remains nicely padded right beneath the unbubbled text */}
+ {msg.sources && msg.sources.length > 0 && (
+
+
Sources
+ {msg.sources.map((s, idx) => (
+
+ π {s.source || s.file_name} Β· Page {s.page || s.page_number}
+
+ ))}
+
+ )}
+
+ )}
+
+ ))}
+
+
+
+ {/* INPUT */}
+
+
+ setQuestion(e.target.value)}
+ onKeyDown={e => e.key === "Enter" && askQuestion()}
+ style={{
+ flex: 1, padding: "16px 20px", borderRadius: "14px",
+ border: "1px solid #222", background: "#161616", color: "white",
+ fontSize: "15px", outline: "none",
+ }}
+ />
+
+
+
+
+
+ );
+}
+
+export default Dashboard;
\ No newline at end of file
diff --git a/frontend/src/docpilot/pages/ForgotPassword.jsx b/frontend/src/docpilot/pages/ForgotPassword.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..f6cd9fdaf9023726c65ab118a5db9ce9ec475f64
--- /dev/null
+++ b/frontend/src/docpilot/pages/ForgotPassword.jsx
@@ -0,0 +1,275 @@
+import { useState } from "react";
+
+import { apiRequest } from "../api";
+
+function ForgotPassword({
+ goBack,
+}) {
+ const [email,
+ setEmail] =
+ useState("");
+
+ const [token,
+ setToken] =
+ useState("");
+
+ const [newPassword,
+ setNewPassword] =
+ useState("");
+
+ const [generatedToken,
+ setGeneratedToken] =
+ useState("");
+
+ const requestReset =
+ async () => {
+
+ try {
+
+ const data =
+ await apiRequest(
+ "/auth/forgot-password",
+ "POST",
+ {
+ email,
+ }
+ );
+
+ setGeneratedToken(
+ data.reset_token
+ );
+
+ } catch (error) {
+
+ console.error(error);
+
+ alert(
+ "Email not found"
+ );
+ }
+ };
+
+ const resetPassword =
+ async () => {
+
+ try {
+
+ await apiRequest(
+ "/auth/reset-password",
+ "POST",
+ {
+ token,
+ new_password:
+ newPassword,
+ }
+ );
+
+ alert(
+ "Password reset successful"
+ );
+
+ goBack();
+
+ } catch (error) {
+
+ console.error(error);
+
+ alert(
+ "Invalid token"
+ );
+ }
+ };
+
+ return (
+
+
+
+ Reset
+
+
+
+ setEmail(
+ e.target.value
+ )
+ }
+
+ style={inputStyle}
+ />
+
+
+
+ {generatedToken && (
+
+ Reset Token:
+
+ {
+ generatedToken
+ }
+
+ )}
+
+
+ setToken(
+ e.target.value
+ )
+ }
+
+ style={{
+ ...inputStyle,
+ marginTop:
+ "20px",
+ }}
+ />
+
+
+ setNewPassword(
+ e.target.value
+ )
+ }
+
+ style={inputStyle}
+ />
+
+
+
+
+ Back to Login
+
+
+
+ );
+}
+
+const inputStyle = {
+ width: "100%",
+
+ padding: "18px",
+
+ marginBottom: "16px",
+
+ borderRadius: "14px",
+
+ border: "1px solid #333",
+
+ background: "#1d1d1d",
+
+ color: "white",
+
+ fontSize: "16px",
+
+ outline: "none",
+};
+
+const buttonStyle = {
+ width: "100%",
+
+ padding: "18px",
+
+ borderRadius: "14px",
+
+ border: "1px solid #333",
+
+ background: "#2a2a2a",
+
+ color: "white",
+
+ fontSize: "16px",
+
+ cursor: "pointer",
+};
+
+export default ForgotPassword;
\ No newline at end of file
diff --git a/frontend/src/docpilot/pages/Login.jsx b/frontend/src/docpilot/pages/Login.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..03888228baf23cff3ef7a84fb1e34e32c54f1921
--- /dev/null
+++ b/frontend/src/docpilot/pages/Login.jsx
@@ -0,0 +1,303 @@
+import { useState } from "react";
+
+import { loginRequest } from "../api";
+
+function Login({
+ onLogin,
+ goToSignup,
+ goToForgot,
+}) {
+
+ const [email,
+ setEmail] =
+ useState("");
+
+ const [password,
+ setPassword] =
+ useState("");
+
+ const login =
+ async () => {
+
+ try {
+
+ const data =
+ await loginRequest(
+ email,
+ password
+ );
+
+ if (
+ !data.access_token
+ ) {
+
+ alert(
+ "Invalid credentials"
+ );
+
+ return;
+ }
+
+ localStorage.setItem(
+ "token",
+ data.access_token
+ );
+
+ onLogin();
+
+ } catch (error) {
+
+ console.error(error);
+
+ alert(
+ "Wrong email or password"
+ );
+ }
+ };
+
+ return (
+
+
+
+
+ DocPilot
+
+
+
+ setEmail(
+ e.target.value
+ )
+ }
+
+ style={inputStyle}
+ />
+
+
+ setPassword(
+ e.target.value
+ )
+ }
+
+ style={inputStyle}
+ />
+
+
+
+
+ Don't have an account?
+ {" "}
+ Sign up
+
+
+
+ Forgot password?
+
+
+
+
+ );
+}
+
+const inputStyle = {
+ width: "520px",
+
+ maxWidth:
+ "90vw",
+
+ padding:
+ "22px",
+
+ marginBottom:
+ "18px",
+
+ borderRadius:
+ "18px",
+
+ border:
+ "1px solid #2e2e2e",
+
+ background:
+ "#1d1d1d",
+
+ color:
+ "white",
+
+ fontSize:
+ "18px",
+
+ outline:
+ "none",
+
+ boxSizing:
+ "border-box",
+};
+
+const buttonStyle = {
+ width: "520px",
+
+ maxWidth:
+ "90vw",
+
+ padding:
+ "22px",
+
+ borderRadius:
+ "18px",
+
+ border:
+ "1px solid #333",
+
+ background:
+ "#2a2a2a",
+
+ color:
+ "white",
+
+ fontSize:
+ "18px",
+
+ cursor:
+ "pointer",
+
+ fontWeight:
+ "700",
+
+ boxSizing:
+ "border-box",
+};
+
+export default Login;
\ No newline at end of file
diff --git a/frontend/src/docpilot/pages/Signup.jsx b/frontend/src/docpilot/pages/Signup.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..4950aeab81103dbf5b5de7a75aa354009d6aabb5
--- /dev/null
+++ b/frontend/src/docpilot/pages/Signup.jsx
@@ -0,0 +1,269 @@
+import { useState } from "react";
+
+import { apiRequest } from "../api";
+
+function Signup({
+ goToLogin,
+}) {
+ const [username,
+ setUsername] =
+ useState("");
+
+ const [email, setEmail] =
+ useState("");
+
+ const [password,
+ setPassword] =
+ useState("");
+
+ const signup =
+ async () => {
+ try {
+ await apiRequest(
+ "/auth/signup",
+ "POST",
+ {
+ username,
+ email,
+ password,
+ }
+ );
+
+ alert(
+ "Signup successful"
+ );
+
+ goToLogin();
+
+ } catch (error) {
+ console.error(error);
+
+ alert(
+ "Signup failed"
+ );
+ }
+ };
+
+ return (
+
+
+
+ DocPilot
+
+
+
+ setUsername(
+ e.target.value
+ )
+ }
+
+ style={{
+ width: "100%",
+
+ padding:
+ "18px",
+
+ marginBottom:
+ "16px",
+
+ borderRadius:
+ "14px",
+
+ border:
+ "1px solid #333",
+
+ background:
+ "#1d1d1d",
+
+ color:
+ "white",
+
+ fontSize:
+ "16px",
+
+ outline:
+ "none",
+ }}
+ />
+
+
+ setEmail(
+ e.target.value
+ )
+ }
+
+ style={{
+ width: "100%",
+
+ padding:
+ "18px",
+
+ marginBottom:
+ "16px",
+
+ borderRadius:
+ "14px",
+
+ border:
+ "1px solid #333",
+
+ background:
+ "#1d1d1d",
+
+ color:
+ "white",
+
+ fontSize:
+ "16px",
+
+ outline:
+ "none",
+ }}
+ />
+
+
+ setPassword(
+ e.target.value
+ )
+ }
+
+ style={{
+ width: "100%",
+
+ padding:
+ "18px",
+
+ marginBottom:
+ "20px",
+
+ borderRadius:
+ "14px",
+
+ border:
+ "1px solid #333",
+
+ background:
+ "#1d1d1d",
+
+ color:
+ "white",
+
+ fontSize:
+ "16px",
+
+ outline:
+ "none",
+ }}
+ />
+
+
+
+
+ Already have an account?
+ Login
+
+
+
+ );
+}
+
+export default Signup;
\ No newline at end of file
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..61bde43c5f5a3193483c4eb815f082d344b69e69
--- /dev/null
+++ b/frontend/src/main.jsx
@@ -0,0 +1,12 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App.jsx";
+
+const style = document.createElement("style");
+style.textContent = `
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+ html, body, #root { width: 100%; height: 100%; overflow: hidden; background: #0d0d0d; }
+`;
+document.head.appendChild(style);
+
+ReactDOM.createRoot(document.getElementById("root")).render();
diff --git a/frontend/src/tracepilot/TraceExplorer.jsx b/frontend/src/tracepilot/TraceExplorer.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..f27c633b44c41538a4dd6ed92fc346430fcaecf3
--- /dev/null
+++ b/frontend/src/tracepilot/TraceExplorer.jsx
@@ -0,0 +1,273 @@
+import { useEffect, useState } from "react";
+import axios from "axios";
+
+const api = axios.create({ baseURL: "http://127.0.0.1:8000/tracepilot" });
+
+const relevanceColor = { high: "#4caf50", moderate: "#ff9800", low: "#f44336", none: "#555" };
+const confColor = { high: "#4caf50", medium: "#ff9800", low: "#f44336", none: "#555" };
+const riskColor = { low: "#4caf50", medium: "#ff9800", high: "#f44336" };
+const ansColor = { high: "#4caf50", partial: "#ff9800", none: "#f44336", unknown: "#555" };
+
+export default function TraceExplorer({ onHome, onDocPilot }) {
+ const [traces, setTraces] = useState([]);
+ const [selectedTrace, setSelectedTrace] = useState(null);
+ const [selectedId, setSelectedId] = useState(null);
+ const [replaying, setReplaying] = useState(false);
+
+ useEffect(() => {
+ fetchTraces();
+ const interval = setInterval(fetchTraces, 3000);
+ return () => clearInterval(interval);
+ }, []);
+
+ function fetchTraces() {
+ api.get("/traces").then(r => setTraces(r.data)).catch(() => {});
+ }
+
+ async function loadTrace(traceId) {
+ setSelectedId(traceId);
+ const r = await api.get(`/traces/${traceId}`);
+ setSelectedTrace(r.data);
+ }
+
+ async function replayTrace(traceId) {
+ setReplaying(true);
+ await api.post(`/traces/${traceId}/replay`);
+ fetchTraces();
+ setReplaying(false);
+ }
+
+ const avgLatency = traces.length
+ ? Math.round(traces.reduce((s, t) => s + (t.latency || 0), 0) / traces.length)
+ : 0;
+
+ const groundedCount = traces.filter(t => t.grounded).length;
+
+ return (
+
+
+ {/* HEADER */}
+
+
+ TracePilot
+
+
+ execution intelligence layer
+
+ {onHome && (
+
+
+ {onDocPilot && (
+
+ )}
+
+ )}
+
+ {/* STAT PILLS */}
+
+
+
+
+ 0 ? "#f44336" : "#555"}
+ />
+
+
+
+ {/* BODY */}
+
+
+ {/* SIDEBAR */}
+
+
+ {traces.length} trace{traces.length !== 1 ? "s" : ""}
+
+ {traces.length === 0 && (
+
No traces yet. Ask a question in DocPilot.
+ )}
+ {traces.map(trace => {
+ const rel = trace.evaluation?.retrieval_relevance || trace.retrieval_quality;
+ return (
+
loadTrace(trace.trace_id)} style={{
+ padding: "0.75rem 0.9rem",
+ marginBottom: "0.4rem",
+ border: `1px solid ${selectedId === trace.trace_id ? "#444" : "#1e1e1e"}`,
+ background: selectedId === trace.trace_id ? "#161616" : "#111",
+ cursor: "pointer",
+ borderRadius: "6px",
+ transition: "border-color 0.15s",
+ }}>
+
+ {trace.query}
+
+
+ {rel}
+ {trace.evaluation?.answerability === "none" && unanswerable}
+ {trace.evaluation?.abstained && abstained}
+ {trace.parent_trace_id && replay}
+
+ {trace.latency?.toFixed(0)} ms
+
+
+
+ );
+ })}
+
+
+ {/* DETAIL PANEL */}
+
+ {!selectedTrace ? (
+
+ β select a trace to inspect
+
+ ) : (
+
+ {/* TRACE HEADER */}
+
+
+
query
+
+ {selectedTrace.query}
+
+
+
+
+
+ {/* EVALUATION TAGS */}
+
+ {(() => {
+ const ev = selectedTrace.evaluation || {};
+ return (<>
+ retrieval: {ev.retrieval_relevance || "β"}
+ grounding: {ev.grounding_confidence || "β"}
+ answerability: {ev.answerability || "β"}
+ hallucination risk: {ev.hallucination_risk || "β"}
+ {ev.abstained && abstained β}
+
+ {selectedTrace.model_name} Β· {selectedTrace.latency?.toFixed(0)} ms
+ {selectedTrace.parent_trace_id && ` Β· replay of ${selectedTrace.parent_trace_id.slice(0, 8)}β¦`}
+
+ >);
+ })()}
+
+
+ {/* RESPONSE */}
+
+
+
{selectedTrace.response}
+
+
+
+ {/* CHUNKS */}
+
+ {selectedTrace.retrieved_chunks?.length === 0 && (
+ No chunks retrieved.
+ )}
+ {selectedTrace.retrieved_chunks?.map((chunk, i) => (
+
+
+ rank {chunk.rank}
+
+ score {chunk.score?.toFixed(3)}
+
+
+
{chunk.text}
+
+ ))}
+
+
+ {/* METRICS */}
+
+
+ {/* SPANS */}
+ {selectedTrace.spans?.length > 0 && (
+
+
+ {selectedTrace.spans.map((span, i) => (
+
+ {span.name || span.span_type}
+ {span.duration_ms && {span.duration_ms} ms}
+
+ ))}
+
+
+ )}
+
+ )}
+
+
+
+ );
+}
+
+function Tag({ color, children }) {
+ return (
+ {children}
+ );
+}
+
+function Section({ title, children }) {
+ return (
+
+ );
+}
+
+function Stat({ label, value, color }) {
+ return (
+
+ );
+}
+
+function MetricBox({ label, value }) {
+ return (
+
+ );
+}
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..f3b304a908fc43a10d1f59796b73a75fa64febd4
--- /dev/null
+++ b/frontend/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+ plugins: [react()],
+ server: { port: 5173 }
+});
diff --git a/main.py b/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..6dfb7a970da7fc9c266fbdbba2a4bbcbb57a9a17
--- /dev/null
+++ b/main.py
@@ -0,0 +1,60 @@
+import sys
+import os
+import importlib.util
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+
+def load_app(app_path: str, app_module: str, attr: str):
+ """Load a FastAPI app from a path without polluting sys.path."""
+ # Temporarily set sys.path to the specific backend dir
+ original_path = sys.path.copy()
+ sys.path.insert(0, app_path)
+
+ # Remove any cached 'app' module to avoid collisions
+ to_remove = [k for k in sys.modules if k == "app" or k.startswith("app.")]
+ for k in to_remove:
+ del sys.modules[k]
+
+ spec = importlib.util.spec_from_file_location(
+ app_module,
+ os.path.join(app_path, "app", "main.py"),
+ submodule_search_locations=[os.path.join(app_path, "app")]
+ )
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[app_module] = module
+ spec.loader.exec_module(module)
+
+ sys.path = original_path
+ return getattr(module, attr)
+
+
+DOCPILOT_BACKEND = os.path.join(os.path.dirname(__file__), "DocPilot", "backend")
+TRACEPILOT_BACKEND = os.path.join(os.path.dirname(__file__), "TracePilot", "backend")
+
+docpilot_app = load_app(DOCPILOT_BACKEND, "docpilot_app", "app")
+tracepilot_app = load_app(TRACEPILOT_BACKEND, "tracepilot_app", "app")
+
+app = FastAPI(title="PilotMaster")
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+app.mount("/docpilot", docpilot_app)
+app.mount("/tracepilot", tracepilot_app)
+
+
+@app.get("/")
+def root():
+ return {
+ "platform": "PilotMaster",
+ "services": {
+ "docpilot": "/docpilot",
+ "tracepilot": "/tracepilot",
+ }
+ }
diff --git a/pilotcore/__init__.py b/pilotcore/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/pilotcore/config.py b/pilotcore/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..3f1a02e5e3119e5f64295dc28ca50e2ddff6cfec
--- /dev/null
+++ b/pilotcore/config.py
@@ -0,0 +1,25 @@
+import os
+from dotenv import load_dotenv
+from pathlib import Path
+
+# Load from PilotMaster root .env
+load_dotenv(dotenv_path=Path(__file__).resolve().parent.parent / ".env")
+
+GROQ_API_KEY = os.getenv("GROQ_API_KEY")
+GROQ_MODEL = os.getenv("GROQ_MODEL", "llama-3.1-8b-instant")
+
+DATABASE_URL = os.getenv("DATABASE_URL")
+
+SECRET_KEY = os.getenv("SECRET_KEY")
+ALGORITHM = os.getenv("ALGORITHM", "HS256")
+ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "60"))
+
+TRACEPILOT_URL = os.getenv("TRACEPILOT_URL", "http://localhost:8000")
+DOCPILOT_URL = os.getenv("DOCPILOT_URL", "http://localhost:8000")
+
+VECTOR_STORE_DIR = os.getenv("VECTOR_STORE_DIR", "vector_store")
+
+# Execution version identity β bump these when the respective component changes
+EVALUATOR_VERSION = "1.0"
+PROMPT_VERSION = "1.0"
+RETRIEVER_VERSION = "vector_v1"
diff --git a/pilotcore/evaluation/__init__.py b/pilotcore/evaluation/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/pilotcore/evaluation/evaluator.py b/pilotcore/evaluation/evaluator.py
new file mode 100644
index 0000000000000000000000000000000000000000..c60da9dc957c51a304b5aa0b9eba681924e36bb7
--- /dev/null
+++ b/pilotcore/evaluation/evaluator.py
@@ -0,0 +1,236 @@
+STOPWORDS = {
+ "the", "is", "a", "an", "to", "of", "and", "in", "on", "for",
+ "what", "how", "who", "why", "when", "where", "which", "was",
+ "were", "are", "be", "been", "being", "i", "it", "this", "that",
+ "do", "does", "did", "about", "tell", "me", "explain", "describe",
+ "give", "get", "has", "have", "had", "will", "would", "could",
+ "should", "can", "may", "might", "its", "their", "there",
+}
+
+ABSTENTION_PHRASES = [
+ "i don't have enough information",
+ "i don't have any specific information",
+ "the context does not provide",
+ "cannot answer from the context",
+ "not enough information",
+ "not mentioned in",
+ "no information",
+ "outside the context",
+ "i cannot find",
+ "not available in",
+ "i'm unable to find",
+ "the document does not",
+]
+
+# Query types that signal intent beyond simple keyword lookup
+BROAD_QUERY_VERBS = {"explain", "describe", "tell", "elaborate", "discuss", "summarize", "overview"}
+FACTUAL_QUERY_WORDS = {"what", "which", "who", "when", "where", "how many", "how much"}
+
+
+def _chunk_texts(chunks: list) -> list[str]:
+ return [c["text"] if isinstance(c, dict) else c.chunk.text for c in chunks]
+
+
+def _meaningful_words(text: str) -> set:
+ return {w for w in text.lower().split() if w not in STOPWORDS and len(w) > 2}
+
+
+def _classify_query(query: str) -> str:
+ """
+ Classify query intent to inform answerability evaluation.
+ Returns: direct_fact | broad_query | abstention_likely | keyword_trap
+ """
+ q = query.lower().strip()
+ words = set(q.split())
+
+ if any(v in words for v in BROAD_QUERY_VERBS):
+ return "broad_query"
+
+ # Keyword trap: query is about a person/entity that only appears incidentally
+ if q.startswith("who is") or q.startswith("what is") and len(words) <= 5:
+ return "direct_fact"
+
+ return "direct_fact"
+
+
+def evaluate_retrieval_relevance(query: str, chunks: list, scores: list[float]) -> dict:
+ """
+ Did retrieval fetch semantically relevant context?
+ Uses L2 distance β lower is better.
+ Filters out irrelevant chunks (score > 1.4) before evaluation.
+ """
+ if not chunks or not scores:
+ return {
+ "retrieval_relevance": "none",
+ "retrieval_score_avg": 0.0,
+ "top_retrieval_score": 0.0,
+ }
+
+ # Only consider relevant chunks for scoring
+ relevant_scores = [s for s in scores if s < 1.4]
+
+ if not relevant_scores:
+ return {
+ "retrieval_relevance": "low",
+ "retrieval_score_avg": round(sum(scores) / len(scores), 4),
+ "top_retrieval_score": round(min(scores), 4),
+ }
+
+ top_score = min(relevant_scores)
+ avg_score = round(sum(scores) / len(scores), 4)
+
+ if top_score < 0.8:
+ relevance = "high"
+ elif top_score < 1.2:
+ relevance = "moderate"
+ else:
+ relevance = "low"
+
+ return {
+ "retrieval_relevance": relevance,
+ "retrieval_score_avg": avg_score,
+ "top_retrieval_score": round(top_score, 4),
+ }
+
+
+def evaluate_grounding(response: str, chunks: list) -> dict:
+ """
+ Did the model answer USING retrieved evidence?
+ Abstention is rewarded as correct grounded behavior.
+ """
+ abstained = any(phrase in response.lower() for phrase in ABSTENTION_PHRASES)
+
+ if abstained:
+ return {
+ "grounded": True,
+ "grounding_confidence": "high",
+ "hallucination_risk": "low",
+ "faithfulness_score": 1.0,
+ "abstained": True,
+ }
+
+ response_words = _meaningful_words(response)
+ all_chunk_words = set()
+ for text in _chunk_texts(chunks):
+ all_chunk_words.update(_meaningful_words(text))
+
+ if not response_words:
+ return {
+ "grounded": False,
+ "grounding_confidence": "none",
+ "hallucination_risk": "high",
+ "faithfulness_score": 0.0,
+ "abstained": False,
+ }
+
+ overlap = response_words.intersection(all_chunk_words)
+ faithfulness = round(len(overlap) / len(response_words), 2)
+
+ # Penalize long responses more β longer = more likely to expand beyond evidence
+ length_penalty = min(1.0, len(response_words) / 150)
+ adjusted_hallucination = round((1.0 - faithfulness) * (0.7 + 0.3 * length_penalty), 2)
+
+ grounded = len(overlap) >= 3
+ grounding_confidence = (
+ "high" if faithfulness > 0.5 else
+ "medium" if faithfulness > 0.25 else
+ "low"
+ )
+ hallucination_risk = (
+ "low" if adjusted_hallucination < 0.35 else
+ "medium" if adjusted_hallucination < 0.6 else
+ "high"
+ )
+
+ return {
+ "grounded": grounded,
+ "grounding_confidence": grounding_confidence,
+ "hallucination_risk": hallucination_risk,
+ "faithfulness_score": faithfulness,
+ "abstained": False,
+ }
+
+
+def evaluate_answerability(query: str, chunks: list, scores: list[float] = None) -> dict:
+ """
+ Did the retrieved context actually contain enough information to answer?
+ Intent-aware: broad queries are harder to fully answer than direct facts.
+ Keyword traps are detected when entity appears incidentally.
+ """
+ if not chunks:
+ return {
+ "answerability": "none",
+ "context_sufficiency": "insufficient",
+ "query_coverage": 0.0,
+ "query_type": "unknown",
+ }
+
+ query_type = _classify_query(query)
+ query_words = _meaningful_words(query)
+
+ # Filter to only relevant chunks (ignore noise chunks with high L2 distance)
+ if scores:
+ relevant_chunks = [c for c, s in zip(chunks, scores) if s < 1.4]
+ else:
+ relevant_chunks = chunks
+
+ if not relevant_chunks:
+ return {
+ "answerability": "none",
+ "context_sufficiency": "insufficient",
+ "query_coverage": 0.0,
+ "query_type": query_type,
+ }
+
+ all_chunk_words = set()
+ for text in _chunk_texts(relevant_chunks):
+ all_chunk_words.update(_meaningful_words(text))
+
+ if not query_words:
+ return {
+ "answerability": "partial",
+ "context_sufficiency": "partial",
+ "query_coverage": 0.5,
+ "query_type": query_type,
+ }
+
+ overlap = query_words.intersection(all_chunk_words)
+ coverage = round(len(overlap) / len(query_words), 2)
+
+ # Broad queries are inherently partial β document can never fully cover "explain"
+ if query_type == "broad_query":
+ if coverage >= 0.4:
+ answerability, sufficiency = "partial", "partial"
+ else:
+ answerability, sufficiency = "none", "insufficient"
+
+ # Direct fact queries
+ else:
+ if coverage >= 0.5:
+ answerability, sufficiency = "high", "sufficient"
+ elif coverage >= 0.2:
+ answerability, sufficiency = "partial", "partial"
+ else:
+ answerability, sufficiency = "none", "insufficient"
+
+ return {
+ "answerability": answerability,
+ "context_sufficiency": sufficiency,
+ "query_coverage": coverage,
+ "query_type": query_type,
+ }
+
+
+def run_evaluation(query: str, response: str, chunks: list, scores: list[float]) -> dict:
+ """
+ Full multi-dimensional evaluation contract.
+ """
+ retrieval = evaluate_retrieval_relevance(query, chunks, scores)
+ grounding = evaluate_grounding(response, chunks)
+ answerability = evaluate_answerability(query, chunks, scores)
+
+ return {
+ **retrieval,
+ **grounding,
+ **answerability,
+ }
diff --git a/pilotcore/generation/__init__.py b/pilotcore/generation/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/pilotcore/generation/generator.py b/pilotcore/generation/generator.py
new file mode 100644
index 0000000000000000000000000000000000000000..46ca089edd29e3803c06be91bb999abd2dd2ff38
--- /dev/null
+++ b/pilotcore/generation/generator.py
@@ -0,0 +1,38 @@
+from groq import Groq
+from pilotcore.config import GROQ_API_KEY, GROQ_MODEL
+from pilotcore.generation.prompt_builder import build_prompt
+from pilotcore.tracing.telemetry import emit_event
+
+client = Groq(api_key=GROQ_API_KEY)
+
+
+def generate_response(
+ trace,
+):
+
+ prompt = build_prompt(trace)
+
+ completion = client.chat.completions.create(
+ model=GROQ_MODEL,
+ messages=[
+ {
+ "role": "user",
+ "content": prompt,
+ }
+ ],
+ )
+
+ response = completion.choices[0].message.content
+
+ trace.final_response = response
+
+ emit_event(
+ "generation.completed",
+ {
+ "trace_id": trace.trace_id,
+ "response_length": len(response),
+ "model": GROQ_MODEL,
+ },
+ )
+
+ return response
diff --git a/pilotcore/generation/prompt_builder.py b/pilotcore/generation/prompt_builder.py
new file mode 100644
index 0000000000000000000000000000000000000000..19dd98d4c2c0f05b350da07d1604fc1469ba6534
--- /dev/null
+++ b/pilotcore/generation/prompt_builder.py
@@ -0,0 +1,32 @@
+BROAD_VERBS = {"elaborate", "describe", "summarize", "explain", "overview", "discuss", "tell"}
+
+
+def build_prompt(trace):
+
+ retrieval_result = trace.retrieval_result
+ retrieved_chunks = retrieval_result.retrieved_chunks
+
+ context = "\n\n".join([chunk.chunk.text for chunk in retrieved_chunks])
+
+ query = trace.user_query
+ first_word = query.strip().split()[0].lower() if query.strip() else ""
+ is_broad = first_word in BROAD_VERBS
+
+ if is_broad:
+ prompt = f"""You are a document assistant. Using ONLY the document context below, provide a thorough response to the user's request. Do not use outside knowledge.
+
+Document Context:
+{context}
+
+User Request:
+{query}"""
+ else:
+ prompt = f"""Answer the user's question using the retrieved context below. If the answer is not in the context, say you don't have enough information.
+
+Retrieved Context:
+{context}
+
+User Question:
+{query}"""
+
+ return prompt
diff --git a/pilotcore/retrieval/__init__.py b/pilotcore/retrieval/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/pilotcore/retrieval/embeddings.py b/pilotcore/retrieval/embeddings.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c924466e20f0ee39eefc99f42b9c0076443819c
--- /dev/null
+++ b/pilotcore/retrieval/embeddings.py
@@ -0,0 +1,7 @@
+from sentence_transformers import SentenceTransformer
+
+_model = SentenceTransformer("sentence-transformers/all-mpnet-base-v2")
+
+
+def get_embedding(text: str):
+ return _model.encode(text, normalize_embeddings=True).tolist()
diff --git a/pilotcore/retrieval/retriever.py b/pilotcore/retrieval/retriever.py
new file mode 100644
index 0000000000000000000000000000000000000000..e0d6b31657dad33d116a2035fedf4a06bb1a6f36
--- /dev/null
+++ b/pilotcore/retrieval/retriever.py
@@ -0,0 +1,64 @@
+import time
+
+from pilotcore.retrieval.vector_store import load_user_documents
+from pilotcore.schemas.chunk import Chunk
+from pilotcore.schemas.retrieval import RetrievedChunk, RetrievalResult
+from pilotcore.tracing.telemetry import emit_event
+
+
+def retrieve_chunks(user_id, query, trace_id, source=None, top_k=3, **_):
+ start_time = time.perf_counter()
+ documents = load_user_documents(user_id)
+ query_terms = {
+ term.lower()
+ for term in query.split()
+ if term.strip()
+ }
+
+ matches = []
+ for doc in documents:
+ if source and source != doc.get("source"):
+ continue
+
+ text = doc.get("text", "")
+ text_terms = set(text.lower().split())
+ score = len(query_terms & text_terms)
+ if score <= 0:
+ continue
+
+ matches.append((score, doc))
+
+ matches.sort(key=lambda item: item[0], reverse=True)
+ retrieved_chunks = [
+ RetrievedChunk(
+ chunk=Chunk(
+ chunk_id=str(doc.get("chunk_id", index)),
+ document_id=str(doc.get("document_id", "unknown_document")),
+ user_id=str(user_id),
+ text=doc.get("text", ""),
+ source=doc.get("source"),
+ page_number=doc.get("page"),
+ ),
+ score=float(score),
+ )
+ for index, (score, doc) in enumerate(matches[:top_k])
+ ]
+
+ latency_ms = (time.perf_counter() - start_time) * 1000
+ emit_event(
+ "lexical_retrieval.completed",
+ {
+ "trace_id": trace_id,
+ "latency_ms": latency_ms,
+ "retrieved_chunks": len(retrieved_chunks),
+ "user_id": user_id,
+ },
+ )
+
+ return RetrievalResult(
+ trace_id=trace_id,
+ query=query,
+ retrieved_chunks=retrieved_chunks,
+ latency_ms=latency_ms,
+ retriever_version="lexical_v1",
+ )
diff --git a/pilotcore/retrieval/runtime.py b/pilotcore/retrieval/runtime.py
new file mode 100644
index 0000000000000000000000000000000000000000..4bd8660e07db318ed61b4395c0b01f4ec5d86fbb
--- /dev/null
+++ b/pilotcore/retrieval/runtime.py
@@ -0,0 +1,69 @@
+from pilotcore.retrieval.retriever import (
+ retrieve_chunks,
+)
+
+from pilotcore.retrieval.vector_store import (
+ search_vectors,
+)
+
+from pilotcore.tracing.spans import (
+ start_span,
+ end_span,
+)
+
+
+def retrieve(
+ strategy: str,
+ **kwargs,
+):
+
+ trace = kwargs.get("trace")
+
+ kwargs.pop("trace", None)
+
+ if strategy == "lexical":
+
+ span = start_span(
+ trace_id=trace.trace_id,
+ name="retrieval",
+ )
+
+ trace.spans.append(span)
+
+ result = retrieve_chunks(**kwargs)
+
+ end_span(span)
+
+ return result
+
+ elif strategy == "vector":
+
+ span = start_span(
+ trace_id=trace.trace_id,
+ name="vector_retrieval",
+ )
+
+ trace.spans.append(span)
+
+ from pilotcore.retrieval.embeddings import get_embedding
+
+ query = kwargs.pop("query")
+ user_id = kwargs.pop("user_id", None)
+ source = kwargs.pop("source", None)
+ trace_id = kwargs.pop("trace_id")
+
+ query_embedding = get_embedding(query)
+
+ result = search_vectors(
+ user_id=user_id,
+ query_embedding=query_embedding,
+ source=source,
+ trace_id=trace_id,
+ )
+
+ end_span(span)
+
+ return result
+
+ raise ValueError(
+ f"Unknown retrieval strategy: {strategy}")
diff --git a/pilotcore/retrieval/vector_store.py b/pilotcore/retrieval/vector_store.py
new file mode 100644
index 0000000000000000000000000000000000000000..4323bbabf535425bd7664a6590b21bdc333de01b
--- /dev/null
+++ b/pilotcore/retrieval/vector_store.py
@@ -0,0 +1,230 @@
+import uuid
+import time
+import faiss
+import numpy as np
+import pickle
+import os
+
+from pilotcore.config import VECTOR_STORE_DIR
+from pilotcore.retrieval.embeddings import get_embedding
+from pilotcore.schemas.chunk import Chunk
+from pilotcore.schemas.retrieval import RetrievedChunk, RetrievalResult
+from pilotcore.tracing.telemetry import emit_event
+
+DIMENSION = 768
+
+
+def get_user_vector_dir(user_id: int):
+
+ user_dir = os.path.join(VECTOR_STORE_DIR, f"user_{user_id}")
+
+ os.makedirs(user_dir, exist_ok=True)
+
+ return user_dir
+
+
+def get_index_path(user_id: int):
+
+ return os.path.join(get_user_vector_dir(user_id), "faiss.index")
+
+
+def get_docs_path(user_id: int):
+
+ return os.path.join(get_user_vector_dir(user_id), "documents.pkl")
+
+
+def load_user_index(user_id: int):
+
+ index_path = get_index_path(user_id)
+
+ if os.path.exists(index_path):
+
+ return faiss.read_index(index_path)
+
+ return faiss.IndexFlatL2(DIMENSION)
+
+
+def load_user_documents(user_id: int):
+
+ docs_path = get_docs_path(user_id)
+
+ if os.path.exists(docs_path):
+
+ with open(docs_path, "rb") as f:
+ return pickle.load(f)
+
+ return []
+
+
+def save_index(user_id, index, documents):
+
+ faiss.write_index(index, get_index_path(user_id))
+
+ with open(get_docs_path(user_id), "wb") as f:
+
+ pickle.dump(documents, f)
+
+
+def add_vector(
+ user_id,
+ embedding,
+ text,
+ source,
+ page,
+ chunk_id,
+ document_id,
+):
+
+ index = load_user_index(user_id)
+
+ documents = load_user_documents(user_id)
+
+ vector = np.array([embedding], dtype="float32")
+
+ index.add(vector)
+
+ documents.append(
+ {
+ "document_id": document_id,
+ "text": text,
+ "source": source,
+ "page": page,
+ "chunk_id": chunk_id,
+ }
+ )
+ save_index(user_id, index, documents)
+
+
+def search_vectors(
+ user_id,
+ query_embedding,
+ trace_id: str,
+ source=None,
+ top_k=3,
+):
+
+ start_time = time.perf_counter()
+
+ index = load_user_index(user_id)
+
+ documents = load_user_documents(user_id)
+
+ if index.ntotal == 0:
+
+ return RetrievalResult(
+ trace_id=trace_id,
+ query="embedding_query",
+ retrieved_chunks=[],
+ latency_ms=0,
+ retriever_version="vector_v1",
+ )
+
+ vector = np.array([query_embedding], dtype="float32")
+
+ distances, indices = index.search(vector, min(index.ntotal, 100))
+
+ retrieved_chunks = []
+
+ for distance, idx in zip(distances[0], indices[0]):
+
+ if idx >= len(documents):
+ continue
+
+ doc = documents[idx]
+
+ if source:
+
+ if source != doc["source"]:
+ continue
+
+ retrieved_chunks.append(
+ RetrievedChunk(
+ chunk=Chunk(
+ chunk_id=str(doc.get(
+ "chunk_id",
+ uuid.uuid4(),
+ )),
+ document_id=str(
+ doc.get(
+ "document_id",
+ "unknown_document",
+ )
+ ),
+ user_id=str(user_id),
+ text=doc["text"],
+ source=doc.get("source"),
+ page_number=doc.get("page"),
+ ),
+ score=float(distance),
+ )
+ )
+
+ if len(retrieved_chunks) >= top_k:
+ break
+
+ latency_ms = (
+ time.perf_counter() - start_time
+ ) * 1000
+
+ emit_event(
+ "vector_retrieval.completed",
+ {
+ "trace_id": trace_id,
+ "latency_ms": latency_ms,
+ "retrieved_chunks": len(
+ retrieved_chunks
+ ),
+ "user_id": user_id,
+ },
+ )
+
+ return RetrievalResult(
+ trace_id=trace_id,
+ query="embedding_query",
+ retrieved_chunks=retrieved_chunks,
+ latency_ms=latency_ms,
+ retriever_version="vector_v1",
+ )
+
+
+def reset_vector_store(user_id: int):
+
+ index_path = get_index_path(user_id)
+
+ docs_path = get_docs_path(user_id)
+
+ if os.path.exists(index_path):
+ os.remove(index_path)
+
+ if os.path.exists(docs_path):
+ os.remove(docs_path)
+
+ print(f"Vector store reset for user {user_id}")
+
+
+def rebuild_index_without_document(
+ user_id: int,
+ document_id: int,
+):
+
+ documents = load_user_documents(user_id)
+
+ filtered_documents = [doc for doc in documents if doc["document_id"] != document_id]
+
+ new_index = faiss.IndexFlatL2(DIMENSION)
+
+ for doc in filtered_documents:
+
+ embedding = get_embedding(doc["text"])
+
+ vector = np.array([embedding], dtype="float32")
+
+ new_index.add(vector)
+
+ save_index(
+ user_id,
+ new_index,
+ filtered_documents,
+ )
+
+ print(f"Rebuilt vector index for user {user_id} " f"without document {document_id}")
diff --git a/pilotcore/runtime/__init__.py b/pilotcore/runtime/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/pilotcore/runtime/pipeline.py b/pilotcore/runtime/pipeline.py
new file mode 100644
index 0000000000000000000000000000000000000000..293c198d7fb5eea7e51e1a05578527a3a783c5af
--- /dev/null
+++ b/pilotcore/runtime/pipeline.py
@@ -0,0 +1,114 @@
+import time
+import requests
+from pilotcore.config import TRACEPILOT_URL, GROQ_MODEL, EVALUATOR_VERSION, PROMPT_VERSION, RETRIEVER_VERSION
+from pilotcore.tracing.trace_context import generate_trace_id
+from pilotcore.tracing.trace_manager import create_trace
+from pilotcore.retrieval.runtime import retrieve
+from pilotcore.generation.generator import generate_response
+from pilotcore.generation.prompt_builder import build_prompt
+from pilotcore.evaluation.evaluator import run_evaluation
+
+
+def run_pipeline(
+ query: str,
+ user_id=None,
+ source=None,
+):
+
+ trace_id = generate_trace_id()
+ start_time = time.perf_counter()
+
+ trace = create_trace(
+ trace_id=trace_id,
+ user_query=query,
+ )
+
+ retrieval_result = retrieve(
+ strategy="vector",
+ query=query,
+ user_id=user_id,
+ source=source,
+ trace_id=trace.trace_id,
+ trace=trace,
+ )
+
+ trace.retrieval_result = retrieval_result
+
+ # Filter irrelevant chunks β but for broad document queries,
+ # keep top chunks even if scores are high (no relevant embedding match expected)
+ if trace.retrieval_result:
+ all_chunks = trace.retrieval_result.retrieved_chunks
+ filtered = [c for c in all_chunks if c.score < 1.4]
+ # If filtering removed everything, fall back to top 3 chunks as general context
+ trace.retrieval_result.retrieved_chunks = filtered if filtered else all_chunks[:3]
+
+ response = generate_response(trace)
+ trace.final_response = response
+
+ latency_ms = (time.perf_counter() - start_time) * 1000
+
+ chunks = trace.retrieval_result.retrieved_chunks if trace.retrieval_result else []
+ scores = [c.score for c in chunks]
+
+ evaluation = run_evaluation(
+ query=query,
+ response=response,
+ chunks=chunks,
+ scores=scores,
+ )
+
+ _emit_trace(trace, latency_ms, evaluation, user_id, source)
+
+ return trace
+
+
+def _emit_trace(trace, latency_ms: float, evaluation: dict, user_id=None, source=None):
+ chunks = trace.retrieval_result.retrieved_chunks if trace.retrieval_result else []
+
+ payload = {
+ "trace_id": trace.trace_id,
+ "query": trace.user_query,
+ "response": trace.final_response or "",
+ "prompt": build_prompt(trace),
+ "latency": round(latency_ms, 2),
+ "model_name": GROQ_MODEL,
+ "retrieved_chunks": [
+ {
+ "chunk_id": str(c.chunk.chunk_id),
+ "text": c.chunk.text,
+ "score": c.score,
+ "rank": i,
+ }
+ for i, c in enumerate(chunks)
+ ],
+ "retrieval_score_avg": evaluation.get("retrieval_score_avg", 0.0),
+ "top_retrieval_score": evaluation.get("top_retrieval_score", 0.0),
+ "chunk_count": len(chunks),
+ "response_length": len(trace.final_response or ""),
+ "retrieval_quality": evaluation.get("retrieval_relevance", "none"),
+ "grounded": evaluation.get("grounded", False),
+ "evaluation": evaluation,
+ "evaluator_version": EVALUATOR_VERSION,
+ "prompt_version": PROMPT_VERSION,
+ "retriever_version": RETRIEVER_VERSION,
+ "user_id": str(user_id) if user_id else None,
+ "source": source,
+ "spans": [
+ {
+ "span_id": s.span_id,
+ "name": s.name,
+ "start_time": s.start_time.isoformat(),
+ "end_time": s.end_time.isoformat() if s.end_time else None,
+ }
+ for s in trace.spans
+ ],
+ }
+
+ try:
+ requests.post(
+ f"{TRACEPILOT_URL}/tracepilot/ingest",
+ json=payload,
+ timeout=2,
+ )
+ except Exception:
+ pass
diff --git a/pilotcore/schemas/__init__.py b/pilotcore/schemas/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/pilotcore/schemas/chunk.py b/pilotcore/schemas/chunk.py
new file mode 100644
index 0000000000000000000000000000000000000000..fbee90aca4148856e23f70cf2148f491c860a690
--- /dev/null
+++ b/pilotcore/schemas/chunk.py
@@ -0,0 +1,18 @@
+from pydantic import BaseModel
+from typing import Optional, Dict, Any
+
+
+class Chunk(BaseModel):
+ chunk_id: str
+ document_id: str
+ user_id: str
+
+ text: str
+
+ source: Optional[str] = None
+ page_number: Optional[int] = None
+
+ embedding_model: Optional[str] = None
+ embedding_version: Optional[str] = None
+
+ metadata: Dict[str, Any] = {}
diff --git a/pilotcore/schemas/citation.py b/pilotcore/schemas/citation.py
new file mode 100644
index 0000000000000000000000000000000000000000..402f05be773ac7ee25a2cdf28ce1b1bc33b5f58e
--- /dev/null
+++ b/pilotcore/schemas/citation.py
@@ -0,0 +1,10 @@
+from pydantic import BaseModel
+from typing import Optional
+
+
+class Citation(BaseModel):
+ chunk_id: str
+
+ document_id: str
+
+ quoted_text: Optional[str] = None
diff --git a/pilotcore/schemas/document.py b/pilotcore/schemas/document.py
new file mode 100644
index 0000000000000000000000000000000000000000..79d40cd43373a7e9f1994f309873f3dce6f8e2e5
--- /dev/null
+++ b/pilotcore/schemas/document.py
@@ -0,0 +1,15 @@
+from pydantic import BaseModel
+from datetime import datetime
+from typing import Optional
+
+
+class DocumentMetadata(BaseModel):
+ document_id: str
+
+ user_id: str
+
+ filename: str
+
+ uploaded_at: datetime
+
+ file_type: Optional[str] = None
diff --git a/pilotcore/schemas/retrieval.py b/pilotcore/schemas/retrieval.py
new file mode 100644
index 0000000000000000000000000000000000000000..8047b58fd8c2f3cb7ac4985fd7672e94ca881620
--- /dev/null
+++ b/pilotcore/schemas/retrieval.py
@@ -0,0 +1,21 @@
+from pydantic import BaseModel
+from typing import List
+
+from pilotcore.schemas.chunk import Chunk
+
+
+class RetrievedChunk(BaseModel):
+ chunk: Chunk
+ score: float
+
+
+class RetrievalResult(BaseModel):
+ trace_id: str
+
+ query: str
+
+ retrieved_chunks: List[RetrievedChunk]
+
+ latency_ms: float
+
+ retriever_version: str
diff --git a/pilotcore/schemas/span.py b/pilotcore/schemas/span.py
new file mode 100644
index 0000000000000000000000000000000000000000..f5045458850cc38445cb9befb69d62474e98f776
--- /dev/null
+++ b/pilotcore/schemas/span.py
@@ -0,0 +1,15 @@
+from pydantic import BaseModel
+from datetime import datetime
+from typing import Optional
+
+
+class Span(BaseModel):
+ span_id: str
+
+ trace_id: str
+
+ name: str
+
+ start_time: datetime
+
+ end_time: Optional[datetime] = None
diff --git a/pilotcore/schemas/trace.py b/pilotcore/schemas/trace.py
new file mode 100644
index 0000000000000000000000000000000000000000..9dcbaf4a57deb6cbf618d24c70c9c823c24374d4
--- /dev/null
+++ b/pilotcore/schemas/trace.py
@@ -0,0 +1,20 @@
+from pydantic import BaseModel
+from datetime import datetime
+from typing import Optional, List
+
+from pilotcore.schemas.retrieval import RetrievalResult
+from pilotcore.schemas.span import Span
+
+
+class Trace(BaseModel):
+ trace_id: str
+
+ user_query: str
+
+ retrieval_result: Optional[RetrievalResult] = None
+
+ final_response: Optional[str] = None
+
+ spans: List[Span] = []
+
+ created_at: datetime
diff --git a/pilotcore/tracing/__init__.py b/pilotcore/tracing/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/pilotcore/tracing/spans.py b/pilotcore/tracing/spans.py
new file mode 100644
index 0000000000000000000000000000000000000000..687c2cba004cde83b5ad1a57f886bfc24c2aa781
--- /dev/null
+++ b/pilotcore/tracing/spans.py
@@ -0,0 +1,23 @@
+from datetime import datetime
+
+from pilotcore.schemas.span import Span
+
+
+def start_span(
+ trace_id: str,
+ name: str,
+):
+
+ return Span(
+ span_id=f"{trace_id}:{name}",
+ trace_id=trace_id,
+ name=name,
+ start_time=datetime.utcnow(),
+ )
+
+
+def end_span(span: Span):
+
+ span.end_time = datetime.utcnow()
+
+ return span
diff --git a/pilotcore/tracing/telemetry.py b/pilotcore/tracing/telemetry.py
new file mode 100644
index 0000000000000000000000000000000000000000..1a7007ab1f175d60a469317ac39782bd7f11920a
--- /dev/null
+++ b/pilotcore/tracing/telemetry.py
@@ -0,0 +1,13 @@
+import requests
+from pilotcore.config import TRACEPILOT_URL
+
+
+def emit_event(event_type: str, payload: dict):
+ try:
+ requests.post(
+ f"{TRACEPILOT_URL}/tracepilot/events",
+ json={"event_type": event_type, "payload": payload},
+ timeout=1,
+ )
+ except Exception:
+ pass
diff --git a/pilotcore/tracing/trace_context.py b/pilotcore/tracing/trace_context.py
new file mode 100644
index 0000000000000000000000000000000000000000..b87eb23b517adb43dcfbee55cc83851fff260aa3
--- /dev/null
+++ b/pilotcore/tracing/trace_context.py
@@ -0,0 +1,6 @@
+import uuid
+
+
+def generate_trace_id():
+
+ return str(uuid.uuid4())
diff --git a/pilotcore/tracing/trace_manager.py b/pilotcore/tracing/trace_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..150de0513829e7a62100f954d7ba75eab3194bef
--- /dev/null
+++ b/pilotcore/tracing/trace_manager.py
@@ -0,0 +1,15 @@
+from datetime import datetime
+
+from pilotcore.schemas.trace import Trace
+
+
+def create_trace(
+ trace_id: str,
+ user_query: str,
+):
+
+ return Trace(
+ trace_id=trace_id,
+ user_query=user_query,
+ created_at=datetime.utcnow(),
+ )
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4a675b406be5ecab62f76b555e05a47d537a17ad
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,26 @@
+fastapi==0.115.12
+uvicorn[standard]==0.34.2
+python-multipart==0.0.20
+requests==2.32.3
+numpy==1.26.4
+faiss-cpu==1.7.4
+pypdf==5.5.0
+pytesseract==0.3.13
+pdf2image==1.17.0
+Pillow==10.3.0
+python-dotenv==1.1.0
+sqlalchemy==2.0.41
+psycopg2-binary==2.9.10
+passlib[bcrypt]==1.7.4
+bcrypt==4.0.1
+python-jose[cryptography]==3.4.0
+email-validator==2.2.0
+pydantic==2.11.5
+python-docx==1.1.2
+pandas==2.2.3
+openpyxl==3.1.5
+markdown==3.8
+sentence-transformers==4.1.0
+einops==0.8.1
+groq==0.24.0
+httpx==0.28.1
\ No newline at end of file
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..97e3f52d75b70e403f7cfc8b67ac8cc2a74d3c10
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,19 @@
+from setuptools import setup, find_packages
+
+setup(
+ name="pilotcore",
+ version="1.0.0",
+ description="PilotCore β the execution kernel for the PilotMaster AI observability ecosystem",
+ packages=find_packages(),
+ python_requires=">=3.10",
+ install_requires=[
+ "fastapi",
+ "groq",
+ "sentence-transformers",
+ "faiss-cpu",
+ "numpy",
+ "pydantic",
+ "python-dotenv",
+ "requests",
+ ],
+)