shak3008 commited on
Commit
e7e5bf0
·
0 Parent(s):

Initial PilotMaster deployment

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +58 -0
  2. DocPilot/backend/app/api/auth.py +166 -0
  3. DocPilot/backend/app/api/billing.py +61 -0
  4. DocPilot/backend/app/api/chat.py +91 -0
  5. DocPilot/backend/app/api/documents.py +181 -0
  6. DocPilot/backend/app/api/history.py +89 -0
  7. DocPilot/backend/app/core/config.py +1 -0
  8. DocPilot/backend/app/core/dependencies.py +43 -0
  9. DocPilot/backend/app/core/security.py +21 -0
  10. DocPilot/backend/app/db/database.py +9 -0
  11. DocPilot/backend/app/db/session.py +11 -0
  12. DocPilot/backend/app/main.py +52 -0
  13. DocPilot/backend/app/models/__init__.py +3 -0
  14. DocPilot/backend/app/models/chat.py +69 -0
  15. DocPilot/backend/app/models/document.py +29 -0
  16. DocPilot/backend/app/models/user.py +17 -0
  17. DocPilot/backend/app/schemas/auth.py +19 -0
  18. DocPilot/backend/app/schemas/chat.py +10 -0
  19. DocPilot/backend/app/schemas/document.py +25 -0
  20. DocPilot/backend/app/schemas/password.py +10 -0
  21. DocPilot/backend/app/services/ingestion.py +242 -0
  22. DocPilot/backend/app/services/rag.py +43 -0
  23. Dockerfile +15 -0
  24. README.md +273 -0
  25. TracePilot/.gitignore +5 -0
  26. TracePilot/backend/Dockerfile +0 -0
  27. TracePilot/backend/app/__init__.py +0 -0
  28. TracePilot/backend/app/analytics/failure_detector.py +30 -0
  29. TracePilot/backend/app/api/__init__.py +0 -0
  30. TracePilot/backend/app/core/config.py +1 -0
  31. TracePilot/backend/app/core/llm.py +1 -0
  32. TracePilot/backend/app/db/database.py +61 -0
  33. TracePilot/backend/app/evaluation/evaluator.py +16 -0
  34. TracePilot/backend/app/main.py +216 -0
  35. TracePilot/backend/app/models/trace.py +42 -0
  36. TracePilot/backend/app/pipelines/pipeline_runner.py +12 -0
  37. TracePilot/backend/app/tracing/replay.py +24 -0
  38. TracePilot/backend/app/tracing/trace_manager.py +137 -0
  39. TracePilot/backend/data/knowledge_base.txt +15 -0
  40. TracePilot/backend/requirements.txt +0 -0
  41. docker-compose.yml +61 -0
  42. frontend/index.html +12 -0
  43. frontend/package-lock.json +1914 -0
  44. frontend/package.json +20 -0
  45. frontend/src/App.jsx +313 -0
  46. frontend/src/docpilot/App.jsx +3 -0
  47. frontend/src/docpilot/api.js +35 -0
  48. frontend/src/docpilot/pages/Dashboard.jsx +260 -0
  49. frontend/src/docpilot/pages/ForgotPassword.jsx +275 -0
  50. frontend/src/docpilot/pages/Login.jsx +303 -0
.gitignore ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ *.egg-info/
7
+ *.egg
8
+ dist/
9
+ build/
10
+
11
+ # Virtual environments
12
+ venv/
13
+ .venv/
14
+ env/
15
+
16
+ # Environment variables — never commit real credentials
17
+ .env
18
+
19
+ # Databases
20
+ *.db
21
+ *.sqlite3
22
+
23
+ # FAISS vector stores — user data, never commit
24
+ vector_store/
25
+ *.index
26
+ *.pkl
27
+
28
+ # Uploaded files — user data, never commit
29
+ temp/
30
+
31
+ # Node
32
+ node_modules/
33
+
34
+ # Vite build output
35
+ dist/
36
+ .vite/
37
+
38
+ # OS
39
+ .DS_Store
40
+ Thumbs.db
41
+
42
+ # Logs
43
+ *.log
44
+
45
+ # IDE
46
+ .vscode/
47
+ .idea/
48
+
49
+ # React
50
+ coverage/
51
+
52
+ # Docker overrides
53
+ *.override.yml
54
+
55
+
56
+
57
+ hf_home/
58
+ .cache/
DocPilot/backend/app/api/auth.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ reset_tokens = {}
2
+
3
+ import secrets
4
+
5
+ from fastapi import (
6
+ APIRouter,
7
+ Depends,
8
+ HTTPException,
9
+ )
10
+
11
+ from fastapi.security import (
12
+ OAuth2PasswordRequestForm,
13
+ )
14
+
15
+ from sqlalchemy.orm import Session
16
+
17
+ from app.db.session import get_db
18
+
19
+ from app.models.user import User
20
+
21
+ from app.schemas.auth import (
22
+ UserCreate,
23
+ )
24
+
25
+ from app.schemas.password import (
26
+ ForgotPasswordRequest,
27
+ ResetPasswordRequest,
28
+ )
29
+
30
+ from app.core.security import (
31
+ hash_password,
32
+ verify_password,
33
+ create_access_token,
34
+ )
35
+
36
+ from app.core.dependencies import (
37
+ get_current_user,
38
+ )
39
+
40
+ router = APIRouter()
41
+
42
+
43
+ @router.post("/signup")
44
+ def signup(
45
+ user: UserCreate,
46
+ db: Session = Depends(get_db),
47
+ ):
48
+
49
+ existing_user = db.query(User).filter(User.email == user.email).first()
50
+
51
+ if existing_user:
52
+
53
+ raise HTTPException(
54
+ status_code=400,
55
+ detail="Email already registered",
56
+ )
57
+
58
+ new_user = User(
59
+ username=user.username,
60
+ email=user.email,
61
+ hashed_password=hash_password(user.password),
62
+ )
63
+
64
+ db.add(new_user)
65
+
66
+ db.commit()
67
+
68
+ db.refresh(new_user)
69
+
70
+ return {"message": "User created successfully"}
71
+
72
+
73
+ @router.post("/login")
74
+ def login(
75
+ form_data: OAuth2PasswordRequestForm = Depends(),
76
+ db: Session = Depends(get_db),
77
+ ):
78
+
79
+ db_user = db.query(User).filter(User.email == form_data.username).first()
80
+
81
+ if not db_user:
82
+
83
+ raise HTTPException(
84
+ status_code=401,
85
+ detail="Invalid credentials",
86
+ )
87
+
88
+ if not verify_password(
89
+ form_data.password,
90
+ db_user.hashed_password,
91
+ ):
92
+
93
+ raise HTTPException(
94
+ status_code=401,
95
+ detail="Invalid credentials",
96
+ )
97
+
98
+ access_token = create_access_token(data={"sub": db_user.email})
99
+
100
+ return {
101
+ "access_token": access_token,
102
+ "token_type": "bearer",
103
+ }
104
+
105
+
106
+ @router.get("/me")
107
+ def get_me(current_user=Depends(get_current_user)):
108
+
109
+ return {
110
+ "id": current_user.id,
111
+ "username": current_user.username,
112
+ "email": current_user.email,
113
+ "plan": current_user.plan,
114
+ }
115
+
116
+
117
+ @router.post("/forgot-password")
118
+ def forgot_password(
119
+ email_data: ForgotPasswordRequest,
120
+ db: Session = Depends(get_db),
121
+ ):
122
+
123
+ user = db.query(User).filter(User.email == email_data.email).first()
124
+
125
+ if not user:
126
+
127
+ raise HTTPException(
128
+ status_code=404,
129
+ detail="User not found",
130
+ )
131
+
132
+ token = secrets.token_hex(16)
133
+
134
+ reset_tokens[token] = user.email
135
+
136
+ return {"reset_token": token}
137
+
138
+
139
+ @router.post("/reset-password")
140
+ def reset_password(
141
+ data: ResetPasswordRequest,
142
+ db: Session = Depends(get_db),
143
+ ):
144
+
145
+ token = data.token
146
+
147
+ new_password = data.new_password
148
+
149
+ if token not in reset_tokens:
150
+
151
+ raise HTTPException(
152
+ status_code=400,
153
+ detail="Invalid token",
154
+ )
155
+
156
+ email = reset_tokens[token]
157
+
158
+ user = db.query(User).filter(User.email == email).first()
159
+
160
+ user.hashed_password = hash_password(new_password)
161
+
162
+ db.commit()
163
+
164
+ del reset_tokens[token]
165
+
166
+ return {"message": "Password reset successful"}
DocPilot/backend/app/api/billing.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import (
2
+ APIRouter,
3
+ Depends,
4
+ )
5
+
6
+ from sqlalchemy.orm import Session
7
+
8
+ from app.db.session import get_db
9
+
10
+ from app.core.dependencies import (
11
+ get_current_user,
12
+ )
13
+
14
+ router = APIRouter()
15
+
16
+
17
+ @router.get("/me")
18
+ def get_plan(
19
+ current_user=Depends(get_current_user),
20
+ ):
21
+
22
+ return {
23
+ "username": current_user.username,
24
+ "plan": current_user.plan,
25
+ }
26
+
27
+
28
+ @router.post("/upgrade")
29
+ def upgrade_plan(
30
+ current_user=Depends(get_current_user),
31
+ db: Session = Depends(get_db),
32
+ ):
33
+
34
+ current_user.plan = "pro"
35
+
36
+ db.commit()
37
+
38
+ db.refresh(current_user)
39
+
40
+ return {
41
+ "message": "Upgraded to pro plan.",
42
+ "plan": current_user.plan,
43
+ }
44
+
45
+
46
+ @router.post("/downgrade")
47
+ def downgrade_plan(
48
+ current_user=Depends(get_current_user),
49
+ db: Session = Depends(get_db),
50
+ ):
51
+
52
+ current_user.plan = "free"
53
+
54
+ db.commit()
55
+
56
+ db.refresh(current_user)
57
+
58
+ return {
59
+ "message": "Downgraded to free plan.",
60
+ "plan": current_user.plan,
61
+ }
DocPilot/backend/app/api/chat.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import (
2
+ APIRouter,
3
+ Depends,
4
+ HTTPException,
5
+ )
6
+
7
+ from groq import AuthenticationError
8
+
9
+ from sqlalchemy.orm import Session
10
+
11
+ from app.services.rag import (
12
+ ask_question,
13
+ )
14
+
15
+ from app.core.dependencies import (
16
+ get_current_user,
17
+ )
18
+
19
+ from app.db.session import get_db
20
+
21
+ from app.schemas.chat import ChatRequest
22
+
23
+ from app.models.chat import (
24
+ ChatSession,
25
+ ChatMessage,
26
+ )
27
+
28
+ router = APIRouter()
29
+
30
+
31
+ @router.post("/ask")
32
+ def ask(
33
+ query: ChatRequest,
34
+ current_user=Depends(get_current_user),
35
+ db: Session = Depends(get_db),
36
+ ):
37
+
38
+ session_id = query.session_id
39
+
40
+ if not session_id:
41
+
42
+ session = ChatSession(
43
+ owner_id=current_user.id,
44
+ title=query.question.strip()[:40],
45
+ )
46
+
47
+ db.add(session)
48
+
49
+ db.commit()
50
+
51
+ db.refresh(session)
52
+
53
+ session_id = session.id
54
+
55
+ user_message = ChatMessage(
56
+ session_id=session_id,
57
+ role="user",
58
+ content=query.question,
59
+ )
60
+
61
+ db.add(user_message)
62
+
63
+ db.commit()
64
+
65
+ try:
66
+ rag_response = ask_question(
67
+ question=query.question,
68
+ user_id=current_user.id,
69
+ source=query.source,
70
+ )
71
+ except AuthenticationError as exc:
72
+ raise HTTPException(
73
+ status_code=502,
74
+ detail="Groq authentication failed. Check GROQ_API_KEY in .env and restart the server.",
75
+ ) from exc
76
+
77
+ assistant_message = ChatMessage(
78
+ session_id=session_id,
79
+ role="assistant",
80
+ content=rag_response["answer"],
81
+ )
82
+
83
+ db.add(assistant_message)
84
+
85
+ db.commit()
86
+
87
+ return {
88
+ "session_id": session_id,
89
+ "answer": rag_response["answer"],
90
+ "sources": rag_response["sources"],
91
+ }
DocPilot/backend/app/api/documents.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import (
2
+ APIRouter,
3
+ UploadFile,
4
+ File,
5
+ Depends,
6
+ HTTPException,
7
+ )
8
+
9
+ from sqlalchemy.orm import Session
10
+
11
+ import shutil
12
+ import os
13
+
14
+ from app.services.ingestion import (
15
+ process_document,
16
+ )
17
+
18
+ from pilotcore.retrieval.vector_store import (
19
+ reset_vector_store,
20
+ rebuild_index_without_document,
21
+ )
22
+
23
+ from app.core.dependencies import (
24
+ get_current_user,
25
+ )
26
+
27
+ from app.db.session import get_db
28
+
29
+ from app.models.document import Document
30
+
31
+ from app.schemas.document import (
32
+ DocumentResponse,
33
+ )
34
+
35
+ router = APIRouter()
36
+
37
+
38
+ @router.post("/upload")
39
+ async def upload_document(
40
+ file: UploadFile = File(...),
41
+ current_user=Depends(get_current_user),
42
+ db: Session = Depends(get_db),
43
+ ):
44
+
45
+ document_count = (
46
+ db.query(Document).filter(Document.owner_id == current_user.id).count()
47
+ )
48
+
49
+ if current_user.plan == "free" and document_count >= 3:
50
+
51
+ raise HTTPException(
52
+ status_code=403,
53
+ detail="Free plan upload limit reached.",
54
+ )
55
+
56
+ os.makedirs(
57
+ "temp",
58
+ exist_ok=True,
59
+ )
60
+
61
+ allowed_extensions = [
62
+ ".pdf",
63
+ ".docx",
64
+ ".txt",
65
+ ".md",
66
+ ".csv",
67
+ ".xlsx",
68
+ ".png",
69
+ ".jpg",
70
+ ".jpeg",
71
+ ]
72
+
73
+ file_ext = os.path.splitext(file.filename)[1].lower()
74
+
75
+ if file_ext not in allowed_extensions:
76
+
77
+ raise HTTPException(
78
+ status_code=400,
79
+ detail="Unsupported file type.",
80
+ )
81
+
82
+ file_path = f"temp/{file.filename}"
83
+
84
+ with open(
85
+ file_path,
86
+ "wb",
87
+ ) as buffer:
88
+
89
+ shutil.copyfileobj(
90
+ file.file,
91
+ buffer,
92
+ )
93
+
94
+ document = Document(
95
+ owner_id=current_user.id,
96
+ filename=file.filename,
97
+ filepath=file_path,
98
+ file_size=os.path.getsize(file_path),
99
+ )
100
+
101
+ db.add(document)
102
+
103
+ db.commit()
104
+
105
+ db.refresh(document)
106
+
107
+ process_document(
108
+ file_path,
109
+ current_user.id,
110
+ document.id,
111
+ )
112
+
113
+ return {
114
+ "message": "Document uploaded",
115
+ "document_id": document.id,
116
+ }
117
+
118
+
119
+ @router.get(
120
+ "/",
121
+ response_model=list[DocumentResponse],
122
+ )
123
+ def get_documents(
124
+ db: Session = Depends(get_db),
125
+ current_user=Depends(get_current_user),
126
+ ):
127
+
128
+ documents = db.query(Document).filter(Document.owner_id == current_user.id).all()
129
+
130
+ return documents
131
+
132
+
133
+ @router.delete("/reset")
134
+ def reset_documents(
135
+ current_user=Depends(get_current_user),
136
+ ):
137
+
138
+ reset_vector_store(current_user.id)
139
+
140
+ return {"message": "Vector store cleared."}
141
+
142
+
143
+ @router.delete("/{document_id}")
144
+ def delete_document(
145
+ document_id: int,
146
+ db: Session = Depends(get_db),
147
+ current_user=Depends(get_current_user),
148
+ ):
149
+
150
+ document = (
151
+ db.query(Document)
152
+ .filter(
153
+ Document.id == document_id,
154
+ Document.owner_id == current_user.id,
155
+ )
156
+ .first()
157
+ )
158
+
159
+ if not document:
160
+
161
+ raise HTTPException(
162
+ status_code=404,
163
+ detail="Document not found",
164
+ )
165
+
166
+ if os.path.exists(document.filepath):
167
+
168
+ os.remove(document.filepath)
169
+
170
+ rebuild_index_without_document(
171
+ current_user.id,
172
+ document.id,
173
+ )
174
+
175
+ db.delete(document)
176
+
177
+ db.commit()
178
+
179
+ return {
180
+ "message": "Document deleted",
181
+ }
DocPilot/backend/app/api/history.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import (
2
+ APIRouter,
3
+ Depends,
4
+ )
5
+
6
+ from sqlalchemy.orm import Session
7
+
8
+ from app.db.session import get_db
9
+
10
+ from app.models.chat import (
11
+ ChatSession,
12
+ ChatMessage,
13
+ )
14
+
15
+ from app.core.dependencies import (
16
+ get_current_user,
17
+ )
18
+
19
+ router = APIRouter()
20
+
21
+
22
+ @router.get("/sessions")
23
+ def get_sessions(
24
+ db: Session = Depends(get_db),
25
+ current_user=Depends(get_current_user),
26
+ ):
27
+
28
+ sessions = (
29
+ db.query(ChatSession)
30
+ .filter(ChatSession.owner_id == current_user.id)
31
+ .order_by(ChatSession.id.desc())
32
+ .all()
33
+ )
34
+
35
+ return sessions
36
+
37
+
38
+ @router.get("/{session_id}")
39
+ def get_session_messages(
40
+ session_id: int,
41
+ db: Session = Depends(get_db),
42
+ current_user=Depends(get_current_user),
43
+ ):
44
+
45
+ session = (
46
+ db.query(ChatSession)
47
+ .filter(
48
+ ChatSession.id == session_id,
49
+ ChatSession.owner_id == current_user.id,
50
+ )
51
+ .first()
52
+ )
53
+
54
+ if not session:
55
+
56
+ return {"detail": "Session not found"}
57
+
58
+ messages = db.query(ChatMessage).filter(ChatMessage.session_id == session_id).all()
59
+
60
+ return messages
61
+
62
+
63
+ @router.delete("/{session_id}")
64
+ def delete_session(
65
+ session_id: int,
66
+ db: Session = Depends(get_db),
67
+ current_user=Depends(get_current_user),
68
+ ):
69
+
70
+ session = (
71
+ db.query(ChatSession)
72
+ .filter(
73
+ ChatSession.id == session_id,
74
+ ChatSession.owner_id == current_user.id,
75
+ )
76
+ .first()
77
+ )
78
+
79
+ if not session:
80
+
81
+ return {"detail": "Session not found"}
82
+
83
+ db.query(ChatMessage).filter(ChatMessage.session_id == session_id).delete()
84
+
85
+ db.delete(session)
86
+
87
+ db.commit()
88
+
89
+ return {"message": "Chat deleted"}
DocPilot/backend/app/core/config.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from pilotcore.config import DATABASE_URL, SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
DocPilot/backend/app/core/dependencies.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Depends, HTTPException
2
+
3
+ from fastapi.security import OAuth2PasswordBearer
4
+
5
+ from jose import JWTError, jwt
6
+
7
+ from sqlalchemy.orm import Session
8
+
9
+ from app.db.session import get_db
10
+
11
+ from app.models.user import User
12
+
13
+ from app.core.security import SECRET_KEY, ALGORITHM
14
+
15
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
16
+
17
+
18
+ def get_current_user(
19
+ token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)
20
+ ):
21
+
22
+ credentials_exception = HTTPException(
23
+ status_code=401, detail="Could not validate credentials"
24
+ )
25
+
26
+ try:
27
+
28
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
29
+
30
+ email = payload.get("sub")
31
+
32
+ if email is None:
33
+ raise credentials_exception
34
+
35
+ except JWTError:
36
+ raise credentials_exception
37
+
38
+ user = db.query(User).filter(User.email == email).first()
39
+
40
+ if user is None:
41
+ raise credentials_exception
42
+
43
+ return user
DocPilot/backend/app/core/security.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timedelta
2
+ from jose import jwt
3
+ from passlib.context import CryptContext
4
+ from pilotcore.config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
5
+
6
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
7
+
8
+
9
+ def hash_password(password: str):
10
+ return pwd_context.hash(password)
11
+
12
+
13
+ def verify_password(plain_password, hashed_password):
14
+ return pwd_context.verify(plain_password, hashed_password)
15
+
16
+
17
+ def create_access_token(data: dict):
18
+ to_encode = data.copy()
19
+ expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
20
+ to_encode.update({"exp": expire})
21
+ return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
DocPilot/backend/app/db/database.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.orm import declarative_base, sessionmaker
3
+ from pilotcore.config import DATABASE_URL
4
+
5
+ engine = create_engine(DATABASE_URL)
6
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
7
+ Base = declarative_base()
8
+
9
+ from app.models import *
DocPilot/backend/app/db/session.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.db.database import SessionLocal
2
+
3
+
4
+ def get_db():
5
+ db = SessionLocal()
6
+
7
+ try:
8
+ yield db
9
+
10
+ finally:
11
+ db.close()
DocPilot/backend/app/main.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+
5
+ from app.api import (
6
+ chat,
7
+ documents,
8
+ auth,
9
+ billing,
10
+ )
11
+
12
+ from app.db.database import (
13
+ engine,
14
+ Base,
15
+ )
16
+
17
+ from app.models import (
18
+ User,
19
+ Document,
20
+ ChatSession,
21
+ ChatMessage,
22
+ )
23
+ from app.api import history
24
+
25
+ Base.metadata.create_all(bind=engine)
26
+
27
+ app = FastAPI()
28
+
29
+ app.include_router(chat.router, prefix="/chat")
30
+
31
+ app.include_router(documents.router, prefix="/docs")
32
+
33
+ app.include_router(auth.router, prefix="/auth")
34
+
35
+ app.include_router(billing.router, prefix="/billing")
36
+ app.include_router(history.router, prefix="/history")
37
+
38
+ app.add_middleware(
39
+ CORSMiddleware,
40
+ allow_origins=["*"],
41
+ allow_credentials=True,
42
+ allow_methods=["*"],
43
+ allow_headers=["*"],
44
+ )
45
+
46
+
47
+ @app.get("/")
48
+ def root():
49
+
50
+ return {
51
+ "status": "running",
52
+ }
DocPilot/backend/app/models/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .user import User
2
+ from .document import Document
3
+ from .chat import ChatSession, ChatMessage
DocPilot/backend/app/models/chat.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import (
2
+ Column,
3
+ Integer,
4
+ Text,
5
+ String,
6
+ ForeignKey,
7
+ DateTime,
8
+ )
9
+
10
+ from sqlalchemy.sql import func
11
+
12
+ from app.db.database import Base
13
+
14
+
15
+ class ChatSession(Base):
16
+ __tablename__ = "chat_sessions"
17
+
18
+ id = Column(
19
+ Integer,
20
+ primary_key=True,
21
+ index=True,
22
+ )
23
+
24
+ owner_id = Column(
25
+ Integer,
26
+ ForeignKey("users.id"),
27
+ nullable=False,
28
+ )
29
+
30
+ title = Column(
31
+ String,
32
+ default="New Chat",
33
+ )
34
+
35
+ created_at = Column(
36
+ DateTime(timezone=True),
37
+ server_default=func.now(),
38
+ )
39
+
40
+
41
+ class ChatMessage(Base):
42
+ __tablename__ = "chat_messages"
43
+
44
+ id = Column(
45
+ Integer,
46
+ primary_key=True,
47
+ index=True,
48
+ )
49
+
50
+ session_id = Column(
51
+ Integer,
52
+ ForeignKey("chat_sessions.id"),
53
+ nullable=False,
54
+ )
55
+
56
+ role = Column(
57
+ Text,
58
+ nullable=False,
59
+ )
60
+
61
+ content = Column(
62
+ Text,
63
+ nullable=False,
64
+ )
65
+
66
+ created_at = Column(
67
+ DateTime(timezone=True),
68
+ server_default=func.now(),
69
+ )
DocPilot/backend/app/models/document.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
2
+
3
+ from sqlalchemy.sql import func
4
+
5
+ from app.db.database import Base
6
+
7
+
8
+ class Document(Base):
9
+ __tablename__ = "documents"
10
+
11
+ id = Column(Integer, primary_key=True, index=True)
12
+
13
+ owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
14
+
15
+ filename = Column(String, nullable=False)
16
+
17
+ filepath = Column(String, nullable=False)
18
+
19
+ file_size = Column(Integer)
20
+
21
+ page_count = Column(Integer)
22
+
23
+ chunk_count = Column(Integer)
24
+
25
+ ocr_used = Column(Boolean, default=False)
26
+
27
+ status = Column(String, default="processed")
28
+
29
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
DocPilot/backend/app/models/user.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String
2
+
3
+ from app.db.database import Base
4
+
5
+
6
+ class User(Base):
7
+ __tablename__ = "users"
8
+
9
+ id = Column(Integer, primary_key=True, index=True)
10
+
11
+ username = Column(String, unique=True, nullable=False)
12
+
13
+ email = Column(String, unique=True, nullable=False)
14
+
15
+ hashed_password = Column(String, nullable=False)
16
+
17
+ plan = Column(String, default="free")
DocPilot/backend/app/schemas/auth.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+ from typing import Optional
4
+
5
+
6
+ class UserCreate(BaseModel):
7
+ username: str
8
+ email: str
9
+ password: str
10
+
11
+
12
+ class UserLogin(BaseModel):
13
+ email: str
14
+ password: str
15
+
16
+
17
+ class Token(BaseModel):
18
+ access_token: str
19
+ token_type: str
DocPilot/backend/app/schemas/chat.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class ChatRequest(BaseModel):
5
+
6
+ question: str
7
+
8
+ source: str | None = None
9
+
10
+ session_id: int | None = None
DocPilot/backend/app/schemas/document.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from datetime import datetime
3
+
4
+
5
+ class DocumentResponse(BaseModel):
6
+ id: int
7
+
8
+ filename: str
9
+
10
+ filepath: str
11
+
12
+ file_size: int | None = None
13
+
14
+ page_count: int | None = None
15
+
16
+ chunk_count: int | None = None
17
+
18
+ ocr_used: bool
19
+
20
+ status: str
21
+
22
+ created_at: datetime
23
+
24
+ class Config:
25
+ from_attributes = True
DocPilot/backend/app/schemas/password.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class ForgotPasswordRequest(BaseModel):
5
+ email: str
6
+
7
+
8
+ class ResetPasswordRequest(BaseModel):
9
+ token: str
10
+ new_password: str
DocPilot/backend/app/services/ingestion.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pypdf import PdfReader
2
+
3
+ from pdf2image import convert_from_path
4
+
5
+ from docx import Document
6
+
7
+ import pandas as pd
8
+
9
+ import markdown
10
+
11
+ import pytesseract
12
+
13
+ from PIL import Image
14
+
15
+ import os
16
+ import time
17
+ import requests
18
+
19
+ from app.services.rag import add_chunks
20
+ from pilotcore.config import TRACEPILOT_URL
21
+
22
+ pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
23
+ POPPLER_PATH = r"C:\Users\Adhi\Desktop\poppler-26.02.0\Library\bin"
24
+
25
+
26
+ def chunk_text(
27
+ text,
28
+ chunk_size=500,
29
+ overlap=50,
30
+ ):
31
+
32
+ chunks = []
33
+
34
+ start = 0
35
+
36
+ while start < len(text):
37
+
38
+ end = start + chunk_size
39
+
40
+ chunks.append(text[start:end])
41
+
42
+ start += chunk_size - overlap
43
+
44
+ return chunks
45
+
46
+
47
+ def extract_pdf_text(
48
+ file_path,
49
+ ):
50
+
51
+ reader = PdfReader(file_path)
52
+
53
+ full_text = ""
54
+
55
+ for page in reader.pages:
56
+
57
+ text = page.extract_text() or ""
58
+
59
+ full_text += text + "\n"
60
+
61
+ return full_text
62
+
63
+
64
+ def extract_pdf_ocr(
65
+ file_path,
66
+ ):
67
+
68
+ images = convert_from_path(
69
+ file_path,
70
+ poppler_path=POPPLER_PATH,
71
+ )
72
+
73
+ text = ""
74
+
75
+ for image in images:
76
+
77
+ text += pytesseract.image_to_string(image)
78
+
79
+ return text
80
+
81
+
82
+ def extract_docx_text(
83
+ file_path,
84
+ ):
85
+
86
+ doc = Document(file_path)
87
+
88
+ return "\n".join(para.text for para in doc.paragraphs)
89
+
90
+
91
+ def extract_txt_text(
92
+ file_path,
93
+ ):
94
+
95
+ with open(
96
+ file_path,
97
+ "r",
98
+ encoding="utf-8",
99
+ ) as f:
100
+
101
+ return f.read()
102
+
103
+
104
+ def extract_md_text(
105
+ file_path,
106
+ ):
107
+
108
+ with open(
109
+ file_path,
110
+ "r",
111
+ encoding="utf-8",
112
+ ) as f:
113
+
114
+ return markdown.markdown(f.read())
115
+
116
+
117
+ def extract_csv_text(
118
+ file_path,
119
+ ):
120
+
121
+ df = pd.read_csv(file_path)
122
+
123
+ return df.to_string()
124
+
125
+
126
+ def extract_xlsx_text(
127
+ file_path,
128
+ ):
129
+
130
+ df = pd.read_excel(file_path)
131
+
132
+ return df.to_string()
133
+
134
+
135
+ def extract_image_text(
136
+ file_path,
137
+ ):
138
+
139
+ image = Image.open(file_path)
140
+
141
+ return pytesseract.image_to_string(image)
142
+
143
+
144
+ def extract_text(
145
+ file_path,
146
+ ):
147
+
148
+ ext = os.path.splitext(file_path)[1].lower()
149
+
150
+ if ext == ".pdf":
151
+
152
+ text = extract_pdf_text(file_path)
153
+
154
+ if len(text.strip()) < 10:
155
+
156
+ print("OCR triggered")
157
+
158
+ text = extract_pdf_ocr(file_path)
159
+
160
+ return text
161
+
162
+ elif ext == ".docx":
163
+
164
+ return extract_docx_text(file_path)
165
+
166
+ elif ext == ".txt":
167
+
168
+ return extract_txt_text(file_path)
169
+
170
+ elif ext == ".md":
171
+
172
+ return extract_md_text(file_path)
173
+
174
+ elif ext == ".csv":
175
+
176
+ return extract_csv_text(file_path)
177
+
178
+ elif ext == ".xlsx":
179
+
180
+ return extract_xlsx_text(file_path)
181
+
182
+ elif ext in [
183
+ ".png",
184
+ ".jpg",
185
+ ".jpeg",
186
+ ]:
187
+
188
+ return extract_image_text(file_path)
189
+
190
+ else:
191
+
192
+ raise Exception("Unsupported file type")
193
+
194
+
195
+ def process_document(
196
+ file_path,
197
+ user_id,
198
+ document_id,
199
+ ):
200
+
201
+ start_time = time.perf_counter()
202
+
203
+ text = extract_text(file_path)
204
+
205
+ text = " ".join(text.split())
206
+
207
+ if len(text.split()) < 5:
208
+ return
209
+
210
+ chunks = chunk_text(text)
211
+
212
+ all_chunks = [
213
+ {
214
+ "document_id": document_id,
215
+ "text": chunk,
216
+ "source": os.path.basename(file_path),
217
+ "page": 1,
218
+ "chunk_id": i,
219
+ }
220
+ for i, chunk in enumerate(chunks)
221
+ ]
222
+
223
+ add_chunks(all_chunks, user_id)
224
+
225
+ latency_ms = (time.perf_counter() - start_time) * 1000
226
+
227
+ try:
228
+ requests.post(
229
+ f"{TRACEPILOT_URL}/tracepilot/ingest/document",
230
+ json={
231
+ "document_id": str(document_id),
232
+ "user_id": str(user_id),
233
+ "filename": os.path.basename(file_path),
234
+ "chunk_count": len(all_chunks),
235
+ "char_count": len(text),
236
+ "latency_ms": round(latency_ms, 2),
237
+ "status": "success",
238
+ },
239
+ timeout=2,
240
+ )
241
+ except Exception:
242
+ pass
DocPilot/backend/app/services/rag.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pilotcore.retrieval.embeddings import get_embedding
2
+ from pilotcore.retrieval.vector_store import add_vector
3
+ from pilotcore.runtime.pipeline import run_pipeline
4
+
5
+
6
+ def add_chunks(chunks, user_id):
7
+ for chunk in chunks:
8
+ embedding = get_embedding(chunk["text"])
9
+ add_vector(
10
+ user_id=user_id,
11
+ embedding=embedding,
12
+ text=chunk["text"],
13
+ source=chunk["source"],
14
+ page=chunk["page"],
15
+ chunk_id=chunk["chunk_id"],
16
+ document_id=chunk["document_id"],
17
+ )
18
+
19
+
20
+ def ask_question(question, user_id, source=None):
21
+ trace = run_pipeline(
22
+ query=question,
23
+ user_id=user_id,
24
+ source=source,
25
+ )
26
+
27
+ retrieved = trace.retrieval_result.retrieved_chunks
28
+
29
+ if not retrieved:
30
+ return {"answer": "No relevant context found.", "sources": []}
31
+
32
+ sources = []
33
+ seen = set()
34
+ for item in retrieved:
35
+ key = (item.chunk.source, item.chunk.page_number)
36
+ if key not in seen:
37
+ seen.add(key)
38
+ sources.append({"source": item.chunk.source, "page": item.chunk.page_number})
39
+
40
+ return {
41
+ "answer": trace.final_response,
42
+ "sources": sources,
43
+ }
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y \
6
+ tesseract-ocr \
7
+ poppler-utils
8
+
9
+ COPY . .
10
+
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ EXPOSE 7860
14
+
15
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: PilotMaster Backend
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_file: Dockerfile
8
+ app_port: 7860
9
+ pinned: false
10
+ ---
11
+
12
+ # PilotMaster
13
+
14
+ 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.
15
+
16
+ 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.
17
+
18
+ ---
19
+
20
+ ## What it is
21
+
22
+ PilotMaster is made up of three layers that work together:
23
+
24
+ ### PilotCore — the execution kernel
25
+
26
+ 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.
27
+
28
+ ### DocPilot — the user-facing product
29
+
30
+ 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.
31
+
32
+ ### TracePilot — the observability layer
33
+
34
+ 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.
35
+
36
+ ---
37
+
38
+ ## How a single question flows through the system
39
+
40
+ 1. User types a question in DocPilot
41
+ 2. DocPilot calls `run_pipeline(query, user_id, source)` in PilotCore
42
+ 3. PilotCore embeds the query using `all-mpnet-base-v2` (sentence-transformers, runs locally)
43
+ 4. PilotCore searches the user's FAISS vector store for relevant chunks
44
+ 5. Irrelevant chunks (L2 distance > 1.4) are filtered out before the LLM sees them
45
+ 6. PilotCore builds a prompt — QA-style for direct questions, summarization-style for broad requests
46
+ 7. PilotCore calls Groq (`llama-3.1-8b-instant`) for generation
47
+ 8. PilotCore runs four-dimensional evaluation on the response
48
+ 9. PilotCore emits the full trace to TracePilot via HTTP
49
+ 10. User sees the answer in DocPilot. AI engineer sees the trace in TracePilot.
50
+
51
+ ---
52
+
53
+ ## The evaluation system
54
+
55
+ Every response is judged across four independent dimensions:
56
+
57
+ **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.
58
+
59
+ **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.
60
+
61
+ **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.
62
+
63
+ **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.
64
+
65
+ ---
66
+
67
+ ## Tech stack
68
+
69
+ | Layer | Technology |
70
+ | ------------ | ------------------------------------------------------------------------ |
71
+ | LLM | Groq — llama-3.1-8b-instant |
72
+ | Embeddings | sentence-transformers — all-mpnet-base-v2 (local, no server needed) |
73
+ | Vector store | FAISS (per-user, persisted to disk) |
74
+ | Backend | FastAPI (unified server, DocPilot + TracePilot mounted as sub-apps) |
75
+ | Database | PostgreSQL (DocPilot users/docs/chat), SQLite (TracePilot traces) |
76
+ | Frontend | React + Vite (unified app, tab-switched between DocPilot and TracePilot) |
77
+
78
+ ---
79
+
80
+ ## Running it
81
+
82
+ ### Prerequisites
83
+
84
+ - Python 3.10+
85
+ - PostgreSQL running locally (`rag_saas` database)
86
+ - A Groq API key
87
+
88
+ ### Setup
89
+
90
+ ```bash
91
+ # Install Python dependencies
92
+ pip install -r DocPilot/backend/requirements.txt
93
+ pip install -e .
94
+
95
+ # Install frontend dependencies
96
+ cd frontend && npm install
97
+ ```
98
+
99
+ ### Configure
100
+
101
+ Copy `.env.example` to `.env` and fill in your values:
102
+
103
+ ```bash
104
+ cp .env.example .env
105
+ ```
106
+
107
+ ```env
108
+ GROQ_API_KEY=your_groq_api_key_here
109
+ DATABASE_URL=postgresql://postgres:yourpassword@localhost:5432/rag_saas
110
+ SECRET_KEY=your_secret_key_here
111
+ ```
112
+
113
+ Get a free Groq API key at [console.groq.com](https://console.groq.com).
114
+
115
+ ### Start
116
+
117
+ ```bash
118
+ # Terminal 1 — unified backend (DocPilot + TracePilot on one server)
119
+ uvicorn main:app --reload --port 8000
120
+
121
+ # Terminal 2 — unified frontend (DocPilot + TracePilot in one window)
122
+ cd frontend && npm run dev
123
+ ```
124
+
125
+ Open `http://localhost:5173`. Sign up, log in, and you're in PilotMaster.
126
+
127
+ ---
128
+
129
+ ## 🌐 Production Cloud Deployment
130
+
131
+ PilotMaster is architected to decouple cleanly across managed infrastructure tiers for production environments:
132
+
133
+ ### 1. Database Tier (Serverless Postgres)
134
+
135
+ - **Provider:** Neon.tech or Supabase.
136
+ - **Setup:** Provision a managed PostgreSQL instance and use the provided serverless connection string (`postgresql://...`) to replace your local database URL.
137
+
138
+ ### 2. Backend API Substrate (FastAPI Core)
139
+
140
+ - **Provider:** Render or Railway (Python Web Service Web Tier).
141
+ - **Build Command:** `pip install -r DocPilot/backend/requirements.txt && pip install -e .`
142
+ - **Start Command:** `uvicorn main:app --host 0.0.0.0 --port $PORT`
143
+ - **Environment Variables Required:**
144
+ - `GROQ_API_KEY`: Production hardware passkey from console.groq.com.
145
+ - `DATABASE_URL`: Your live cloud connection string.
146
+ - `SECRET_KEY`: A secure string for handling application auth state.
147
+
148
+ ### 3. Frontend Application Layer (React + Vite static edge)
149
+
150
+ - **Provider:** Vercel or Netlify.
151
+ - **Root Directory:** `frontend/`
152
+ - **Build Command:** `npm run build`
153
+ - **Output Directory:** `dist`
154
+ - **Environment Variables Required:**
155
+ - `VITE_API_BASE_URL`: Point this to your live Render/Railway backend domain.
156
+
157
+ ## Project structure
158
+
159
+ ```
160
+ PilotMaster/
161
+ ├── main.py # Unified backend entry point
162
+ ├── .env # Single source of truth for all config
163
+ ├── frontend/ # Unified React app (DocPilot + TracePilot)
164
+ │ └── src/
165
+ │ ├── App.jsx # PilotMaster home, auth, routing
166
+ │ ├── docpilot/ # DocPilot workspace
167
+ │ └── tracepilot/ # TracePilot workspace
168
+ ├── pilotcore/ # Execution kernel (shared by both apps)
169
+ │ ├── config.py # Central config — all services import from here
170
+ │ ├── evaluation/ # Multi-dimensional response evaluation
171
+ │ ├── generation/ # Groq LLM client + prompt builder
172
+ │ ├── retrieval/ # Embeddings, FAISS vector store, retrieval runtime
173
+ │ ├── runtime/ # Pipeline orchestration + trace emission
174
+ │ ├── schemas/ # Canonical data contracts (Trace, Chunk, Span, etc.)
175
+ │ └── tracing/ # Span timing, trace creation, telemetry
176
+ ├── DocPilot/backend/ # Product layer — auth, billing, documents, chat
177
+ └── TracePilot/backend/ # Observability layer — trace storage, evaluation, replay
178
+ ```
179
+
180
+ ---
181
+
182
+ ## PilotCore — the execution kernel
183
+
184
+ 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.
185
+
186
+ 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.
187
+
188
+ **What PilotCore owns:**
189
+
190
+ - **Embeddings** — converts text to vectors using `all-mpnet-base-v2` running locally via sentence-transformers. No external embedding API, no network dependency.
191
+ - **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.
192
+ - **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.
193
+ - **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.
194
+ - **Generation** — calls Groq's `llama-3.1-8b-instant` with the constructed prompt and returns the response.
195
+ - **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.
196
+ - **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.
197
+ - **Schemas** — defines the canonical data contracts used across the system: Trace, Chunk, RetrievedChunk, RetrievalResult, Span, Citation, DocumentMetadata.
198
+ - **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.
199
+
200
+ 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.
201
+
202
+ ---
203
+
204
+ ## Using DocPilot
205
+
206
+ DocPilot is the document intelligence interface. Here's what a typical session looks like:
207
+
208
+ **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.
209
+
210
+ **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.
211
+
212
+ **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.
213
+
214
+ **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.
215
+
216
+ **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.
217
+
218
+ **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.
219
+
220
+ ---
221
+
222
+ ## Using TracePilot
223
+
224
+ TracePilot is the execution intelligence dashboard. It's designed to be open alongside DocPilot — every question asked in DocPilot automatically appears here within seconds.
225
+
226
+ **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.
227
+
228
+ **Inspecting a trace.** Click any trace to open the full detail view. At the top you'll see the four evaluation dimensions:
229
+
230
+ - **retrieval** — how semantically close were the retrieved chunks to the query?
231
+ - **grounding** — how much of the response came from the retrieved evidence?
232
+ - **answerability** — did the document actually contain enough to answer this?
233
+ - **hallucination risk** — how much did the response go beyond the evidence?
234
+
235
+ 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.
236
+
237
+ **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.
238
+
239
+ **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.
240
+
241
+ **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.
242
+
243
+ **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.
244
+
245
+ **Jump to DocPilot.** The DocPilot → button in the header takes you back to the chat interface without losing your place.
246
+
247
+ ---
248
+
249
+ ## What makes this different
250
+
251
+ 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.
252
+
253
+ 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.
254
+
255
+ ---
256
+
257
+ ## Architectural honesty — what this is and what it isn’t
258
+
259
+ 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.
260
+
261
+ **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.
262
+
263
+ **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.
264
+
265
+ **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.
266
+
267
+ **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.
268
+
269
+ **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.
270
+
271
+ **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.
272
+
273
+ These are not oversights. They are the known frontier of the system — the places where the architecture is intentionally designed to grow.
TracePilot/.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ venv/
2
+ .env
3
+ __pycache__/
4
+ *.pyc
5
+ tracepilot.db
TracePilot/backend/Dockerfile ADDED
File without changes
TracePilot/backend/app/__init__.py ADDED
File without changes
TracePilot/backend/app/analytics/failure_detector.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.tracing.trace_manager import get_traces
2
+
3
+
4
+ def detect_failures():
5
+
6
+ traces = get_traces()
7
+
8
+ failures = []
9
+
10
+ for trace in traces:
11
+
12
+ reasons = []
13
+
14
+ if trace.retrieval_quality == "poor":
15
+ reasons.append("poor_retrieval")
16
+
17
+ if trace.latency > 2000:
18
+ reasons.append("high_latency")
19
+
20
+ if reasons:
21
+
22
+ failures.append({
23
+ "trace_id": trace.trace_id,
24
+ "query": trace.query,
25
+ "reasons": reasons,
26
+ "latency": trace.latency,
27
+ "retrieval_quality": trace.retrieval_quality
28
+ })
29
+
30
+ return failures
TracePilot/backend/app/api/__init__.py ADDED
File without changes
TracePilot/backend/app/core/config.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from pilotcore.config import GROQ_API_KEY, GROQ_MODEL, TRACEPILOT_URL, DOCPILOT_URL
TracePilot/backend/app/core/llm.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from pilotcore.generation.generator import generate_response
TracePilot/backend/app/db/database.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+
3
+ DB_PATH = "tracepilot.db"
4
+
5
+
6
+ def get_connection():
7
+ conn = sqlite3.connect(DB_PATH)
8
+ conn.row_factory = sqlite3.Row
9
+ return conn
10
+
11
+
12
+ def init_db():
13
+
14
+ conn = get_connection()
15
+ cursor = conn.cursor()
16
+
17
+ cursor.execute("""
18
+ CREATE TABLE IF NOT EXISTS traces (
19
+ trace_id TEXT PRIMARY KEY,
20
+ query TEXT,
21
+ retrieved_chunks TEXT,
22
+ prompt TEXT,
23
+ response TEXT,
24
+ latency REAL,
25
+ timestamp TEXT,
26
+ model_name TEXT,
27
+ retrieval_score_avg REAL,
28
+ response_length INTEGER,
29
+ chunk_count INTEGER,
30
+ parent_trace_id TEXT,
31
+ retrieval_quality TEXT,
32
+ grounded BOOLEAN,
33
+ top_retrieval_score REAL,
34
+ spans TEXT,
35
+ failure_types TEXT,
36
+ prompt_mode TEXT DEFAULT 'strict',
37
+ evaluation TEXT,
38
+ user_id TEXT,
39
+ source TEXT,
40
+ evaluator_version TEXT DEFAULT '1.0',
41
+ prompt_version TEXT DEFAULT '1.0',
42
+ retriever_version TEXT DEFAULT 'vector_v1'
43
+ )
44
+ """)
45
+
46
+ cursor.execute("""
47
+ CREATE TABLE IF NOT EXISTS ingestion_traces (
48
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
49
+ document_id TEXT,
50
+ user_id TEXT,
51
+ filename TEXT,
52
+ chunk_count INTEGER,
53
+ char_count INTEGER,
54
+ latency_ms REAL,
55
+ status TEXT,
56
+ timestamp TEXT
57
+ )
58
+ """)
59
+
60
+ conn.commit()
61
+ conn.close()
TracePilot/backend/app/evaluation/evaluator.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pilotcore.evaluation.evaluator import run_evaluation
2
+
3
+
4
+ class Evaluator:
5
+
6
+ def evaluate(self, query, response, chunks):
7
+ scores = [c.get("score", 0) if isinstance(c, dict) else c.score for c in chunks]
8
+ result = run_evaluation(query=query, response=response, chunks=chunks, scores=scores)
9
+ # Map to legacy keys pipeline_runner still uses
10
+ return {
11
+ "grounded": result.get("grounded", False),
12
+ "hallucination_score": 1.0 - result.get("faithfulness_score", 0.0),
13
+ "faithfulness_score": result.get("faithfulness_score", 0.0),
14
+ "abstained": result.get("abstained", False),
15
+ **result,
16
+ }
TracePilot/backend/app/main.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
+ from typing import List, Optional
5
+ from datetime import datetime
6
+
7
+ from app.db.database import init_db
8
+ from app.pipelines.pipeline_runner import PipelineRunner
9
+ from app.tracing.trace_manager import get_traces, get_trace_by_id, save_trace
10
+ from app.tracing.replay import replay_trace as run_replay
11
+ from app.analytics.failure_detector import detect_failures
12
+ from app.models.trace import Trace, RetrievedChunk
13
+
14
+ app = FastAPI()
15
+
16
+ app.add_middleware(
17
+ CORSMiddleware,
18
+ allow_origins=["*"],
19
+ allow_credentials=True,
20
+ allow_methods=["*"],
21
+ allow_headers=["*"],
22
+ )
23
+
24
+ init_db()
25
+
26
+
27
+ class QueryRequest(BaseModel):
28
+ query: str
29
+ prompt_mode: str = "strict"
30
+
31
+
32
+ class IngestChunk(BaseModel):
33
+ chunk_id: str
34
+ text: str
35
+ score: float
36
+ rank: int
37
+
38
+
39
+ class IngestRequest(BaseModel):
40
+ trace_id: str
41
+ query: str
42
+ response: str
43
+ prompt: str
44
+ latency: float
45
+ model_name: str
46
+ retrieved_chunks: List[IngestChunk]
47
+ retrieval_score_avg: float
48
+ top_retrieval_score: float
49
+ chunk_count: int
50
+ response_length: int
51
+ retrieval_quality: str
52
+ grounded: bool
53
+ evaluation: Optional[dict] = None
54
+ spans: list = []
55
+ failure_types: list = []
56
+ prompt_mode: str = "strict"
57
+ parent_trace_id: Optional[str] = None
58
+ user_id: Optional[str] = None
59
+ source: Optional[str] = None
60
+ evaluator_version: Optional[str] = "1.0"
61
+ prompt_version: Optional[str] = "1.0"
62
+ retriever_version: Optional[str] = "vector_v1"
63
+
64
+
65
+ class EventRequest(BaseModel):
66
+ event_type: str
67
+ payload: dict
68
+
69
+
70
+ class IngestDocumentRequest(BaseModel):
71
+ document_id: str
72
+ user_id: str
73
+ filename: str
74
+ chunk_count: int
75
+ char_count: int
76
+ latency_ms: float
77
+ status: str
78
+
79
+
80
+ @app.post("/ingest/document")
81
+ def ingest_document(request: IngestDocumentRequest):
82
+ from app.db.database import get_connection
83
+ conn = get_connection()
84
+ cursor = conn.cursor()
85
+ cursor.execute("""
86
+ INSERT INTO ingestion_traces
87
+ (document_id, user_id, filename, chunk_count, char_count, latency_ms, status, timestamp)
88
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
89
+ """, (
90
+ request.document_id,
91
+ request.user_id,
92
+ request.filename,
93
+ request.chunk_count,
94
+ request.char_count,
95
+ request.latency_ms,
96
+ request.status,
97
+ str(datetime.utcnow()),
98
+ ))
99
+ conn.commit()
100
+ conn.close()
101
+ return {"status": "ok", "document_id": request.document_id}
102
+
103
+
104
+ @app.get("/ingestion-traces")
105
+ def get_ingestion_traces():
106
+ from app.db.database import get_connection
107
+ conn = get_connection()
108
+ cursor = conn.cursor()
109
+ cursor.execute("SELECT * FROM ingestion_traces ORDER BY timestamp DESC")
110
+ rows = cursor.fetchall()
111
+ conn.close()
112
+ return [dict(row) for row in rows]
113
+
114
+
115
+ @app.post("/ingest")
116
+ def ingest_trace(request: IngestRequest):
117
+ trace = Trace(
118
+ trace_id=request.trace_id,
119
+ query=request.query,
120
+ retrieved_chunks=[
121
+ RetrievedChunk(**c.dict()) for c in request.retrieved_chunks
122
+ ],
123
+ prompt=request.prompt,
124
+ response=request.response,
125
+ latency=request.latency,
126
+ timestamp=datetime.utcnow(),
127
+ model_name=request.model_name,
128
+ retrieval_score_avg=request.retrieval_score_avg,
129
+ response_length=request.response_length,
130
+ chunk_count=request.chunk_count,
131
+ parent_trace_id=request.parent_trace_id,
132
+ retrieval_quality=request.retrieval_quality,
133
+ grounded=request.grounded,
134
+ top_retrieval_score=request.top_retrieval_score,
135
+ spans=request.spans,
136
+ failure_types=request.failure_types,
137
+ prompt_mode=request.prompt_mode,
138
+ evaluation=request.evaluation or {},
139
+ user_id=request.user_id,
140
+ source=request.source,
141
+ evaluator_version=request.evaluator_version or "1.0",
142
+ prompt_version=request.prompt_version or "1.0",
143
+ retriever_version=request.retriever_version or "vector_v1",
144
+ )
145
+ save_trace(trace)
146
+ return {"status": "ok", "trace_id": trace.trace_id}
147
+
148
+
149
+ @app.post("/events")
150
+ def receive_event(request: EventRequest):
151
+ return {"status": "ok"}
152
+
153
+
154
+ @app.post("/ask")
155
+ def ask_question(request: QueryRequest):
156
+ runner = PipelineRunner()
157
+ return runner.run(request.query, prompt_mode=request.prompt_mode)
158
+
159
+
160
+ @app.get("/analytics/failures")
161
+ def get_failures():
162
+ return detect_failures()
163
+
164
+
165
+ @app.get("/traces")
166
+ def get_all_traces(retrieval_quality: str | None = None):
167
+ return get_traces(retrieval_quality)
168
+
169
+
170
+ @app.get("/traces/compare")
171
+ def compare_traces(trace_id_1: str, trace_id_2: str):
172
+ trace_1 = get_trace_by_id(trace_id_1)
173
+ trace_2 = get_trace_by_id(trace_id_2)
174
+
175
+ if isinstance(trace_1, dict):
176
+ raise HTTPException(status_code=404, detail="First trace not found")
177
+ if isinstance(trace_2, dict):
178
+ raise HTTPException(status_code=404, detail="Second trace not found")
179
+
180
+ return {
181
+ "trace_1": {
182
+ "trace_id": trace_1.trace_id,
183
+ "model_name": trace_1.model_name,
184
+ "latency": trace_1.latency,
185
+ "retrieval_score_avg": trace_1.retrieval_score_avg,
186
+ "response_length": trace_1.response_length,
187
+ "chunk_count": trace_1.chunk_count,
188
+ },
189
+ "trace_2": {
190
+ "trace_id": trace_2.trace_id,
191
+ "model_name": trace_2.model_name,
192
+ "latency": trace_2.latency,
193
+ "retrieval_score_avg": trace_2.retrieval_score_avg,
194
+ "response_length": trace_2.response_length,
195
+ "chunk_count": trace_2.chunk_count,
196
+ },
197
+ "differences": {
198
+ "latency_delta": round(trace_2.latency - trace_1.latency, 2),
199
+ "retrieval_score_delta": round(trace_2.retrieval_score_avg - trace_1.retrieval_score_avg, 2),
200
+ "response_length_delta": trace_2.response_length - trace_1.response_length,
201
+ "response_changed": trace_1.response != trace_2.response,
202
+ },
203
+ }
204
+
205
+
206
+ @app.get("/traces/{trace_id}")
207
+ def fetch_trace(trace_id: str):
208
+ return get_trace_by_id(trace_id)
209
+
210
+
211
+ @app.post("/traces/{trace_id}/replay")
212
+ def replay_trace_endpoint(trace_id: str):
213
+ result = run_replay(trace_id)
214
+ if "error" in result:
215
+ raise HTTPException(status_code=404, detail=result["error"])
216
+ return result
TracePilot/backend/app/models/trace.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import List
3
+ from uuid import uuid4
4
+ from datetime import datetime
5
+
6
+
7
+ class RetrievedChunk(BaseModel):
8
+ chunk_id: str
9
+ text: str
10
+ score: float
11
+ rank: int
12
+
13
+
14
+ class Trace(BaseModel):
15
+ trace_id: str
16
+ query: str
17
+ retrieved_chunks: List[RetrievedChunk]
18
+ prompt: str
19
+ response: str
20
+ latency: float
21
+ timestamp: datetime
22
+ model_name: str
23
+ retrieval_score_avg: float
24
+ response_length: int
25
+ chunk_count: int
26
+ parent_trace_id: str | None = None
27
+ retrieval_quality: str
28
+ grounded: bool
29
+ top_retrieval_score: float = 0.0
30
+ prompt_mode: str = "strict"
31
+ spans: list = []
32
+ failure_types: list = []
33
+ evaluation: dict = {}
34
+ user_id: str | None = None
35
+ source: str | None = None
36
+ evaluator_version: str = "1.0"
37
+ prompt_version: str = "1.0"
38
+ retriever_version: str = "vector_v1"
39
+
40
+ @staticmethod
41
+ def create_id():
42
+ return str(uuid4())
TracePilot/backend/app/pipelines/pipeline_runner.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pilotcore.runtime.pipeline import run_pipeline
2
+
3
+
4
+ class PipelineRunner:
5
+
6
+ def run(self, query, parent_trace_id=None, prompt_mode="strict"):
7
+ trace = run_pipeline(query=query)
8
+ return {
9
+ "trace_id": trace.trace_id,
10
+ "response": trace.final_response,
11
+ "parent_trace_id": parent_trace_id,
12
+ }
TracePilot/backend/app/tracing/replay.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.tracing.trace_manager import get_trace
2
+
3
+
4
+ def replay_trace(trace_id: str):
5
+
6
+ original_trace = get_trace(trace_id)
7
+
8
+ if not original_trace:
9
+ return {"error": "Trace not found"}
10
+
11
+ from pilotcore.runtime.pipeline import run_pipeline
12
+
13
+ trace = run_pipeline(
14
+ query=original_trace["query"],
15
+ user_id=original_trace.get("user_id"),
16
+ source=original_trace.get("source"),
17
+ )
18
+
19
+ return {
20
+ "trace_id": trace.trace_id,
21
+ "query": trace.user_query,
22
+ "response": trace.final_response,
23
+ "parent_trace_id": trace_id,
24
+ }
TracePilot/backend/app/tracing/trace_manager.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ from app.db.database import get_connection
4
+ from app.models.trace import Trace, RetrievedChunk
5
+
6
+
7
+ def save_trace(trace: Trace):
8
+
9
+ conn = get_connection()
10
+ cursor = conn.cursor()
11
+
12
+ cursor.execute("""
13
+ INSERT INTO traces (
14
+ trace_id, query, retrieved_chunks, prompt, response, latency,
15
+ timestamp, model_name, retrieval_score_avg, response_length,
16
+ chunk_count, parent_trace_id, retrieval_quality, grounded,
17
+ top_retrieval_score, spans, failure_types, prompt_mode, evaluation,
18
+ user_id, source, evaluator_version, prompt_version, retriever_version
19
+ )
20
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
21
+ """, (
22
+ trace.trace_id, trace.query,
23
+ json.dumps([chunk.dict() for chunk in trace.retrieved_chunks]),
24
+ trace.prompt, trace.response, trace.latency, str(trace.timestamp),
25
+ trace.model_name, trace.retrieval_score_avg, trace.response_length,
26
+ trace.chunk_count, trace.parent_trace_id, trace.retrieval_quality,
27
+ trace.grounded, trace.top_retrieval_score,
28
+ json.dumps(trace.spans), json.dumps(trace.failure_types),
29
+ trace.prompt_mode, json.dumps(trace.evaluation),
30
+ trace.user_id, trace.source,
31
+ trace.evaluator_version, trace.prompt_version, trace.retriever_version
32
+ ))
33
+
34
+ conn.commit()
35
+ conn.close()
36
+
37
+
38
+ def _row_to_trace(row) -> Trace:
39
+ return Trace(
40
+ trace_id=row["trace_id"],
41
+ query=row["query"],
42
+ retrieved_chunks=[
43
+ RetrievedChunk(**chunk)
44
+ for chunk in json.loads(row["retrieved_chunks"])
45
+ ],
46
+ prompt=row["prompt"],
47
+ response=row["response"],
48
+ latency=row["latency"],
49
+ timestamp=row["timestamp"],
50
+ model_name=row["model_name"],
51
+ retrieval_score_avg=row["retrieval_score_avg"],
52
+ response_length=row["response_length"],
53
+ chunk_count=row["chunk_count"],
54
+ parent_trace_id=row["parent_trace_id"],
55
+ retrieval_quality=row["retrieval_quality"],
56
+ grounded=row["grounded"],
57
+ top_retrieval_score=row["top_retrieval_score"],
58
+ spans=json.loads(row["spans"] or "[]"),
59
+ failure_types=json.loads(row["failure_types"] or "[]"),
60
+ prompt_mode=row["prompt_mode"] or "strict",
61
+ evaluation=json.loads(row["evaluation"] or "{}"),
62
+ user_id=row["user_id"],
63
+ source=row["source"],
64
+ evaluator_version=row["evaluator_version"] or "1.0",
65
+ prompt_version=row["prompt_version"] or "1.0",
66
+ retriever_version=row["retriever_version"] or "vector_v1",
67
+ )
68
+
69
+
70
+ def get_traces(retrieval_quality=None):
71
+
72
+ conn = get_connection()
73
+ cursor = conn.cursor()
74
+
75
+ if retrieval_quality:
76
+ cursor.execute(
77
+ "SELECT * FROM traces WHERE retrieval_quality = ? ORDER BY timestamp DESC",
78
+ (retrieval_quality,)
79
+ )
80
+ else:
81
+ cursor.execute("SELECT * FROM traces ORDER BY timestamp DESC")
82
+
83
+ rows = cursor.fetchall()
84
+ conn.close()
85
+
86
+ return [_row_to_trace(row) for row in rows]
87
+
88
+
89
+ def get_trace_by_id(trace_id: str):
90
+
91
+ conn = get_connection()
92
+ cursor = conn.cursor()
93
+ cursor.execute("SELECT * FROM traces WHERE trace_id = ?", (trace_id,))
94
+ row = cursor.fetchone()
95
+ conn.close()
96
+
97
+ if not row:
98
+ return {"error": "Trace not found"}
99
+
100
+ return _row_to_trace(row)
101
+
102
+
103
+ def get_trace(trace_id: str) -> dict | None:
104
+
105
+ conn = get_connection()
106
+ cursor = conn.cursor()
107
+ cursor.execute("SELECT * FROM traces WHERE trace_id = ?", (trace_id,))
108
+ row = cursor.fetchone()
109
+ conn.close()
110
+
111
+ if not row:
112
+ return None
113
+
114
+ return {
115
+ "trace_id": row["trace_id"],
116
+ "query": row["query"],
117
+ "retrieved_chunks": json.loads(row["retrieved_chunks"]),
118
+ "prompt": row["prompt"],
119
+ "response": row["response"],
120
+ "latency": row["latency"],
121
+ "timestamp": row["timestamp"],
122
+ "model_name": row["model_name"],
123
+ "retrieval_score_avg": row["retrieval_score_avg"],
124
+ "response_length": row["response_length"],
125
+ "chunk_count": row["chunk_count"],
126
+ "parent_trace_id": row["parent_trace_id"],
127
+ "retrieval_quality": row["retrieval_quality"],
128
+ "grounded": row["grounded"],
129
+ "top_retrieval_score": row["top_retrieval_score"],
130
+ "spans": json.loads(row["spans"] or "[]"),
131
+ "failure_types": json.loads(row["failure_types"] or "[]"),
132
+ "user_id": row["user_id"],
133
+ "source": row["source"],
134
+ "evaluator_version": row["evaluator_version"] or "1.0",
135
+ "prompt_version": row["prompt_version"] or "1.0",
136
+ "retriever_version": row["retriever_version"] or "vector_v1",
137
+ }
TracePilot/backend/data/knowledge_base.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Tawang is a town in Arunachal Pradesh in northeastern India.
2
+
3
+ Tawang Monastery is one of the largest Buddhist monasteries in India.
4
+
5
+ Visitors usually travel to Tawang from Guwahati via Tezpur.
6
+
7
+ The best time to visit Tawang is from March to October.
8
+
9
+ An Inner Line Permit is required for Indian citizens visiting Arunachal Pradesh.
10
+
11
+ Sela Pass is a famous mountain pass on the route to Tawang.
12
+
13
+ Madhuri Lake is a popular tourist attraction near Tawang.
14
+
15
+ Travelers visiting high-altitude areas should prepare for cold weather and altitude sickness.
TracePilot/backend/requirements.txt ADDED
Binary file (812 Bytes). View file
 
docker-compose.yml ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+
3
+ postgres:
4
+ image: postgres:16
5
+ environment:
6
+ POSTGRES_USER: postgres
7
+ POSTGRES_PASSWORD: root
8
+ POSTGRES_DB: rag_saas
9
+ ports:
10
+ - "5432:5432"
11
+ volumes:
12
+ - postgres_data:/var/lib/postgresql/data
13
+ healthcheck:
14
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
15
+ interval: 5s
16
+ timeout: 5s
17
+ retries: 5
18
+
19
+ docpilot-backend:
20
+ build: ./DocPilot/backend
21
+ ports:
22
+ - "8000:8000"
23
+ volumes:
24
+ - ./DocPilot/backend:/app
25
+ - ./pilotcore:/app/pilotcore
26
+ - ./.env:/.env
27
+ env_file:
28
+ - .env
29
+ depends_on:
30
+ postgres:
31
+ condition: service_healthy
32
+
33
+ docpilot-frontend:
34
+ build: ./DocPilot/frontend
35
+ ports:
36
+ - "5173:5173"
37
+ depends_on:
38
+ - docpilot-backend
39
+
40
+ tracepilot-backend:
41
+ build: ./TracePilot/backend
42
+ ports:
43
+ - "8001:8001"
44
+ volumes:
45
+ - ./TracePilot/backend:/app
46
+ - ./pilotcore:/app/pilotcore
47
+ - ./.env:/.env
48
+ env_file:
49
+ - .env
50
+ depends_on:
51
+ - docpilot-backend
52
+
53
+ tracepilot-frontend:
54
+ build: ./TracePilot/frontend
55
+ ports:
56
+ - "5174:5174"
57
+ depends_on:
58
+ - tracepilot-backend
59
+
60
+ volumes:
61
+ postgres_data:
frontend/index.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>PilotMaster</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.jsx"></script>
11
+ </body>
12
+ </html>
frontend/package-lock.json ADDED
@@ -0,0 +1,1914 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "pilotmaster-frontend",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "pilotmaster-frontend",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "axios": "^1.6.0",
12
+ "react": "^18.2.0",
13
+ "react-dom": "^18.2.0",
14
+ "react-dropzone": "^14.3.8"
15
+ },
16
+ "devDependencies": {
17
+ "@vitejs/plugin-react": "^4.2.0",
18
+ "vite": "^5.0.0"
19
+ }
20
+ },
21
+ "node_modules/@babel/code-frame": {
22
+ "version": "7.29.0",
23
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
24
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
25
+ "dev": true,
26
+ "dependencies": {
27
+ "@babel/helper-validator-identifier": "^7.28.5",
28
+ "js-tokens": "^4.0.0",
29
+ "picocolors": "^1.1.1"
30
+ },
31
+ "engines": {
32
+ "node": ">=6.9.0"
33
+ }
34
+ },
35
+ "node_modules/@babel/compat-data": {
36
+ "version": "7.29.3",
37
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
38
+ "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
39
+ "dev": true,
40
+ "engines": {
41
+ "node": ">=6.9.0"
42
+ }
43
+ },
44
+ "node_modules/@babel/core": {
45
+ "version": "7.29.0",
46
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
47
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
48
+ "dev": true,
49
+ "dependencies": {
50
+ "@babel/code-frame": "^7.29.0",
51
+ "@babel/generator": "^7.29.0",
52
+ "@babel/helper-compilation-targets": "^7.28.6",
53
+ "@babel/helper-module-transforms": "^7.28.6",
54
+ "@babel/helpers": "^7.28.6",
55
+ "@babel/parser": "^7.29.0",
56
+ "@babel/template": "^7.28.6",
57
+ "@babel/traverse": "^7.29.0",
58
+ "@babel/types": "^7.29.0",
59
+ "@jridgewell/remapping": "^2.3.5",
60
+ "convert-source-map": "^2.0.0",
61
+ "debug": "^4.1.0",
62
+ "gensync": "^1.0.0-beta.2",
63
+ "json5": "^2.2.3",
64
+ "semver": "^6.3.1"
65
+ },
66
+ "engines": {
67
+ "node": ">=6.9.0"
68
+ },
69
+ "funding": {
70
+ "type": "opencollective",
71
+ "url": "https://opencollective.com/babel"
72
+ }
73
+ },
74
+ "node_modules/@babel/generator": {
75
+ "version": "7.29.1",
76
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
77
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
78
+ "dev": true,
79
+ "dependencies": {
80
+ "@babel/parser": "^7.29.0",
81
+ "@babel/types": "^7.29.0",
82
+ "@jridgewell/gen-mapping": "^0.3.12",
83
+ "@jridgewell/trace-mapping": "^0.3.28",
84
+ "jsesc": "^3.0.2"
85
+ },
86
+ "engines": {
87
+ "node": ">=6.9.0"
88
+ }
89
+ },
90
+ "node_modules/@babel/helper-compilation-targets": {
91
+ "version": "7.28.6",
92
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
93
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
94
+ "dev": true,
95
+ "dependencies": {
96
+ "@babel/compat-data": "^7.28.6",
97
+ "@babel/helper-validator-option": "^7.27.1",
98
+ "browserslist": "^4.24.0",
99
+ "lru-cache": "^5.1.1",
100
+ "semver": "^6.3.1"
101
+ },
102
+ "engines": {
103
+ "node": ">=6.9.0"
104
+ }
105
+ },
106
+ "node_modules/@babel/helper-globals": {
107
+ "version": "7.28.0",
108
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
109
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
110
+ "dev": true,
111
+ "engines": {
112
+ "node": ">=6.9.0"
113
+ }
114
+ },
115
+ "node_modules/@babel/helper-module-imports": {
116
+ "version": "7.28.6",
117
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
118
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
119
+ "dev": true,
120
+ "dependencies": {
121
+ "@babel/traverse": "^7.28.6",
122
+ "@babel/types": "^7.28.6"
123
+ },
124
+ "engines": {
125
+ "node": ">=6.9.0"
126
+ }
127
+ },
128
+ "node_modules/@babel/helper-module-transforms": {
129
+ "version": "7.28.6",
130
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
131
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
132
+ "dev": true,
133
+ "dependencies": {
134
+ "@babel/helper-module-imports": "^7.28.6",
135
+ "@babel/helper-validator-identifier": "^7.28.5",
136
+ "@babel/traverse": "^7.28.6"
137
+ },
138
+ "engines": {
139
+ "node": ">=6.9.0"
140
+ },
141
+ "peerDependencies": {
142
+ "@babel/core": "^7.0.0"
143
+ }
144
+ },
145
+ "node_modules/@babel/helper-plugin-utils": {
146
+ "version": "7.28.6",
147
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
148
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
149
+ "dev": true,
150
+ "engines": {
151
+ "node": ">=6.9.0"
152
+ }
153
+ },
154
+ "node_modules/@babel/helper-string-parser": {
155
+ "version": "7.27.1",
156
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
157
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
158
+ "dev": true,
159
+ "engines": {
160
+ "node": ">=6.9.0"
161
+ }
162
+ },
163
+ "node_modules/@babel/helper-validator-identifier": {
164
+ "version": "7.28.5",
165
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
166
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
167
+ "dev": true,
168
+ "engines": {
169
+ "node": ">=6.9.0"
170
+ }
171
+ },
172
+ "node_modules/@babel/helper-validator-option": {
173
+ "version": "7.27.1",
174
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
175
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
176
+ "dev": true,
177
+ "engines": {
178
+ "node": ">=6.9.0"
179
+ }
180
+ },
181
+ "node_modules/@babel/helpers": {
182
+ "version": "7.29.2",
183
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
184
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
185
+ "dev": true,
186
+ "dependencies": {
187
+ "@babel/template": "^7.28.6",
188
+ "@babel/types": "^7.29.0"
189
+ },
190
+ "engines": {
191
+ "node": ">=6.9.0"
192
+ }
193
+ },
194
+ "node_modules/@babel/parser": {
195
+ "version": "7.29.3",
196
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
197
+ "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
198
+ "dev": true,
199
+ "dependencies": {
200
+ "@babel/types": "^7.29.0"
201
+ },
202
+ "bin": {
203
+ "parser": "bin/babel-parser.js"
204
+ },
205
+ "engines": {
206
+ "node": ">=6.0.0"
207
+ }
208
+ },
209
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
210
+ "version": "7.27.1",
211
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
212
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
213
+ "dev": true,
214
+ "dependencies": {
215
+ "@babel/helper-plugin-utils": "^7.27.1"
216
+ },
217
+ "engines": {
218
+ "node": ">=6.9.0"
219
+ },
220
+ "peerDependencies": {
221
+ "@babel/core": "^7.0.0-0"
222
+ }
223
+ },
224
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
225
+ "version": "7.27.1",
226
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
227
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
228
+ "dev": true,
229
+ "dependencies": {
230
+ "@babel/helper-plugin-utils": "^7.27.1"
231
+ },
232
+ "engines": {
233
+ "node": ">=6.9.0"
234
+ },
235
+ "peerDependencies": {
236
+ "@babel/core": "^7.0.0-0"
237
+ }
238
+ },
239
+ "node_modules/@babel/template": {
240
+ "version": "7.28.6",
241
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
242
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
243
+ "dev": true,
244
+ "dependencies": {
245
+ "@babel/code-frame": "^7.28.6",
246
+ "@babel/parser": "^7.28.6",
247
+ "@babel/types": "^7.28.6"
248
+ },
249
+ "engines": {
250
+ "node": ">=6.9.0"
251
+ }
252
+ },
253
+ "node_modules/@babel/traverse": {
254
+ "version": "7.29.0",
255
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
256
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
257
+ "dev": true,
258
+ "dependencies": {
259
+ "@babel/code-frame": "^7.29.0",
260
+ "@babel/generator": "^7.29.0",
261
+ "@babel/helper-globals": "^7.28.0",
262
+ "@babel/parser": "^7.29.0",
263
+ "@babel/template": "^7.28.6",
264
+ "@babel/types": "^7.29.0",
265
+ "debug": "^4.3.1"
266
+ },
267
+ "engines": {
268
+ "node": ">=6.9.0"
269
+ }
270
+ },
271
+ "node_modules/@babel/types": {
272
+ "version": "7.29.0",
273
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
274
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
275
+ "dev": true,
276
+ "dependencies": {
277
+ "@babel/helper-string-parser": "^7.27.1",
278
+ "@babel/helper-validator-identifier": "^7.28.5"
279
+ },
280
+ "engines": {
281
+ "node": ">=6.9.0"
282
+ }
283
+ },
284
+ "node_modules/@esbuild/aix-ppc64": {
285
+ "version": "0.21.5",
286
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
287
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
288
+ "cpu": [
289
+ "ppc64"
290
+ ],
291
+ "dev": true,
292
+ "optional": true,
293
+ "os": [
294
+ "aix"
295
+ ],
296
+ "engines": {
297
+ "node": ">=12"
298
+ }
299
+ },
300
+ "node_modules/@esbuild/android-arm": {
301
+ "version": "0.21.5",
302
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
303
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
304
+ "cpu": [
305
+ "arm"
306
+ ],
307
+ "dev": true,
308
+ "optional": true,
309
+ "os": [
310
+ "android"
311
+ ],
312
+ "engines": {
313
+ "node": ">=12"
314
+ }
315
+ },
316
+ "node_modules/@esbuild/android-arm64": {
317
+ "version": "0.21.5",
318
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
319
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
320
+ "cpu": [
321
+ "arm64"
322
+ ],
323
+ "dev": true,
324
+ "optional": true,
325
+ "os": [
326
+ "android"
327
+ ],
328
+ "engines": {
329
+ "node": ">=12"
330
+ }
331
+ },
332
+ "node_modules/@esbuild/android-x64": {
333
+ "version": "0.21.5",
334
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
335
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
336
+ "cpu": [
337
+ "x64"
338
+ ],
339
+ "dev": true,
340
+ "optional": true,
341
+ "os": [
342
+ "android"
343
+ ],
344
+ "engines": {
345
+ "node": ">=12"
346
+ }
347
+ },
348
+ "node_modules/@esbuild/darwin-arm64": {
349
+ "version": "0.21.5",
350
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
351
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
352
+ "cpu": [
353
+ "arm64"
354
+ ],
355
+ "dev": true,
356
+ "optional": true,
357
+ "os": [
358
+ "darwin"
359
+ ],
360
+ "engines": {
361
+ "node": ">=12"
362
+ }
363
+ },
364
+ "node_modules/@esbuild/darwin-x64": {
365
+ "version": "0.21.5",
366
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
367
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
368
+ "cpu": [
369
+ "x64"
370
+ ],
371
+ "dev": true,
372
+ "optional": true,
373
+ "os": [
374
+ "darwin"
375
+ ],
376
+ "engines": {
377
+ "node": ">=12"
378
+ }
379
+ },
380
+ "node_modules/@esbuild/freebsd-arm64": {
381
+ "version": "0.21.5",
382
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
383
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
384
+ "cpu": [
385
+ "arm64"
386
+ ],
387
+ "dev": true,
388
+ "optional": true,
389
+ "os": [
390
+ "freebsd"
391
+ ],
392
+ "engines": {
393
+ "node": ">=12"
394
+ }
395
+ },
396
+ "node_modules/@esbuild/freebsd-x64": {
397
+ "version": "0.21.5",
398
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
399
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
400
+ "cpu": [
401
+ "x64"
402
+ ],
403
+ "dev": true,
404
+ "optional": true,
405
+ "os": [
406
+ "freebsd"
407
+ ],
408
+ "engines": {
409
+ "node": ">=12"
410
+ }
411
+ },
412
+ "node_modules/@esbuild/linux-arm": {
413
+ "version": "0.21.5",
414
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
415
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
416
+ "cpu": [
417
+ "arm"
418
+ ],
419
+ "dev": true,
420
+ "optional": true,
421
+ "os": [
422
+ "linux"
423
+ ],
424
+ "engines": {
425
+ "node": ">=12"
426
+ }
427
+ },
428
+ "node_modules/@esbuild/linux-arm64": {
429
+ "version": "0.21.5",
430
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
431
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
432
+ "cpu": [
433
+ "arm64"
434
+ ],
435
+ "dev": true,
436
+ "optional": true,
437
+ "os": [
438
+ "linux"
439
+ ],
440
+ "engines": {
441
+ "node": ">=12"
442
+ }
443
+ },
444
+ "node_modules/@esbuild/linux-ia32": {
445
+ "version": "0.21.5",
446
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
447
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
448
+ "cpu": [
449
+ "ia32"
450
+ ],
451
+ "dev": true,
452
+ "optional": true,
453
+ "os": [
454
+ "linux"
455
+ ],
456
+ "engines": {
457
+ "node": ">=12"
458
+ }
459
+ },
460
+ "node_modules/@esbuild/linux-loong64": {
461
+ "version": "0.21.5",
462
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
463
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
464
+ "cpu": [
465
+ "loong64"
466
+ ],
467
+ "dev": true,
468
+ "optional": true,
469
+ "os": [
470
+ "linux"
471
+ ],
472
+ "engines": {
473
+ "node": ">=12"
474
+ }
475
+ },
476
+ "node_modules/@esbuild/linux-mips64el": {
477
+ "version": "0.21.5",
478
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
479
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
480
+ "cpu": [
481
+ "mips64el"
482
+ ],
483
+ "dev": true,
484
+ "optional": true,
485
+ "os": [
486
+ "linux"
487
+ ],
488
+ "engines": {
489
+ "node": ">=12"
490
+ }
491
+ },
492
+ "node_modules/@esbuild/linux-ppc64": {
493
+ "version": "0.21.5",
494
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
495
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
496
+ "cpu": [
497
+ "ppc64"
498
+ ],
499
+ "dev": true,
500
+ "optional": true,
501
+ "os": [
502
+ "linux"
503
+ ],
504
+ "engines": {
505
+ "node": ">=12"
506
+ }
507
+ },
508
+ "node_modules/@esbuild/linux-riscv64": {
509
+ "version": "0.21.5",
510
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
511
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
512
+ "cpu": [
513
+ "riscv64"
514
+ ],
515
+ "dev": true,
516
+ "optional": true,
517
+ "os": [
518
+ "linux"
519
+ ],
520
+ "engines": {
521
+ "node": ">=12"
522
+ }
523
+ },
524
+ "node_modules/@esbuild/linux-s390x": {
525
+ "version": "0.21.5",
526
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
527
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
528
+ "cpu": [
529
+ "s390x"
530
+ ],
531
+ "dev": true,
532
+ "optional": true,
533
+ "os": [
534
+ "linux"
535
+ ],
536
+ "engines": {
537
+ "node": ">=12"
538
+ }
539
+ },
540
+ "node_modules/@esbuild/linux-x64": {
541
+ "version": "0.21.5",
542
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
543
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
544
+ "cpu": [
545
+ "x64"
546
+ ],
547
+ "dev": true,
548
+ "optional": true,
549
+ "os": [
550
+ "linux"
551
+ ],
552
+ "engines": {
553
+ "node": ">=12"
554
+ }
555
+ },
556
+ "node_modules/@esbuild/netbsd-x64": {
557
+ "version": "0.21.5",
558
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
559
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
560
+ "cpu": [
561
+ "x64"
562
+ ],
563
+ "dev": true,
564
+ "optional": true,
565
+ "os": [
566
+ "netbsd"
567
+ ],
568
+ "engines": {
569
+ "node": ">=12"
570
+ }
571
+ },
572
+ "node_modules/@esbuild/openbsd-x64": {
573
+ "version": "0.21.5",
574
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
575
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
576
+ "cpu": [
577
+ "x64"
578
+ ],
579
+ "dev": true,
580
+ "optional": true,
581
+ "os": [
582
+ "openbsd"
583
+ ],
584
+ "engines": {
585
+ "node": ">=12"
586
+ }
587
+ },
588
+ "node_modules/@esbuild/sunos-x64": {
589
+ "version": "0.21.5",
590
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
591
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
592
+ "cpu": [
593
+ "x64"
594
+ ],
595
+ "dev": true,
596
+ "optional": true,
597
+ "os": [
598
+ "sunos"
599
+ ],
600
+ "engines": {
601
+ "node": ">=12"
602
+ }
603
+ },
604
+ "node_modules/@esbuild/win32-arm64": {
605
+ "version": "0.21.5",
606
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
607
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
608
+ "cpu": [
609
+ "arm64"
610
+ ],
611
+ "dev": true,
612
+ "optional": true,
613
+ "os": [
614
+ "win32"
615
+ ],
616
+ "engines": {
617
+ "node": ">=12"
618
+ }
619
+ },
620
+ "node_modules/@esbuild/win32-ia32": {
621
+ "version": "0.21.5",
622
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
623
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
624
+ "cpu": [
625
+ "ia32"
626
+ ],
627
+ "dev": true,
628
+ "optional": true,
629
+ "os": [
630
+ "win32"
631
+ ],
632
+ "engines": {
633
+ "node": ">=12"
634
+ }
635
+ },
636
+ "node_modules/@esbuild/win32-x64": {
637
+ "version": "0.21.5",
638
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
639
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
640
+ "cpu": [
641
+ "x64"
642
+ ],
643
+ "dev": true,
644
+ "optional": true,
645
+ "os": [
646
+ "win32"
647
+ ],
648
+ "engines": {
649
+ "node": ">=12"
650
+ }
651
+ },
652
+ "node_modules/@jridgewell/gen-mapping": {
653
+ "version": "0.3.13",
654
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
655
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
656
+ "dev": true,
657
+ "dependencies": {
658
+ "@jridgewell/sourcemap-codec": "^1.5.0",
659
+ "@jridgewell/trace-mapping": "^0.3.24"
660
+ }
661
+ },
662
+ "node_modules/@jridgewell/remapping": {
663
+ "version": "2.3.5",
664
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
665
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
666
+ "dev": true,
667
+ "dependencies": {
668
+ "@jridgewell/gen-mapping": "^0.3.5",
669
+ "@jridgewell/trace-mapping": "^0.3.24"
670
+ }
671
+ },
672
+ "node_modules/@jridgewell/resolve-uri": {
673
+ "version": "3.1.2",
674
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
675
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
676
+ "dev": true,
677
+ "engines": {
678
+ "node": ">=6.0.0"
679
+ }
680
+ },
681
+ "node_modules/@jridgewell/sourcemap-codec": {
682
+ "version": "1.5.5",
683
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
684
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
685
+ "dev": true
686
+ },
687
+ "node_modules/@jridgewell/trace-mapping": {
688
+ "version": "0.3.31",
689
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
690
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
691
+ "dev": true,
692
+ "dependencies": {
693
+ "@jridgewell/resolve-uri": "^3.1.0",
694
+ "@jridgewell/sourcemap-codec": "^1.4.14"
695
+ }
696
+ },
697
+ "node_modules/@rolldown/pluginutils": {
698
+ "version": "1.0.0-beta.27",
699
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
700
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
701
+ "dev": true
702
+ },
703
+ "node_modules/@rollup/rollup-android-arm-eabi": {
704
+ "version": "4.60.4",
705
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
706
+ "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
707
+ "cpu": [
708
+ "arm"
709
+ ],
710
+ "dev": true,
711
+ "optional": true,
712
+ "os": [
713
+ "android"
714
+ ]
715
+ },
716
+ "node_modules/@rollup/rollup-android-arm64": {
717
+ "version": "4.60.4",
718
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
719
+ "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
720
+ "cpu": [
721
+ "arm64"
722
+ ],
723
+ "dev": true,
724
+ "optional": true,
725
+ "os": [
726
+ "android"
727
+ ]
728
+ },
729
+ "node_modules/@rollup/rollup-darwin-arm64": {
730
+ "version": "4.60.4",
731
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
732
+ "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
733
+ "cpu": [
734
+ "arm64"
735
+ ],
736
+ "dev": true,
737
+ "optional": true,
738
+ "os": [
739
+ "darwin"
740
+ ]
741
+ },
742
+ "node_modules/@rollup/rollup-darwin-x64": {
743
+ "version": "4.60.4",
744
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
745
+ "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
746
+ "cpu": [
747
+ "x64"
748
+ ],
749
+ "dev": true,
750
+ "optional": true,
751
+ "os": [
752
+ "darwin"
753
+ ]
754
+ },
755
+ "node_modules/@rollup/rollup-freebsd-arm64": {
756
+ "version": "4.60.4",
757
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
758
+ "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
759
+ "cpu": [
760
+ "arm64"
761
+ ],
762
+ "dev": true,
763
+ "optional": true,
764
+ "os": [
765
+ "freebsd"
766
+ ]
767
+ },
768
+ "node_modules/@rollup/rollup-freebsd-x64": {
769
+ "version": "4.60.4",
770
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
771
+ "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
772
+ "cpu": [
773
+ "x64"
774
+ ],
775
+ "dev": true,
776
+ "optional": true,
777
+ "os": [
778
+ "freebsd"
779
+ ]
780
+ },
781
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
782
+ "version": "4.60.4",
783
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
784
+ "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
785
+ "cpu": [
786
+ "arm"
787
+ ],
788
+ "dev": true,
789
+ "optional": true,
790
+ "os": [
791
+ "linux"
792
+ ]
793
+ },
794
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
795
+ "version": "4.60.4",
796
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
797
+ "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
798
+ "cpu": [
799
+ "arm"
800
+ ],
801
+ "dev": true,
802
+ "optional": true,
803
+ "os": [
804
+ "linux"
805
+ ]
806
+ },
807
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
808
+ "version": "4.60.4",
809
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
810
+ "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
811
+ "cpu": [
812
+ "arm64"
813
+ ],
814
+ "dev": true,
815
+ "optional": true,
816
+ "os": [
817
+ "linux"
818
+ ]
819
+ },
820
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
821
+ "version": "4.60.4",
822
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
823
+ "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
824
+ "cpu": [
825
+ "arm64"
826
+ ],
827
+ "dev": true,
828
+ "optional": true,
829
+ "os": [
830
+ "linux"
831
+ ]
832
+ },
833
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
834
+ "version": "4.60.4",
835
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
836
+ "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
837
+ "cpu": [
838
+ "loong64"
839
+ ],
840
+ "dev": true,
841
+ "optional": true,
842
+ "os": [
843
+ "linux"
844
+ ]
845
+ },
846
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
847
+ "version": "4.60.4",
848
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
849
+ "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
850
+ "cpu": [
851
+ "loong64"
852
+ ],
853
+ "dev": true,
854
+ "optional": true,
855
+ "os": [
856
+ "linux"
857
+ ]
858
+ },
859
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
860
+ "version": "4.60.4",
861
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
862
+ "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
863
+ "cpu": [
864
+ "ppc64"
865
+ ],
866
+ "dev": true,
867
+ "optional": true,
868
+ "os": [
869
+ "linux"
870
+ ]
871
+ },
872
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
873
+ "version": "4.60.4",
874
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
875
+ "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
876
+ "cpu": [
877
+ "ppc64"
878
+ ],
879
+ "dev": true,
880
+ "optional": true,
881
+ "os": [
882
+ "linux"
883
+ ]
884
+ },
885
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
886
+ "version": "4.60.4",
887
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
888
+ "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
889
+ "cpu": [
890
+ "riscv64"
891
+ ],
892
+ "dev": true,
893
+ "optional": true,
894
+ "os": [
895
+ "linux"
896
+ ]
897
+ },
898
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
899
+ "version": "4.60.4",
900
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
901
+ "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
902
+ "cpu": [
903
+ "riscv64"
904
+ ],
905
+ "dev": true,
906
+ "optional": true,
907
+ "os": [
908
+ "linux"
909
+ ]
910
+ },
911
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
912
+ "version": "4.60.4",
913
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
914
+ "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
915
+ "cpu": [
916
+ "s390x"
917
+ ],
918
+ "dev": true,
919
+ "optional": true,
920
+ "os": [
921
+ "linux"
922
+ ]
923
+ },
924
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
925
+ "version": "4.60.4",
926
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
927
+ "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
928
+ "cpu": [
929
+ "x64"
930
+ ],
931
+ "dev": true,
932
+ "optional": true,
933
+ "os": [
934
+ "linux"
935
+ ]
936
+ },
937
+ "node_modules/@rollup/rollup-linux-x64-musl": {
938
+ "version": "4.60.4",
939
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
940
+ "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
941
+ "cpu": [
942
+ "x64"
943
+ ],
944
+ "dev": true,
945
+ "optional": true,
946
+ "os": [
947
+ "linux"
948
+ ]
949
+ },
950
+ "node_modules/@rollup/rollup-openbsd-x64": {
951
+ "version": "4.60.4",
952
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
953
+ "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
954
+ "cpu": [
955
+ "x64"
956
+ ],
957
+ "dev": true,
958
+ "optional": true,
959
+ "os": [
960
+ "openbsd"
961
+ ]
962
+ },
963
+ "node_modules/@rollup/rollup-openharmony-arm64": {
964
+ "version": "4.60.4",
965
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
966
+ "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
967
+ "cpu": [
968
+ "arm64"
969
+ ],
970
+ "dev": true,
971
+ "optional": true,
972
+ "os": [
973
+ "openharmony"
974
+ ]
975
+ },
976
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
977
+ "version": "4.60.4",
978
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
979
+ "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
980
+ "cpu": [
981
+ "arm64"
982
+ ],
983
+ "dev": true,
984
+ "optional": true,
985
+ "os": [
986
+ "win32"
987
+ ]
988
+ },
989
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
990
+ "version": "4.60.4",
991
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
992
+ "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
993
+ "cpu": [
994
+ "ia32"
995
+ ],
996
+ "dev": true,
997
+ "optional": true,
998
+ "os": [
999
+ "win32"
1000
+ ]
1001
+ },
1002
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1003
+ "version": "4.60.4",
1004
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
1005
+ "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
1006
+ "cpu": [
1007
+ "x64"
1008
+ ],
1009
+ "dev": true,
1010
+ "optional": true,
1011
+ "os": [
1012
+ "win32"
1013
+ ]
1014
+ },
1015
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1016
+ "version": "4.60.4",
1017
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
1018
+ "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
1019
+ "cpu": [
1020
+ "x64"
1021
+ ],
1022
+ "dev": true,
1023
+ "optional": true,
1024
+ "os": [
1025
+ "win32"
1026
+ ]
1027
+ },
1028
+ "node_modules/@types/babel__core": {
1029
+ "version": "7.20.5",
1030
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1031
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1032
+ "dev": true,
1033
+ "dependencies": {
1034
+ "@babel/parser": "^7.20.7",
1035
+ "@babel/types": "^7.20.7",
1036
+ "@types/babel__generator": "*",
1037
+ "@types/babel__template": "*",
1038
+ "@types/babel__traverse": "*"
1039
+ }
1040
+ },
1041
+ "node_modules/@types/babel__generator": {
1042
+ "version": "7.27.0",
1043
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1044
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1045
+ "dev": true,
1046
+ "dependencies": {
1047
+ "@babel/types": "^7.0.0"
1048
+ }
1049
+ },
1050
+ "node_modules/@types/babel__template": {
1051
+ "version": "7.4.4",
1052
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1053
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1054
+ "dev": true,
1055
+ "dependencies": {
1056
+ "@babel/parser": "^7.1.0",
1057
+ "@babel/types": "^7.0.0"
1058
+ }
1059
+ },
1060
+ "node_modules/@types/babel__traverse": {
1061
+ "version": "7.28.0",
1062
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1063
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1064
+ "dev": true,
1065
+ "dependencies": {
1066
+ "@babel/types": "^7.28.2"
1067
+ }
1068
+ },
1069
+ "node_modules/@types/estree": {
1070
+ "version": "1.0.8",
1071
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
1072
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1073
+ "dev": true
1074
+ },
1075
+ "node_modules/@vitejs/plugin-react": {
1076
+ "version": "4.7.0",
1077
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
1078
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
1079
+ "dev": true,
1080
+ "dependencies": {
1081
+ "@babel/core": "^7.28.0",
1082
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1083
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1084
+ "@rolldown/pluginutils": "1.0.0-beta.27",
1085
+ "@types/babel__core": "^7.20.5",
1086
+ "react-refresh": "^0.17.0"
1087
+ },
1088
+ "engines": {
1089
+ "node": "^14.18.0 || >=16.0.0"
1090
+ },
1091
+ "peerDependencies": {
1092
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1093
+ }
1094
+ },
1095
+ "node_modules/agent-base": {
1096
+ "version": "6.0.2",
1097
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
1098
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
1099
+ "dependencies": {
1100
+ "debug": "4"
1101
+ },
1102
+ "engines": {
1103
+ "node": ">= 6.0.0"
1104
+ }
1105
+ },
1106
+ "node_modules/asynckit": {
1107
+ "version": "0.4.0",
1108
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
1109
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
1110
+ },
1111
+ "node_modules/attr-accept": {
1112
+ "version": "2.2.5",
1113
+ "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
1114
+ "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
1115
+ "engines": {
1116
+ "node": ">=4"
1117
+ }
1118
+ },
1119
+ "node_modules/axios": {
1120
+ "version": "1.16.1",
1121
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz",
1122
+ "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
1123
+ "dependencies": {
1124
+ "follow-redirects": "^1.16.0",
1125
+ "form-data": "^4.0.5",
1126
+ "https-proxy-agent": "^5.0.1",
1127
+ "proxy-from-env": "^2.1.0"
1128
+ }
1129
+ },
1130
+ "node_modules/baseline-browser-mapping": {
1131
+ "version": "2.10.31",
1132
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz",
1133
+ "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==",
1134
+ "dev": true,
1135
+ "bin": {
1136
+ "baseline-browser-mapping": "dist/cli.cjs"
1137
+ },
1138
+ "engines": {
1139
+ "node": ">=6.0.0"
1140
+ }
1141
+ },
1142
+ "node_modules/browserslist": {
1143
+ "version": "4.28.2",
1144
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
1145
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
1146
+ "dev": true,
1147
+ "funding": [
1148
+ {
1149
+ "type": "opencollective",
1150
+ "url": "https://opencollective.com/browserslist"
1151
+ },
1152
+ {
1153
+ "type": "tidelift",
1154
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1155
+ },
1156
+ {
1157
+ "type": "github",
1158
+ "url": "https://github.com/sponsors/ai"
1159
+ }
1160
+ ],
1161
+ "dependencies": {
1162
+ "baseline-browser-mapping": "^2.10.12",
1163
+ "caniuse-lite": "^1.0.30001782",
1164
+ "electron-to-chromium": "^1.5.328",
1165
+ "node-releases": "^2.0.36",
1166
+ "update-browserslist-db": "^1.2.3"
1167
+ },
1168
+ "bin": {
1169
+ "browserslist": "cli.js"
1170
+ },
1171
+ "engines": {
1172
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1173
+ }
1174
+ },
1175
+ "node_modules/call-bind-apply-helpers": {
1176
+ "version": "1.0.2",
1177
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
1178
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
1179
+ "dependencies": {
1180
+ "es-errors": "^1.3.0",
1181
+ "function-bind": "^1.1.2"
1182
+ },
1183
+ "engines": {
1184
+ "node": ">= 0.4"
1185
+ }
1186
+ },
1187
+ "node_modules/caniuse-lite": {
1188
+ "version": "1.0.30001793",
1189
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
1190
+ "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
1191
+ "dev": true,
1192
+ "funding": [
1193
+ {
1194
+ "type": "opencollective",
1195
+ "url": "https://opencollective.com/browserslist"
1196
+ },
1197
+ {
1198
+ "type": "tidelift",
1199
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1200
+ },
1201
+ {
1202
+ "type": "github",
1203
+ "url": "https://github.com/sponsors/ai"
1204
+ }
1205
+ ]
1206
+ },
1207
+ "node_modules/combined-stream": {
1208
+ "version": "1.0.8",
1209
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
1210
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
1211
+ "dependencies": {
1212
+ "delayed-stream": "~1.0.0"
1213
+ },
1214
+ "engines": {
1215
+ "node": ">= 0.8"
1216
+ }
1217
+ },
1218
+ "node_modules/convert-source-map": {
1219
+ "version": "2.0.0",
1220
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1221
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1222
+ "dev": true
1223
+ },
1224
+ "node_modules/debug": {
1225
+ "version": "4.4.3",
1226
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1227
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1228
+ "dependencies": {
1229
+ "ms": "^2.1.3"
1230
+ },
1231
+ "engines": {
1232
+ "node": ">=6.0"
1233
+ },
1234
+ "peerDependenciesMeta": {
1235
+ "supports-color": {
1236
+ "optional": true
1237
+ }
1238
+ }
1239
+ },
1240
+ "node_modules/delayed-stream": {
1241
+ "version": "1.0.0",
1242
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
1243
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
1244
+ "engines": {
1245
+ "node": ">=0.4.0"
1246
+ }
1247
+ },
1248
+ "node_modules/dunder-proto": {
1249
+ "version": "1.0.1",
1250
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
1251
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
1252
+ "dependencies": {
1253
+ "call-bind-apply-helpers": "^1.0.1",
1254
+ "es-errors": "^1.3.0",
1255
+ "gopd": "^1.2.0"
1256
+ },
1257
+ "engines": {
1258
+ "node": ">= 0.4"
1259
+ }
1260
+ },
1261
+ "node_modules/electron-to-chromium": {
1262
+ "version": "1.5.359",
1263
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.359.tgz",
1264
+ "integrity": "sha512-8lPELWuYZIWk7NDvCNthtmMw/7Q5Wu25NpM4djFMHBmk8DubPAtL4YTOp7ou0e7HyJtwkVlWv8XMLURnrtgJQw==",
1265
+ "dev": true
1266
+ },
1267
+ "node_modules/es-define-property": {
1268
+ "version": "1.0.1",
1269
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
1270
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
1271
+ "engines": {
1272
+ "node": ">= 0.4"
1273
+ }
1274
+ },
1275
+ "node_modules/es-errors": {
1276
+ "version": "1.3.0",
1277
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
1278
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
1279
+ "engines": {
1280
+ "node": ">= 0.4"
1281
+ }
1282
+ },
1283
+ "node_modules/es-object-atoms": {
1284
+ "version": "1.1.1",
1285
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
1286
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
1287
+ "dependencies": {
1288
+ "es-errors": "^1.3.0"
1289
+ },
1290
+ "engines": {
1291
+ "node": ">= 0.4"
1292
+ }
1293
+ },
1294
+ "node_modules/es-set-tostringtag": {
1295
+ "version": "2.1.0",
1296
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
1297
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
1298
+ "dependencies": {
1299
+ "es-errors": "^1.3.0",
1300
+ "get-intrinsic": "^1.2.6",
1301
+ "has-tostringtag": "^1.0.2",
1302
+ "hasown": "^2.0.2"
1303
+ },
1304
+ "engines": {
1305
+ "node": ">= 0.4"
1306
+ }
1307
+ },
1308
+ "node_modules/esbuild": {
1309
+ "version": "0.21.5",
1310
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
1311
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
1312
+ "dev": true,
1313
+ "hasInstallScript": true,
1314
+ "bin": {
1315
+ "esbuild": "bin/esbuild"
1316
+ },
1317
+ "engines": {
1318
+ "node": ">=12"
1319
+ },
1320
+ "optionalDependencies": {
1321
+ "@esbuild/aix-ppc64": "0.21.5",
1322
+ "@esbuild/android-arm": "0.21.5",
1323
+ "@esbuild/android-arm64": "0.21.5",
1324
+ "@esbuild/android-x64": "0.21.5",
1325
+ "@esbuild/darwin-arm64": "0.21.5",
1326
+ "@esbuild/darwin-x64": "0.21.5",
1327
+ "@esbuild/freebsd-arm64": "0.21.5",
1328
+ "@esbuild/freebsd-x64": "0.21.5",
1329
+ "@esbuild/linux-arm": "0.21.5",
1330
+ "@esbuild/linux-arm64": "0.21.5",
1331
+ "@esbuild/linux-ia32": "0.21.5",
1332
+ "@esbuild/linux-loong64": "0.21.5",
1333
+ "@esbuild/linux-mips64el": "0.21.5",
1334
+ "@esbuild/linux-ppc64": "0.21.5",
1335
+ "@esbuild/linux-riscv64": "0.21.5",
1336
+ "@esbuild/linux-s390x": "0.21.5",
1337
+ "@esbuild/linux-x64": "0.21.5",
1338
+ "@esbuild/netbsd-x64": "0.21.5",
1339
+ "@esbuild/openbsd-x64": "0.21.5",
1340
+ "@esbuild/sunos-x64": "0.21.5",
1341
+ "@esbuild/win32-arm64": "0.21.5",
1342
+ "@esbuild/win32-ia32": "0.21.5",
1343
+ "@esbuild/win32-x64": "0.21.5"
1344
+ }
1345
+ },
1346
+ "node_modules/escalade": {
1347
+ "version": "3.2.0",
1348
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1349
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1350
+ "dev": true,
1351
+ "engines": {
1352
+ "node": ">=6"
1353
+ }
1354
+ },
1355
+ "node_modules/file-selector": {
1356
+ "version": "2.1.2",
1357
+ "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
1358
+ "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
1359
+ "dependencies": {
1360
+ "tslib": "^2.7.0"
1361
+ },
1362
+ "engines": {
1363
+ "node": ">= 12"
1364
+ }
1365
+ },
1366
+ "node_modules/follow-redirects": {
1367
+ "version": "1.16.0",
1368
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
1369
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
1370
+ "funding": [
1371
+ {
1372
+ "type": "individual",
1373
+ "url": "https://github.com/sponsors/RubenVerborgh"
1374
+ }
1375
+ ],
1376
+ "engines": {
1377
+ "node": ">=4.0"
1378
+ },
1379
+ "peerDependenciesMeta": {
1380
+ "debug": {
1381
+ "optional": true
1382
+ }
1383
+ }
1384
+ },
1385
+ "node_modules/form-data": {
1386
+ "version": "4.0.5",
1387
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
1388
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
1389
+ "dependencies": {
1390
+ "asynckit": "^0.4.0",
1391
+ "combined-stream": "^1.0.8",
1392
+ "es-set-tostringtag": "^2.1.0",
1393
+ "hasown": "^2.0.2",
1394
+ "mime-types": "^2.1.12"
1395
+ },
1396
+ "engines": {
1397
+ "node": ">= 6"
1398
+ }
1399
+ },
1400
+ "node_modules/fsevents": {
1401
+ "version": "2.3.3",
1402
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1403
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1404
+ "dev": true,
1405
+ "hasInstallScript": true,
1406
+ "optional": true,
1407
+ "os": [
1408
+ "darwin"
1409
+ ],
1410
+ "engines": {
1411
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1412
+ }
1413
+ },
1414
+ "node_modules/function-bind": {
1415
+ "version": "1.1.2",
1416
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1417
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1418
+ "funding": {
1419
+ "url": "https://github.com/sponsors/ljharb"
1420
+ }
1421
+ },
1422
+ "node_modules/gensync": {
1423
+ "version": "1.0.0-beta.2",
1424
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1425
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1426
+ "dev": true,
1427
+ "engines": {
1428
+ "node": ">=6.9.0"
1429
+ }
1430
+ },
1431
+ "node_modules/get-intrinsic": {
1432
+ "version": "1.3.0",
1433
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
1434
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
1435
+ "dependencies": {
1436
+ "call-bind-apply-helpers": "^1.0.2",
1437
+ "es-define-property": "^1.0.1",
1438
+ "es-errors": "^1.3.0",
1439
+ "es-object-atoms": "^1.1.1",
1440
+ "function-bind": "^1.1.2",
1441
+ "get-proto": "^1.0.1",
1442
+ "gopd": "^1.2.0",
1443
+ "has-symbols": "^1.1.0",
1444
+ "hasown": "^2.0.2",
1445
+ "math-intrinsics": "^1.1.0"
1446
+ },
1447
+ "engines": {
1448
+ "node": ">= 0.4"
1449
+ },
1450
+ "funding": {
1451
+ "url": "https://github.com/sponsors/ljharb"
1452
+ }
1453
+ },
1454
+ "node_modules/get-proto": {
1455
+ "version": "1.0.1",
1456
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
1457
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
1458
+ "dependencies": {
1459
+ "dunder-proto": "^1.0.1",
1460
+ "es-object-atoms": "^1.0.0"
1461
+ },
1462
+ "engines": {
1463
+ "node": ">= 0.4"
1464
+ }
1465
+ },
1466
+ "node_modules/gopd": {
1467
+ "version": "1.2.0",
1468
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
1469
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
1470
+ "engines": {
1471
+ "node": ">= 0.4"
1472
+ },
1473
+ "funding": {
1474
+ "url": "https://github.com/sponsors/ljharb"
1475
+ }
1476
+ },
1477
+ "node_modules/has-symbols": {
1478
+ "version": "1.1.0",
1479
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
1480
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
1481
+ "engines": {
1482
+ "node": ">= 0.4"
1483
+ },
1484
+ "funding": {
1485
+ "url": "https://github.com/sponsors/ljharb"
1486
+ }
1487
+ },
1488
+ "node_modules/has-tostringtag": {
1489
+ "version": "1.0.2",
1490
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
1491
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
1492
+ "dependencies": {
1493
+ "has-symbols": "^1.0.3"
1494
+ },
1495
+ "engines": {
1496
+ "node": ">= 0.4"
1497
+ },
1498
+ "funding": {
1499
+ "url": "https://github.com/sponsors/ljharb"
1500
+ }
1501
+ },
1502
+ "node_modules/hasown": {
1503
+ "version": "2.0.3",
1504
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
1505
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
1506
+ "dependencies": {
1507
+ "function-bind": "^1.1.2"
1508
+ },
1509
+ "engines": {
1510
+ "node": ">= 0.4"
1511
+ }
1512
+ },
1513
+ "node_modules/https-proxy-agent": {
1514
+ "version": "5.0.1",
1515
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
1516
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
1517
+ "dependencies": {
1518
+ "agent-base": "6",
1519
+ "debug": "4"
1520
+ },
1521
+ "engines": {
1522
+ "node": ">= 6"
1523
+ }
1524
+ },
1525
+ "node_modules/js-tokens": {
1526
+ "version": "4.0.0",
1527
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1528
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
1529
+ },
1530
+ "node_modules/jsesc": {
1531
+ "version": "3.1.0",
1532
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1533
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1534
+ "dev": true,
1535
+ "bin": {
1536
+ "jsesc": "bin/jsesc"
1537
+ },
1538
+ "engines": {
1539
+ "node": ">=6"
1540
+ }
1541
+ },
1542
+ "node_modules/json5": {
1543
+ "version": "2.2.3",
1544
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1545
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1546
+ "dev": true,
1547
+ "bin": {
1548
+ "json5": "lib/cli.js"
1549
+ },
1550
+ "engines": {
1551
+ "node": ">=6"
1552
+ }
1553
+ },
1554
+ "node_modules/loose-envify": {
1555
+ "version": "1.4.0",
1556
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
1557
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
1558
+ "dependencies": {
1559
+ "js-tokens": "^3.0.0 || ^4.0.0"
1560
+ },
1561
+ "bin": {
1562
+ "loose-envify": "cli.js"
1563
+ }
1564
+ },
1565
+ "node_modules/lru-cache": {
1566
+ "version": "5.1.1",
1567
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1568
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1569
+ "dev": true,
1570
+ "dependencies": {
1571
+ "yallist": "^3.0.2"
1572
+ }
1573
+ },
1574
+ "node_modules/math-intrinsics": {
1575
+ "version": "1.1.0",
1576
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1577
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
1578
+ "engines": {
1579
+ "node": ">= 0.4"
1580
+ }
1581
+ },
1582
+ "node_modules/mime-db": {
1583
+ "version": "1.52.0",
1584
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1585
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1586
+ "engines": {
1587
+ "node": ">= 0.6"
1588
+ }
1589
+ },
1590
+ "node_modules/mime-types": {
1591
+ "version": "2.1.35",
1592
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1593
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1594
+ "dependencies": {
1595
+ "mime-db": "1.52.0"
1596
+ },
1597
+ "engines": {
1598
+ "node": ">= 0.6"
1599
+ }
1600
+ },
1601
+ "node_modules/ms": {
1602
+ "version": "2.1.3",
1603
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1604
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
1605
+ },
1606
+ "node_modules/nanoid": {
1607
+ "version": "3.3.12",
1608
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
1609
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
1610
+ "dev": true,
1611
+ "funding": [
1612
+ {
1613
+ "type": "github",
1614
+ "url": "https://github.com/sponsors/ai"
1615
+ }
1616
+ ],
1617
+ "bin": {
1618
+ "nanoid": "bin/nanoid.cjs"
1619
+ },
1620
+ "engines": {
1621
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1622
+ }
1623
+ },
1624
+ "node_modules/node-releases": {
1625
+ "version": "2.0.44",
1626
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz",
1627
+ "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==",
1628
+ "dev": true
1629
+ },
1630
+ "node_modules/object-assign": {
1631
+ "version": "4.1.1",
1632
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1633
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1634
+ "engines": {
1635
+ "node": ">=0.10.0"
1636
+ }
1637
+ },
1638
+ "node_modules/picocolors": {
1639
+ "version": "1.1.1",
1640
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1641
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1642
+ "dev": true
1643
+ },
1644
+ "node_modules/postcss": {
1645
+ "version": "8.5.14",
1646
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
1647
+ "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
1648
+ "dev": true,
1649
+ "funding": [
1650
+ {
1651
+ "type": "opencollective",
1652
+ "url": "https://opencollective.com/postcss/"
1653
+ },
1654
+ {
1655
+ "type": "tidelift",
1656
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1657
+ },
1658
+ {
1659
+ "type": "github",
1660
+ "url": "https://github.com/sponsors/ai"
1661
+ }
1662
+ ],
1663
+ "dependencies": {
1664
+ "nanoid": "^3.3.11",
1665
+ "picocolors": "^1.1.1",
1666
+ "source-map-js": "^1.2.1"
1667
+ },
1668
+ "engines": {
1669
+ "node": "^10 || ^12 || >=14"
1670
+ }
1671
+ },
1672
+ "node_modules/prop-types": {
1673
+ "version": "15.8.1",
1674
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
1675
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
1676
+ "dependencies": {
1677
+ "loose-envify": "^1.4.0",
1678
+ "object-assign": "^4.1.1",
1679
+ "react-is": "^16.13.1"
1680
+ }
1681
+ },
1682
+ "node_modules/proxy-from-env": {
1683
+ "version": "2.1.0",
1684
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
1685
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
1686
+ "engines": {
1687
+ "node": ">=10"
1688
+ }
1689
+ },
1690
+ "node_modules/react": {
1691
+ "version": "18.3.1",
1692
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1693
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1694
+ "dependencies": {
1695
+ "loose-envify": "^1.1.0"
1696
+ },
1697
+ "engines": {
1698
+ "node": ">=0.10.0"
1699
+ }
1700
+ },
1701
+ "node_modules/react-dom": {
1702
+ "version": "18.3.1",
1703
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
1704
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1705
+ "dependencies": {
1706
+ "loose-envify": "^1.1.0",
1707
+ "scheduler": "^0.23.2"
1708
+ },
1709
+ "peerDependencies": {
1710
+ "react": "^18.3.1"
1711
+ }
1712
+ },
1713
+ "node_modules/react-dropzone": {
1714
+ "version": "14.4.1",
1715
+ "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.4.1.tgz",
1716
+ "integrity": "sha512-QDuV76v3uKbHiH34SpwifZ+gOLi1+RdsCO1kl5vxMT4wW8R82+sthjvBw4th3NHF/XX6FBsqDYZVNN+pnhaw0g==",
1717
+ "dependencies": {
1718
+ "attr-accept": "^2.2.4",
1719
+ "file-selector": "^2.1.0",
1720
+ "prop-types": "^15.8.1"
1721
+ },
1722
+ "engines": {
1723
+ "node": ">= 10.13"
1724
+ },
1725
+ "peerDependencies": {
1726
+ "react": ">= 16.8 || 18.0.0"
1727
+ }
1728
+ },
1729
+ "node_modules/react-is": {
1730
+ "version": "16.13.1",
1731
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
1732
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
1733
+ },
1734
+ "node_modules/react-refresh": {
1735
+ "version": "0.17.0",
1736
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
1737
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
1738
+ "dev": true,
1739
+ "engines": {
1740
+ "node": ">=0.10.0"
1741
+ }
1742
+ },
1743
+ "node_modules/rollup": {
1744
+ "version": "4.60.4",
1745
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
1746
+ "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
1747
+ "dev": true,
1748
+ "dependencies": {
1749
+ "@types/estree": "1.0.8"
1750
+ },
1751
+ "bin": {
1752
+ "rollup": "dist/bin/rollup"
1753
+ },
1754
+ "engines": {
1755
+ "node": ">=18.0.0",
1756
+ "npm": ">=8.0.0"
1757
+ },
1758
+ "optionalDependencies": {
1759
+ "@rollup/rollup-android-arm-eabi": "4.60.4",
1760
+ "@rollup/rollup-android-arm64": "4.60.4",
1761
+ "@rollup/rollup-darwin-arm64": "4.60.4",
1762
+ "@rollup/rollup-darwin-x64": "4.60.4",
1763
+ "@rollup/rollup-freebsd-arm64": "4.60.4",
1764
+ "@rollup/rollup-freebsd-x64": "4.60.4",
1765
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
1766
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.4",
1767
+ "@rollup/rollup-linux-arm64-gnu": "4.60.4",
1768
+ "@rollup/rollup-linux-arm64-musl": "4.60.4",
1769
+ "@rollup/rollup-linux-loong64-gnu": "4.60.4",
1770
+ "@rollup/rollup-linux-loong64-musl": "4.60.4",
1771
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.4",
1772
+ "@rollup/rollup-linux-ppc64-musl": "4.60.4",
1773
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.4",
1774
+ "@rollup/rollup-linux-riscv64-musl": "4.60.4",
1775
+ "@rollup/rollup-linux-s390x-gnu": "4.60.4",
1776
+ "@rollup/rollup-linux-x64-gnu": "4.60.4",
1777
+ "@rollup/rollup-linux-x64-musl": "4.60.4",
1778
+ "@rollup/rollup-openbsd-x64": "4.60.4",
1779
+ "@rollup/rollup-openharmony-arm64": "4.60.4",
1780
+ "@rollup/rollup-win32-arm64-msvc": "4.60.4",
1781
+ "@rollup/rollup-win32-ia32-msvc": "4.60.4",
1782
+ "@rollup/rollup-win32-x64-gnu": "4.60.4",
1783
+ "@rollup/rollup-win32-x64-msvc": "4.60.4",
1784
+ "fsevents": "~2.3.2"
1785
+ }
1786
+ },
1787
+ "node_modules/scheduler": {
1788
+ "version": "0.23.2",
1789
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
1790
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
1791
+ "dependencies": {
1792
+ "loose-envify": "^1.1.0"
1793
+ }
1794
+ },
1795
+ "node_modules/semver": {
1796
+ "version": "6.3.1",
1797
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
1798
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
1799
+ "dev": true,
1800
+ "bin": {
1801
+ "semver": "bin/semver.js"
1802
+ }
1803
+ },
1804
+ "node_modules/source-map-js": {
1805
+ "version": "1.2.1",
1806
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1807
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1808
+ "dev": true,
1809
+ "engines": {
1810
+ "node": ">=0.10.0"
1811
+ }
1812
+ },
1813
+ "node_modules/tslib": {
1814
+ "version": "2.8.1",
1815
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
1816
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
1817
+ },
1818
+ "node_modules/update-browserslist-db": {
1819
+ "version": "1.2.3",
1820
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
1821
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
1822
+ "dev": true,
1823
+ "funding": [
1824
+ {
1825
+ "type": "opencollective",
1826
+ "url": "https://opencollective.com/browserslist"
1827
+ },
1828
+ {
1829
+ "type": "tidelift",
1830
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1831
+ },
1832
+ {
1833
+ "type": "github",
1834
+ "url": "https://github.com/sponsors/ai"
1835
+ }
1836
+ ],
1837
+ "dependencies": {
1838
+ "escalade": "^3.2.0",
1839
+ "picocolors": "^1.1.1"
1840
+ },
1841
+ "bin": {
1842
+ "update-browserslist-db": "cli.js"
1843
+ },
1844
+ "peerDependencies": {
1845
+ "browserslist": ">= 4.21.0"
1846
+ }
1847
+ },
1848
+ "node_modules/vite": {
1849
+ "version": "5.4.21",
1850
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
1851
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
1852
+ "dev": true,
1853
+ "dependencies": {
1854
+ "esbuild": "^0.21.3",
1855
+ "postcss": "^8.4.43",
1856
+ "rollup": "^4.20.0"
1857
+ },
1858
+ "bin": {
1859
+ "vite": "bin/vite.js"
1860
+ },
1861
+ "engines": {
1862
+ "node": "^18.0.0 || >=20.0.0"
1863
+ },
1864
+ "funding": {
1865
+ "url": "https://github.com/vitejs/vite?sponsor=1"
1866
+ },
1867
+ "optionalDependencies": {
1868
+ "fsevents": "~2.3.3"
1869
+ },
1870
+ "peerDependencies": {
1871
+ "@types/node": "^18.0.0 || >=20.0.0",
1872
+ "less": "*",
1873
+ "lightningcss": "^1.21.0",
1874
+ "sass": "*",
1875
+ "sass-embedded": "*",
1876
+ "stylus": "*",
1877
+ "sugarss": "*",
1878
+ "terser": "^5.4.0"
1879
+ },
1880
+ "peerDependenciesMeta": {
1881
+ "@types/node": {
1882
+ "optional": true
1883
+ },
1884
+ "less": {
1885
+ "optional": true
1886
+ },
1887
+ "lightningcss": {
1888
+ "optional": true
1889
+ },
1890
+ "sass": {
1891
+ "optional": true
1892
+ },
1893
+ "sass-embedded": {
1894
+ "optional": true
1895
+ },
1896
+ "stylus": {
1897
+ "optional": true
1898
+ },
1899
+ "sugarss": {
1900
+ "optional": true
1901
+ },
1902
+ "terser": {
1903
+ "optional": true
1904
+ }
1905
+ }
1906
+ },
1907
+ "node_modules/yallist": {
1908
+ "version": "3.1.1",
1909
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
1910
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
1911
+ "dev": true
1912
+ }
1913
+ }
1914
+ }
frontend/package.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "pilotmaster-frontend",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "dependencies": {
11
+ "axios": "^1.6.0",
12
+ "react": "^18.2.0",
13
+ "react-dom": "^18.2.0",
14
+ "react-dropzone": "^14.3.8"
15
+ },
16
+ "devDependencies": {
17
+ "@vitejs/plugin-react": "^4.2.0",
18
+ "vite": "^5.0.0"
19
+ }
20
+ }
frontend/src/App.jsx ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react";
2
+ import { apiRequest, loginRequest } from "./docpilot/api.js";
3
+ import DocPilotDashboard from "./docpilot/pages/Dashboard.jsx";
4
+ import TraceExplorer from "./tracepilot/TraceExplorer.jsx";
5
+
6
+ export default function App() {
7
+ const [auth, setAuth] = useState(false);
8
+ const [loading, setLoading] = useState(true);
9
+ const [screen, setScreen] = useState("login");
10
+ const [username, setUsername] = useState("");
11
+ const [plan, setPlan] = useState("free");
12
+
13
+ useEffect(() => {
14
+ const validate = async () => {
15
+ const token = localStorage.getItem("token");
16
+ if (!token) { setLoading(false); return; }
17
+ try {
18
+ const data = await apiRequest("/auth/me");
19
+ if (data.email) {
20
+ setAuth(true);
21
+ setUsername(data.username);
22
+ setPlan(data.plan);
23
+ setScreen("home");
24
+ } else {
25
+ localStorage.removeItem("token");
26
+ }
27
+ } catch {
28
+ localStorage.removeItem("token");
29
+ }
30
+ setLoading(false);
31
+ };
32
+ validate();
33
+ }, []); // runs once only on mount
34
+
35
+ const logout = () => {
36
+ localStorage.removeItem("token");
37
+ setAuth(false);
38
+ setScreen("login");
39
+ setUsername("");
40
+ };
41
+
42
+ const onLogin = async () => {
43
+ const data = await apiRequest("/auth/me");
44
+ setUsername(data.username);
45
+ setPlan(data.plan);
46
+ setAuth(true);
47
+ setScreen("home");
48
+ };
49
+
50
+ if (loading) return <Splash />;
51
+
52
+ if (!auth) {
53
+ if (screen === "signup") return <Signup goToLogin={() => setScreen("login")} />;
54
+ if (screen === "forgot") return <ForgotPassword goBack={() => setScreen("login")} />;
55
+ return <Login onLogin={onLogin} goToSignup={() => setScreen("signup")} goToForgot={() => setScreen("forgot")} />;
56
+ }
57
+
58
+ if (screen === "docpilot") return (
59
+ <DocPilotDashboard
60
+ onLogout={logout}
61
+ onHome={() => setScreen("home")}
62
+ onTracePilot={() => setScreen("tracepilot")}
63
+ />
64
+ );
65
+
66
+ if (screen === "tracepilot") return (
67
+ <TraceExplorer
68
+ onHome={() => setScreen("home")}
69
+ onDocPilot={() => setScreen("docpilot")}
70
+ />
71
+ );
72
+
73
+ return <PilotMasterHome username={username} plan={plan} onOpen={setScreen} onLogout={logout} />;
74
+ }
75
+
76
+ // ─── HOME ────────────────────────────────────────────────────────────────────
77
+
78
+ function PilotMasterHome({ username, plan, onOpen, onLogout }) {
79
+ const [currentPlan, setCurrentPlan] = useState(plan);
80
+
81
+ const upgradePlan = async () => {
82
+ try {
83
+ const data = await apiRequest("/billing/upgrade", "POST");
84
+ setCurrentPlan(data.plan);
85
+ } catch { alert("Upgrade failed"); }
86
+ };
87
+
88
+ const downgradePlan = async () => {
89
+ try {
90
+ const data = await apiRequest("/billing/downgrade", "POST");
91
+ setCurrentPlan(data.plan);
92
+ } catch { alert("Downgrade failed"); }
93
+ };
94
+ return (
95
+ <div style={{
96
+ background: "#0d0d0d", color: "white", fontFamily: "Arial",
97
+ width: "100vw", height: "100vh", boxSizing: "border-box",
98
+ display: "grid", gridTemplateRows: "auto 1fr auto", overflow: "hidden",
99
+ }}>
100
+ {/* TOP BAR */}
101
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "22px 48px", borderBottom: "1px solid #1a1a1a" }}>
102
+ <div>
103
+ <h1 style={{ margin: 0, fontSize: "34px", fontFamily: "Georgia, serif", fontWeight: "600", letterSpacing: "-1.5px", color: "white" }}>PilotMaster</h1>
104
+ <p style={{ margin: "3px 0 0", fontSize: "12px", color: "#3a3a3a", letterSpacing: "0.05em" }}>observable AI execution ecosystem</p>
105
+ </div>
106
+ <div style={{ display: "flex", alignItems: "center", gap: "20px" }}>
107
+ <div style={{ textAlign: "right" }}>
108
+ <p style={{ margin: 0, fontSize: "14px", color: "#aaa" }}>{username}</p>
109
+ <p style={{ margin: "2px 0 0", fontSize: "11px", color: "#444", textTransform: "uppercase", letterSpacing: "0.08em" }}>{currentPlan}</p>
110
+ </div>
111
+ {currentPlan === "free" ? (
112
+ <button onClick={upgradePlan} style={{ ...btnStyle, color: "#4caf50", borderColor: "#4caf5030" }}>Upgrade to Pro</button>
113
+ ) : (
114
+ <button onClick={downgradePlan} style={{ ...btnStyle, color: "#888" }}>Downgrade</button>
115
+ )}
116
+ <button onClick={onLogout} style={btnStyle}>Logout</button>
117
+ </div>
118
+ </div>
119
+
120
+ {/* CENTER */}
121
+ <div style={{ display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", gap: "28px" }}>
122
+ <p style={{ margin: 0, fontSize: "11px", color: "#333", letterSpacing: "0.12em", textTransform: "uppercase" }}>select a workspace</p>
123
+ <div style={{ display: "flex", gap: "20px" }}>
124
+ <ProductCard
125
+ name="DocPilot"
126
+ description="Upload documents. Chat with them. Manage your knowledge base."
127
+ tags={["RAG", "Chat", "Documents", "Auth"]}
128
+ onClick={() => onOpen("docpilot")}
129
+ accent="#4caf50"
130
+ />
131
+ <ProductCard
132
+ name="TracePilot"
133
+ description="Observe every execution. Inspect traces, chunks, evaluation scores and spans."
134
+ tags={["Traces", "Evaluation", "Observability", "Replay"]}
135
+ onClick={() => onOpen("tracepilot")}
136
+ accent="#7c4dff"
137
+ />
138
+ </div>
139
+ </div>
140
+
141
+ {/* FOOTER */}
142
+ <div style={{ padding: "14px 48px", borderTop: "1px solid #161616", display: "flex", justifyContent: "space-between" }}>
143
+ <p style={{ margin: 0, fontSize: "11px", color: "#222" }}>PilotMaster · execution kernel: PilotCore</p>
144
+ <p style={{ margin: 0, fontSize: "11px", color: "#222" }}>llama-3.1-8b-instant · all-mpnet-base-v2</p>
145
+ </div>
146
+ </div>
147
+ );
148
+ }
149
+
150
+ function ProductCard({ name, description, tags, onClick, accent }) {
151
+ const [hovered, setHovered] = useState(false);
152
+ return (
153
+ <div
154
+ onClick={onClick}
155
+ onMouseEnter={() => setHovered(true)}
156
+ onMouseLeave={() => setHovered(false)}
157
+ style={{
158
+ width: "320px", padding: "28px", borderRadius: "14px", cursor: "pointer",
159
+ background: hovered ? "#141414" : "#111",
160
+ border: `1px solid ${hovered ? "#2a2a2a" : "#1a1a1a"}`,
161
+ transition: "all 0.15s ease",
162
+ display: "flex", flexDirection: "column", gap: "14px",
163
+ boxSizing: "border-box",
164
+ }}
165
+ >
166
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
167
+ <h2 style={{ margin: 0, fontSize: "26px", fontFamily: "Georgia, serif", fontWeight: "600", letterSpacing: "-1px", color: "white" }}>{name}</h2>
168
+ <span style={{ fontSize: "18px", color: hovered ? accent : "#2a2a2a", transition: "color 0.15s" }}>→</span>
169
+ </div>
170
+ <p style={{ margin: 0, fontSize: "13px", color: "#555", lineHeight: 1.6 }}>{description}</p>
171
+ <div style={{ display: "flex", gap: "6px", flexWrap: "wrap" }}>
172
+ {tags.map(tag => (
173
+ <span key={tag} style={{
174
+ fontSize: "10px", padding: "3px 8px", borderRadius: "4px",
175
+ background: accent + "15", color: accent, border: `1px solid ${accent}25`,
176
+ letterSpacing: "0.05em", textTransform: "uppercase",
177
+ }}>{tag}</span>
178
+ ))}
179
+ </div>
180
+ </div>
181
+ );
182
+ }
183
+
184
+ // ─── AUTH ────────────────────────────────────────────────────────────────────
185
+
186
+ function Login({ onLogin, goToSignup, goToForgot }) {
187
+ const [email, setEmail] = useState("");
188
+ const [password, setPassword] = useState("");
189
+
190
+ const login = async () => {
191
+ try {
192
+ const data = await loginRequest(email, password);
193
+ if (!data.access_token) { alert("Invalid credentials"); return; }
194
+ localStorage.setItem("token", data.access_token);
195
+ onLogin();
196
+ } catch { alert("Wrong email or password"); }
197
+ };
198
+
199
+ return (
200
+ <AuthShell>
201
+ <h1 style={authTitleStyle}>PilotMaster</h1>
202
+ <p style={{ margin: "0 0 36px", color: "#3a3a3a", fontSize: "14px", textAlign: "center" }}>observable AI execution ecosystem</p>
203
+ <input placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} style={inputStyle} />
204
+ <input type="password" placeholder="Password" value={password} onChange={e => setPassword(e.target.value)}
205
+ onKeyDown={e => e.key === "Enter" && login()} style={inputStyle} />
206
+ <button onClick={login} style={primaryBtnStyle}>Login</button>
207
+ <p onClick={goToSignup} style={linkStyle}>Don't have an account? Sign up</p>
208
+ <p onClick={goToForgot} style={{ ...linkStyle, color: "#3a3a3a", marginTop: "10px" }}>Forgot password?</p>
209
+ </AuthShell>
210
+ );
211
+ }
212
+
213
+ function Signup({ goToLogin }) {
214
+ const [username, setUsername] = useState("");
215
+ const [email, setEmail] = useState("");
216
+ const [password, setPassword] = useState("");
217
+
218
+ const signup = async () => {
219
+ try {
220
+ await apiRequest("/auth/signup", "POST", { username, email, password });
221
+ alert("Account created. Please login.");
222
+ goToLogin();
223
+ } catch { alert("Signup failed"); }
224
+ };
225
+
226
+ return (
227
+ <AuthShell>
228
+ <h1 style={authTitleStyle}>PilotMaster</h1>
229
+ <p style={{ margin: "0 0 36px", color: "#3a3a3a", fontSize: "14px", textAlign: "center" }}>create your account</p>
230
+ <input placeholder="Username" value={username} onChange={e => setUsername(e.target.value)} style={inputStyle} />
231
+ <input placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} style={inputStyle} />
232
+ <input type="password" placeholder="Password" value={password} onChange={e => setPassword(e.target.value)} style={inputStyle} />
233
+ <button onClick={signup} style={primaryBtnStyle}>Sign Up</button>
234
+ <p onClick={goToLogin} style={linkStyle}>Already have an account? Login</p>
235
+ </AuthShell>
236
+ );
237
+ }
238
+
239
+ function ForgotPassword({ goBack }) {
240
+ const [email, setEmail] = useState("");
241
+ const [token, setToken] = useState("");
242
+ const [newPassword, setNewPassword] = useState("");
243
+ const [generatedToken, setGeneratedToken] = useState("");
244
+
245
+ return (
246
+ <AuthShell>
247
+ <h1 style={{ ...authTitleStyle, fontSize: "42px", marginBottom: "32px" }}>Reset Password</h1>
248
+ <input placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} style={inputStyle} />
249
+ <button onClick={async () => {
250
+ try { const d = await apiRequest("/auth/forgot-password", "POST", { email }); setGeneratedToken(d.reset_token); }
251
+ catch { alert("Email not found"); }
252
+ }} style={primaryBtnStyle}>Generate Reset Token</button>
253
+ {generatedToken && (
254
+ <div style={{ background: "#141414", border: "1px solid #222", borderRadius: "10px", padding: "12px", fontSize: "12px", color: "#666", wordBreak: "break-all", marginBottom: "14px" }}>
255
+ Token: {generatedToken}
256
+ </div>
257
+ )}
258
+ <input placeholder="Paste Token" value={token} onChange={e => setToken(e.target.value)} style={inputStyle} />
259
+ <input type="password" placeholder="New Password" value={newPassword} onChange={e => setNewPassword(e.target.value)} style={inputStyle} />
260
+ <button onClick={async () => {
261
+ try { await apiRequest("/auth/reset-password", "POST", { token, new_password: newPassword }); alert("Password reset"); goBack(); }
262
+ catch { alert("Invalid token"); }
263
+ }} style={primaryBtnStyle}>Reset Password</button>
264
+ <p onClick={goBack} style={linkStyle}>Back to Login</p>
265
+ </AuthShell>
266
+ );
267
+ }
268
+
269
+ function AuthShell({ children }) {
270
+ return (
271
+ <div style={{ background: "#0d0d0d", width: "100vw", height: "100vh", display: "flex", justifyContent: "center", alignItems: "center", fontFamily: "Arial", boxSizing: "border-box", overflow: "hidden" }}>
272
+ <div style={{ width: "500px", display: "flex", flexDirection: "column" }}>
273
+ {children}
274
+ </div>
275
+ </div>
276
+ );
277
+ }
278
+
279
+ function Splash() {
280
+ return (
281
+ <div style={{ background: "#0d0d0d", color: "#2a2a2a", width: "100vw", height: "100vh", display: "flex", justifyContent: "center", alignItems: "center", fontFamily: "Arial", fontSize: "14px" }}>
282
+ Loading...
283
+ </div>
284
+ );
285
+ }
286
+
287
+ // ─── STYLES ──────────────────────────────────────────────────────────────────
288
+
289
+ const inputStyle = {
290
+ width: "100%", padding: "20px 22px", marginBottom: "16px", borderRadius: "14px",
291
+ border: "1px solid #1e1e1e", background: "#141414", color: "white",
292
+ fontSize: "17px", outline: "none", boxSizing: "border-box",
293
+ };
294
+
295
+ const primaryBtnStyle = {
296
+ width: "100%", padding: "20px", borderRadius: "14px", border: "1px solid #2a2a2a",
297
+ background: "#1a1a1a", color: "white", fontSize: "17px", cursor: "pointer",
298
+ fontWeight: "600", marginBottom: "8px", boxSizing: "border-box",
299
+ };
300
+
301
+ const authTitleStyle = {
302
+ margin: "0 0 8px", fontSize: "64px", fontFamily: "Georgia, serif",
303
+ fontWeight: "600", letterSpacing: "-3px", color: "white", textAlign: "center", lineHeight: 1,
304
+ };
305
+
306
+ const linkStyle = {
307
+ margin: "14px 0 0", color: "#555", textAlign: "center", cursor: "pointer", fontSize: "15px",
308
+ };
309
+
310
+ const btnStyle = {
311
+ padding: "10px 20px", background: "#141414", color: "#888",
312
+ border: "1px solid #222", borderRadius: "10px", cursor: "pointer", fontSize: "13px",
313
+ };
frontend/src/docpilot/App.jsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ // Auth is handled at PilotMaster root (App.jsx)
2
+ // This file kept for compatibility but is no longer the entry point
3
+ export { default } from "./pages/Dashboard.jsx";
frontend/src/docpilot/api.js ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const API_BASE = "http://127.0.0.1:8000/docpilot";
2
+
3
+ export const apiRequest = async (endpoint, method = "GET", body = null) => {
4
+ const token = localStorage.getItem("token");
5
+ const headers = {};
6
+
7
+ if (!(body instanceof FormData)) {
8
+ headers["Content-Type"] = "application/json";
9
+ }
10
+ if (token) {
11
+ headers["Authorization"] = `Bearer ${token}`;
12
+ }
13
+
14
+ const response = await fetch(API_BASE + endpoint, {
15
+ method,
16
+ headers,
17
+ body: body instanceof FormData ? body : body ? JSON.stringify(body) : null,
18
+ });
19
+
20
+ return response.json();
21
+ };
22
+
23
+ export const loginRequest = async (email, password) => {
24
+ const formData = new URLSearchParams();
25
+ formData.append("username", email);
26
+ formData.append("password", password);
27
+
28
+ const response = await fetch(API_BASE + "/auth/login", {
29
+ method: "POST",
30
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
31
+ body: formData,
32
+ });
33
+
34
+ return response.json();
35
+ };
frontend/src/docpilot/pages/Dashboard.jsx ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useRef } from "react";
2
+ import { apiRequest } from "../api";
3
+ import { useDropzone } from "react-dropzone";
4
+
5
+ function Dashboard({ onLogout, onHome, onTracePilot }) {
6
+ const [file, setFile] = useState(null);
7
+ const [question, setQuestion] = useState("");
8
+ const [source, setSource] = useState("");
9
+ const [messages, setMessages] = useState([]);
10
+ const [sessions, setSessions] = useState([]);
11
+ const [currentSessionId, setCurrentSessionId] = useState(null);
12
+ const [username, setUsername] = useState("");
13
+ const [uploading, setUploading] = useState(false);
14
+ const [asking, setAsking] = useState(false);
15
+ const messagesEndRef = useRef(null);
16
+
17
+ useEffect(() => {
18
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
19
+ }, [messages]);
20
+
21
+ useEffect(() => {
22
+ apiRequest("/history/sessions").then(setSessions).catch(() => {});
23
+ apiRequest("/billing/me").then(d => setUsername(d.username)).catch(() => {});
24
+ }, []);
25
+
26
+ const fetchSessions = () =>
27
+ apiRequest("/history/sessions").then(setSessions).catch(() => {});
28
+
29
+ const loadSession = async (sessionId) => {
30
+ const data = await apiRequest(`/history/${sessionId}`);
31
+ // Align keys directly with your backend metrics payload structure
32
+ setMessages(data.map(m => ({
33
+ role: m.role,
34
+ content: m.content,
35
+ sources: m.sources,
36
+ timestamp: m.timestamp || m.created_at || new Date().toISOString()
37
+ })));
38
+ setCurrentSessionId(sessionId);
39
+ };
40
+
41
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
42
+ onDrop: (files) => files.length > 0 && setFile(files[0]),
43
+ accept: {
44
+ "application/pdf": [".pdf"],
45
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"],
46
+ "text/plain": [".txt"],
47
+ "text/markdown": [".md"],
48
+ "text/csv": [".csv"],
49
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"],
50
+ "image/png": [".png"],
51
+ "image/jpeg": [".jpg", ".jpeg"],
52
+ },
53
+ });
54
+
55
+ const uploadFile = async () => {
56
+ if (!file) return;
57
+ setUploading(true);
58
+ const formData = new FormData();
59
+ formData.append("file", file);
60
+ try {
61
+ const data = await apiRequest("/docs/upload", "POST", formData);
62
+ if (data.detail) { alert(data.detail); }
63
+ else { alert(data.message); setSource(file.name); }
64
+ } catch { alert("Upload failed"); }
65
+ setUploading(false);
66
+ };
67
+
68
+ const askQuestion = async () => {
69
+ if (!question) return;
70
+ const q = question;
71
+ setQuestion("");
72
+ setMessages(prev => [
73
+ ...prev,
74
+ { role: "user", content: q, timestamp: new Date().toISOString() },
75
+ { role: "assistant", content: "Thinking...", loading: true, timestamp: new Date().toISOString() }
76
+ ]);
77
+ setAsking(true);
78
+ try {
79
+ const data = await apiRequest("/chat/ask", "POST", { question: q, source, session_id: currentSessionId });
80
+ if (data.session_id) { setCurrentSessionId(data.session_id); fetchSessions(); }
81
+ setMessages(prev => {
82
+ const u = [...prev];
83
+ u[u.length - 1] = {
84
+ role: "assistant",
85
+ content: data.answer,
86
+ sources: data.sources,
87
+ timestamp: new Date().toISOString()
88
+ };
89
+ return u;
90
+ });
91
+ } catch {
92
+ setMessages(prev => {
93
+ const u = [...prev];
94
+ u[u.length - 1] = {
95
+ role: "assistant",
96
+ content: "Something went wrong.",
97
+ timestamp: new Date().toISOString()
98
+ };
99
+ return u;
100
+ });
101
+ }
102
+ setAsking(false);
103
+ };
104
+
105
+ const resetMemory = async () => {
106
+ await apiRequest("/docs/reset", "DELETE");
107
+ setMessages([]); setQuestion(""); setSource(""); setFile(null); setCurrentSessionId(null);
108
+ };
109
+
110
+ return (
111
+ <div style={{ display: "flex", width: "100%", height: "100%", background: "#111", color: "white", fontFamily: "Arial", overflow: "hidden" }}>
112
+
113
+ {/* SIDEBAR */}
114
+ <div style={{ width: "280px", flexShrink: 0, background: "#0d0d0d", borderRight: "1px solid #1e1e1e", display: "flex", flexDirection: "column", overflow: "hidden" }}>
115
+
116
+ {/* SIDEBAR HEADER */}
117
+ <div style={{ padding: "24px 24px 16px", flexShrink: 0 }}>
118
+ <h1 style={{ margin: 0, fontSize: "44px", fontFamily: "Georgia, serif", fontWeight: "600", letterSpacing: "-2px", color: "white", lineHeight: 1 }}>DocPilot</h1>
119
+ <p style={{ margin: "8px 0 0", color: "#555", fontSize: "14px" }}>{username}</p>
120
+ </div>
121
+
122
+ {/* UPLOAD */}
123
+ <div style={{ padding: "0 16px 16px", flexShrink: 0, borderBottom: "1px solid #1a1a1a" }}>
124
+ <div {...getRootProps()} style={{
125
+ border: "1px dashed #2a2a2a", borderRadius: "12px", padding: "20px 16px",
126
+ textAlign: "center", background: isDragActive ? "#1a1a1a" : "#141414",
127
+ cursor: "pointer", color: "#555", fontSize: "13px", marginBottom: "10px",
128
+ }}>
129
+ <input {...getInputProps()} />
130
+ {isDragActive ? <p style={{ margin: 0 }}>Drop here...</p> : <p style={{ margin: 0 }}>Drag & drop or click to upload</p>}
131
+ {file && <p style={{ margin: "8px 0 0", color: "#888", fontSize: "12px" }}>{file.name}</p>}
132
+ </div>
133
+ <button onClick={uploadFile} disabled={uploading} style={{
134
+ width: "100%", padding: "11px", background: "#1a1a1a", color: uploading ? "#555" : "white",
135
+ border: "1px solid #2a2a2a", borderRadius: "10px", cursor: uploading ? "not-allowed" : "pointer", fontSize: "14px",
136
+ }}>
137
+ {uploading ? "Uploading..." : "Upload"}
138
+ </button>
139
+ </div>
140
+
141
+ {/* NEW CHAT */}
142
+ <div style={{ padding: "12px 16px", flexShrink: 0 }}>
143
+ <button onClick={() => { setMessages([]); setCurrentSessionId(null); }} style={{
144
+ width: "100%", padding: "12px", background: "#161616", color: "white",
145
+ border: "1px solid #222", borderRadius: "10px", cursor: "pointer", fontSize: "14px",
146
+ }}>+ New Chat</button>
147
+ </div>
148
+
149
+ {/* SESSIONS */}
150
+ <div style={{ flex: 1, overflowY: "auto", padding: "0 16px 16px" }}>
151
+ <p style={{ margin: "0 0 10px", fontSize: "11px", color: "#333", textTransform: "uppercase", letterSpacing: "0.08em" }}>Conversations</p>
152
+ {sessions.map(session => (
153
+ <div key={session.id} onClick={() => loadSession(session.id)} style={{
154
+ padding: "12px 14px", marginBottom: "6px", borderRadius: "10px",
155
+ background: currentSessionId === session.id ? "#1e1e1e" : "#141414",
156
+ border: "1px solid #1e1e1e", cursor: "pointer", fontSize: "13px",
157
+ display: "flex", justifyContent: "space-between", alignItems: "center",
158
+ }}>
159
+ <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1, color: "#ccc" }}>
160
+ {session.title || `Chat #${session.id}`}
161
+ </span>
162
+ <button onClick={async (e) => {
163
+ e.stopPropagation();
164
+ await apiRequest(`/history/${session.id}`, "DELETE");
165
+ if (currentSessionId === session.id) { setMessages([]); setCurrentSessionId(null); }
166
+ fetchSessions();
167
+ }} style={{ background: "transparent", border: "none", color: "#444", cursor: "pointer", fontSize: "14px", marginLeft: "8px", flexShrink: 0 }}>✕</button>
168
+ </div>
169
+ ))}
170
+ </div>
171
+ </div>
172
+
173
+ {/* MAIN */}
174
+ <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
175
+
176
+ {/* THIN HEADER */}
177
+ <div style={{ padding: "12px 24px", borderBottom: "1px solid #1e1e1e", flexShrink: 0, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
178
+ <p style={{ margin: 0, fontSize: "13px", color: "#555" }}>
179
+ Active Document: <span style={{ color: source ? "#aaa" : "#333" }}>{source || "None"}</span>
180
+ </p>
181
+ <div style={{ display: "flex", gap: "8px" }}>
182
+ {[
183
+ { label: "← Home", onClick: onHome, color: "#aaa" },
184
+ { label: "TracePilot →", onClick: onTracePilot, color: "#7c4dff" },
185
+ { label: "Reset", onClick: resetMemory, color: "#aaa" },
186
+ { label: "Logout", onClick: onLogout, color: "#aaa" },
187
+ ].map(btn => (
188
+ <button key={btn.label} onClick={btn.onClick} style={{
189
+ padding: "8px 14px", background: "#161616", color: btn.color,
190
+ border: "1px solid #222", borderRadius: "8px", cursor: "pointer", fontSize: "13px",
191
+ }}>{btn.label}</button>
192
+ ))}
193
+ </div>
194
+ </div>{/* CHAT AREA */}
195
+ <div style={{ flex: 1, overflowY: "auto", padding: "32px 60px" }}>
196
+ {messages.length === 0 && (
197
+ <p style={{ color: "#333", fontSize: "16px" }}>Ask questions about your document...</p>
198
+ )}
199
+ {messages
200
+ .sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp))
201
+ .map((msg, i) => (
202
+ <div key={i} style={{ marginBottom: "28px", display: "flex", justifyContent: msg.role === "user" ? "flex-end" : "flex-start" }}>
203
+ {msg.role === "user" ? (
204
+ <div style={{ background: "#1e1e1e", padding: "14px 20px", borderRadius: "18px", maxWidth: "65%", fontSize: "16px", lineHeight: 1.6, color: "#eee" }}>
205
+ {msg.content}
206
+ </div>
207
+ ) : (
208
+ <div style={{ maxWidth: "80%", fontSize: "16px", lineHeight: 1.8, color: "#ccc" }}>
209
+
210
+ {/* REMOVED CONTAINER BUBBLE STYLE HERE — JUST RAW TEXT CONTENT */}
211
+ <div>
212
+ {msg.content}
213
+ </div>
214
+
215
+ {/* Sources section remains nicely padded right beneath the unbubbled text */}
216
+ {msg.sources && msg.sources.length > 0 && (
217
+ <div style={{ marginTop: "16px", paddingLeft: "4px", fontSize: "12px", color: "#444" }}>
218
+ <p style={{ margin: "0 0 6px", textTransform: "uppercase", letterSpacing: "0.06em", fontSize: "10px", color: "#555", fontWeight: "bold" }}>Sources</p>
219
+ {msg.sources.map((s, idx) => (
220
+ <div key={idx} style={{ color: "#666", marginBottom: "2px" }}>
221
+ 📁 <span style={{ color: "#888" }}>{s.source || s.file_name}</span> · <span style={{ fontStyle: "italic" }}>Page {s.page || s.page_number}</span>
222
+ </div>
223
+ ))}
224
+ </div>
225
+ )}
226
+ </div>
227
+ )}
228
+ </div>
229
+ ))}
230
+ <div ref={messagesEndRef} />
231
+ </div>
232
+
233
+ {/* INPUT */}
234
+ <div style={{ padding: "16px 24px", borderTop: "1px solid #1e1e1e", flexShrink: 0, background: "#111" }}>
235
+ <div style={{ display: "flex", gap: "12px" }}>
236
+ <input
237
+ type="text"
238
+ placeholder="Ask something..."
239
+ value={question}
240
+ onChange={e => setQuestion(e.target.value)}
241
+ onKeyDown={e => e.key === "Enter" && askQuestion()}
242
+ style={{
243
+ flex: 1, padding: "16px 20px", borderRadius: "14px",
244
+ border: "1px solid #222", background: "#161616", color: "white",
245
+ fontSize: "15px", outline: "none",
246
+ }}
247
+ />
248
+ <button onClick={askQuestion} disabled={asking} style={{
249
+ padding: "16px 28px", borderRadius: "14px", border: "1px solid #222",
250
+ background: "#1e1e1e", color: asking ? "#555" : "white",
251
+ cursor: asking ? "not-allowed" : "pointer", fontSize: "15px",
252
+ }}>Send</button>
253
+ </div>
254
+ </div>
255
+ </div>
256
+ </div>
257
+ );
258
+ }
259
+
260
+ export default Dashboard;
frontend/src/docpilot/pages/ForgotPassword.jsx ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+
3
+ import { apiRequest } from "../api";
4
+
5
+ function ForgotPassword({
6
+ goBack,
7
+ }) {
8
+ const [email,
9
+ setEmail] =
10
+ useState("");
11
+
12
+ const [token,
13
+ setToken] =
14
+ useState("");
15
+
16
+ const [newPassword,
17
+ setNewPassword] =
18
+ useState("");
19
+
20
+ const [generatedToken,
21
+ setGeneratedToken] =
22
+ useState("");
23
+
24
+ const requestReset =
25
+ async () => {
26
+
27
+ try {
28
+
29
+ const data =
30
+ await apiRequest(
31
+ "/auth/forgot-password",
32
+ "POST",
33
+ {
34
+ email,
35
+ }
36
+ );
37
+
38
+ setGeneratedToken(
39
+ data.reset_token
40
+ );
41
+
42
+ } catch (error) {
43
+
44
+ console.error(error);
45
+
46
+ alert(
47
+ "Email not found"
48
+ );
49
+ }
50
+ };
51
+
52
+ const resetPassword =
53
+ async () => {
54
+
55
+ try {
56
+
57
+ await apiRequest(
58
+ "/auth/reset-password",
59
+ "POST",
60
+ {
61
+ token,
62
+ new_password:
63
+ newPassword,
64
+ }
65
+ );
66
+
67
+ alert(
68
+ "Password reset successful"
69
+ );
70
+
71
+ goBack();
72
+
73
+ } catch (error) {
74
+
75
+ console.error(error);
76
+
77
+ alert(
78
+ "Invalid token"
79
+ );
80
+ }
81
+ };
82
+
83
+ return (
84
+ <div
85
+ style={{
86
+ width: "100vw",
87
+ height: "100vh",
88
+
89
+ background:
90
+ "#111",
91
+
92
+ display: "flex",
93
+
94
+ justifyContent:
95
+ "center",
96
+
97
+ alignItems:
98
+ "center",
99
+ }}
100
+ >
101
+ <div
102
+ style={{
103
+ width: "420px",
104
+ }}
105
+ >
106
+ <h1
107
+ style={{
108
+ fontSize:
109
+ "64px",
110
+
111
+ fontWeight:
112
+ "900",
113
+
114
+ color:
115
+ "white",
116
+
117
+ marginBottom:
118
+ "40px",
119
+ }}
120
+ >
121
+ Reset
122
+ </h1>
123
+
124
+ <input
125
+ placeholder="Email"
126
+
127
+ value={email}
128
+
129
+ onChange={(e) =>
130
+ setEmail(
131
+ e.target.value
132
+ )
133
+ }
134
+
135
+ style={inputStyle}
136
+ />
137
+
138
+ <button
139
+ onClick={
140
+ requestReset
141
+ }
142
+
143
+ style={buttonStyle}
144
+ >
145
+ Generate Reset Token
146
+ </button>
147
+
148
+ {generatedToken && (
149
+ <div
150
+ style={{
151
+ color:
152
+ "#aaa",
153
+
154
+ marginTop:
155
+ "20px",
156
+
157
+ wordBreak:
158
+ "break-all",
159
+ }}
160
+ >
161
+ Reset Token:
162
+ <br />
163
+ {
164
+ generatedToken
165
+ }
166
+ </div>
167
+ )}
168
+
169
+ <input
170
+ placeholder="Paste Token"
171
+
172
+ value={token}
173
+
174
+ onChange={(e) =>
175
+ setToken(
176
+ e.target.value
177
+ )
178
+ }
179
+
180
+ style={{
181
+ ...inputStyle,
182
+ marginTop:
183
+ "20px",
184
+ }}
185
+ />
186
+
187
+ <input
188
+ type="password"
189
+
190
+ placeholder="New Password"
191
+
192
+ value={newPassword}
193
+
194
+ onChange={(e) =>
195
+ setNewPassword(
196
+ e.target.value
197
+ )
198
+ }
199
+
200
+ style={inputStyle}
201
+ />
202
+
203
+ <button
204
+ onClick={
205
+ resetPassword
206
+ }
207
+
208
+ style={buttonStyle}
209
+ >
210
+ Reset Password
211
+ </button>
212
+
213
+ <p
214
+ onClick={goBack}
215
+
216
+ style={{
217
+ marginTop:
218
+ "20px",
219
+
220
+ color:
221
+ "#777",
222
+
223
+ cursor:
224
+ "pointer",
225
+
226
+ textAlign:
227
+ "center",
228
+ }}
229
+ >
230
+ Back to Login
231
+ </p>
232
+ </div>
233
+ </div>
234
+ );
235
+ }
236
+
237
+ const inputStyle = {
238
+ width: "100%",
239
+
240
+ padding: "18px",
241
+
242
+ marginBottom: "16px",
243
+
244
+ borderRadius: "14px",
245
+
246
+ border: "1px solid #333",
247
+
248
+ background: "#1d1d1d",
249
+
250
+ color: "white",
251
+
252
+ fontSize: "16px",
253
+
254
+ outline: "none",
255
+ };
256
+
257
+ const buttonStyle = {
258
+ width: "100%",
259
+
260
+ padding: "18px",
261
+
262
+ borderRadius: "14px",
263
+
264
+ border: "1px solid #333",
265
+
266
+ background: "#2a2a2a",
267
+
268
+ color: "white",
269
+
270
+ fontSize: "16px",
271
+
272
+ cursor: "pointer",
273
+ };
274
+
275
+ export default ForgotPassword;
frontend/src/docpilot/pages/Login.jsx ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+
3
+ import { loginRequest } from "../api";
4
+
5
+ function Login({
6
+ onLogin,
7
+ goToSignup,
8
+ goToForgot,
9
+ }) {
10
+
11
+ const [email,
12
+ setEmail] =
13
+ useState("");
14
+
15
+ const [password,
16
+ setPassword] =
17
+ useState("");
18
+
19
+ const login =
20
+ async () => {
21
+
22
+ try {
23
+
24
+ const data =
25
+ await loginRequest(
26
+ email,
27
+ password
28
+ );
29
+
30
+ if (
31
+ !data.access_token
32
+ ) {
33
+
34
+ alert(
35
+ "Invalid credentials"
36
+ );
37
+
38
+ return;
39
+ }
40
+
41
+ localStorage.setItem(
42
+ "token",
43
+ data.access_token
44
+ );
45
+
46
+ onLogin();
47
+
48
+ } catch (error) {
49
+
50
+ console.error(error);
51
+
52
+ alert(
53
+ "Wrong email or password"
54
+ );
55
+ }
56
+ };
57
+
58
+ return (
59
+ <div
60
+ style={{
61
+ width: "100vw",
62
+
63
+ height: "100vh",
64
+
65
+ background:
66
+ "#111",
67
+
68
+ display: "flex",
69
+
70
+ justifyContent:
71
+ "center",
72
+
73
+ alignItems:
74
+ "center",
75
+
76
+ overflow:
77
+ "hidden",
78
+ }}
79
+ >
80
+ <div
81
+ style={{
82
+ width: "100%",
83
+
84
+ maxWidth:
85
+ "520px",
86
+
87
+ display:
88
+ "flex",
89
+
90
+ flexDirection:
91
+ "column",
92
+
93
+ alignItems:
94
+ "center",
95
+
96
+ padding:
97
+ "24px",
98
+
99
+ boxSizing:
100
+ "border-box",
101
+ }}
102
+ >
103
+
104
+ <h1
105
+ style={{
106
+ fontSize:
107
+ "72px",
108
+
109
+ fontFamily:
110
+ "Georgia, serif",
111
+
112
+ fontWeight:
113
+ "600",
114
+
115
+ marginBottom:
116
+ "36px",
117
+
118
+ letterSpacing:
119
+ "-2px",
120
+
121
+ color:
122
+ "white",
123
+
124
+ lineHeight:
125
+ "1",
126
+
127
+ textAlign:
128
+ "center",
129
+ }}
130
+ >
131
+ DocPilot
132
+ </h1>
133
+
134
+ <input
135
+ placeholder="Email"
136
+
137
+ value={email}
138
+
139
+ onChange={(e) =>
140
+ setEmail(
141
+ e.target.value
142
+ )
143
+ }
144
+
145
+ style={inputStyle}
146
+ />
147
+
148
+ <input
149
+ type="password"
150
+
151
+ placeholder="Password"
152
+
153
+ value={password}
154
+
155
+ onChange={(e) =>
156
+ setPassword(
157
+ e.target.value
158
+ )
159
+ }
160
+
161
+ style={inputStyle}
162
+ />
163
+
164
+ <button
165
+ onClick={login}
166
+
167
+ style={buttonStyle}
168
+ >
169
+ Login
170
+ </button>
171
+
172
+ <p
173
+ onClick={
174
+ goToSignup
175
+ }
176
+
177
+ style={{
178
+ marginTop:
179
+ "22px",
180
+
181
+ color:
182
+ "#888",
183
+
184
+ textAlign:
185
+ "center",
186
+
187
+ cursor:
188
+ "pointer",
189
+
190
+ fontWeight:
191
+ "600",
192
+
193
+ fontSize:
194
+ "18px",
195
+ }}
196
+ >
197
+ Don't have an account?
198
+ {" "}
199
+ Sign up
200
+ </p>
201
+
202
+ <p
203
+ onClick={
204
+ goToForgot
205
+ }
206
+
207
+ style={{
208
+ marginTop:
209
+ "14px",
210
+
211
+ color:
212
+ "#666",
213
+
214
+ textAlign:
215
+ "center",
216
+
217
+ cursor:
218
+ "pointer",
219
+
220
+ fontWeight:
221
+ "600",
222
+
223
+ fontSize:
224
+ "16px",
225
+ }}
226
+ >
227
+ Forgot password?
228
+ </p>
229
+
230
+ </div>
231
+ </div>
232
+ );
233
+ }
234
+
235
+ const inputStyle = {
236
+ width: "520px",
237
+
238
+ maxWidth:
239
+ "90vw",
240
+
241
+ padding:
242
+ "22px",
243
+
244
+ marginBottom:
245
+ "18px",
246
+
247
+ borderRadius:
248
+ "18px",
249
+
250
+ border:
251
+ "1px solid #2e2e2e",
252
+
253
+ background:
254
+ "#1d1d1d",
255
+
256
+ color:
257
+ "white",
258
+
259
+ fontSize:
260
+ "18px",
261
+
262
+ outline:
263
+ "none",
264
+
265
+ boxSizing:
266
+ "border-box",
267
+ };
268
+
269
+ const buttonStyle = {
270
+ width: "520px",
271
+
272
+ maxWidth:
273
+ "90vw",
274
+
275
+ padding:
276
+ "22px",
277
+
278
+ borderRadius:
279
+ "18px",
280
+
281
+ border:
282
+ "1px solid #333",
283
+
284
+ background:
285
+ "#2a2a2a",
286
+
287
+ color:
288
+ "white",
289
+
290
+ fontSize:
291
+ "18px",
292
+
293
+ cursor:
294
+ "pointer",
295
+
296
+ fontWeight:
297
+ "700",
298
+
299
+ boxSizing:
300
+ "border-box",
301
+ };
302
+
303
+ export default Login;