added new UI, added form, added api message
Browse files- README.md +3 -13
- backend/app/db/database.py +14 -2
- backend/app/db/database_dynamodb.py +1 -0
- backend/app/db/database_mongodb.py +58 -2
- backend/app/db/models.py +24 -1
- backend/app/main.py +123 -20
- docker-compose.yml +0 -22
- frontend/package.json +1 -0
- frontend/pnpm-lock.yaml +613 -0
- frontend/src/App.jsx +5 -5
- frontend/src/components/Chat.jsx +13 -17
- frontend/src/components/ChatApi.jsx +107 -0
- frontend/src/components/Opportunities.jsx +51 -52
- frontend/src/components/OpportunityForm.jsx +288 -0
- frontend/src/components/layout/TwoColumn.jsx +61 -0
- frontend/src/components/ui/button.jsx +3 -2
- frontend/src/components/ui/card.jsx +54 -0
- frontend/src/components/ui/input.jsx +1 -2
- frontend/src/components/ui/select.jsx +102 -0
- frontend/src/components/ui/tabs.jsx +95 -0
- frontend/src/components/ui/textarea.jsx +18 -0
- frontend/src/lib/{utils.ts β utils.js} +2 -2
- frontend/tsconfig.json +0 -30
- frontend/tsconfig.node.json +0 -12
README.md
CHANGED
@@ -1,21 +1,11 @@
|
|
1 |
-
|
2 |
-
title: SalesBuddy for BetterTech
|
3 |
-
emoji: π
|
4 |
-
colorFrom: pink
|
5 |
-
colorTo: blue
|
6 |
-
sdk: docker
|
7 |
-
pinned: false
|
8 |
-
license: mit
|
9 |
-
short_description: AIE4 Project - SalesBuddy for BetterTech
|
10 |
-
---
|
11 |
-
|
12 |
-
# SalesBuddy for BetterTech
|
13 |
|
14 |
## Description
|
15 |
-
|
16 |
|
17 |
## Prerequisites
|
18 |
- Python 3.11
|
|
|
19 |
- pip
|
20 |
|
21 |
## Steps to Setup and Run the Project Locally
|
|
|
1 |
+
# SalesOrion, formerly SalesBuddy
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
## Description
|
4 |
+
SalesOrion, formerly SalesBuddy
|
5 |
|
6 |
## Prerequisites
|
7 |
- Python 3.11
|
8 |
+
- node 20.11.0
|
9 |
- pip
|
10 |
|
11 |
## Steps to Setup and Run the Project Locally
|
backend/app/db/database.py
CHANGED
@@ -7,11 +7,23 @@ db_type = os.getenv("DB_TYPE")
|
|
7 |
|
8 |
|
9 |
if db_type == "mongodb":
|
10 |
-
from .database_mongodb import get_user_by_username, create_user, save_file, get_user_files
|
11 |
else:
|
12 |
from .database_dynamodb import get_user_by_username, create_user, save_file, get_user_files
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
|
14 |
get_user_by_username
|
15 |
create_user
|
16 |
save_file
|
17 |
-
get_user_files
|
|
|
|
|
|
|
|
7 |
|
8 |
|
9 |
if db_type == "mongodb":
|
10 |
+
from .database_mongodb import get_user_by_username, create_user, save_file, get_user_files, create_opportunity, get_opportunities, get_opportunity_count
|
11 |
else:
|
12 |
from .database_dynamodb import get_user_by_username, create_user, save_file, get_user_files
|
13 |
+
async def create_opportunity(opportunity):
|
14 |
+
"""Dummy function that does nothing"""
|
15 |
+
return None
|
16 |
+
async def get_opportunities(username: str):
|
17 |
+
"""Dummy function that returns empty list"""
|
18 |
+
return []
|
19 |
+
async def get_opportunity_count(username: str):
|
20 |
+
"""Dummy function that returns 0"""
|
21 |
+
return 0
|
22 |
|
23 |
get_user_by_username
|
24 |
create_user
|
25 |
save_file
|
26 |
+
get_user_files
|
27 |
+
create_opportunity
|
28 |
+
get_opportunities
|
29 |
+
get_opportunity_count
|
backend/app/db/database_dynamodb.py
CHANGED
@@ -42,6 +42,7 @@ async def save_file(username: str, file_upload: FileUpload) -> bool:
|
|
42 |
'updated_at': datetime.datetime.now(datetime.UTC).isoformat()
|
43 |
}
|
44 |
)
|
|
|
45 |
return True
|
46 |
except ClientError:
|
47 |
return False
|
|
|
42 |
'updated_at': datetime.datetime.now(datetime.UTC).isoformat()
|
43 |
}
|
44 |
)
|
45 |
+
|
46 |
return True
|
47 |
except ClientError:
|
48 |
return False
|
backend/app/db/database_mongodb.py
CHANGED
@@ -1,10 +1,13 @@
|
|
1 |
# backend/app/database.py
|
|
|
|
|
2 |
from motor.motor_asyncio import AsyncIOMotorClient
|
3 |
import datetime
|
4 |
from typing import Optional, List
|
5 |
-
from .models import User, FileUpload
|
6 |
-
from bson import Binary
|
7 |
import os
|
|
|
8 |
|
9 |
# Get MongoDB connection string from environment variable
|
10 |
MONGO_URI = os.getenv("MONGODB_URI", "mongodb://localhost:27017")
|
@@ -16,6 +19,7 @@ db = client[DB_NAME]
|
|
16 |
# Collections
|
17 |
users_collection = db.users
|
18 |
files_collection = db.files
|
|
|
19 |
|
20 |
async def get_user_by_username(username: str) -> Optional[User]:
|
21 |
"""
|
@@ -91,6 +95,25 @@ async def save_file(username: str, records: any, filename: str) -> bool:
|
|
91 |
upsert=True
|
92 |
)
|
93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
94 |
return bool(result.modified_count or result.upserted_id)
|
95 |
except Exception as e:
|
96 |
print(f"Error saving file: {e}")
|
@@ -169,6 +192,33 @@ async def update_user(username: str, update_data: dict) -> bool:
|
|
169 |
print(f"Error updating user: {e}")
|
170 |
return False
|
171 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
# Index creation function - call this during application startup
|
173 |
async def create_indexes():
|
174 |
"""
|
@@ -184,10 +234,16 @@ async def create_indexes():
|
|
184 |
await files_collection.create_index("created_at")
|
185 |
await files_collection.create_index("updated_at")
|
186 |
|
|
|
|
|
|
|
|
|
|
|
187 |
return True
|
188 |
except Exception as e:
|
189 |
print(f"Error creating indexes: {e}")
|
190 |
return False
|
|
|
191 |
|
192 |
# Optional: Add these to your requirements.txt
|
193 |
# motor==3.3.1
|
|
|
1 |
# backend/app/database.py
|
2 |
+
from fastapi import Request, Depends, HTTPException
|
3 |
+
from fastapi.responses import JSONResponse
|
4 |
from motor.motor_asyncio import AsyncIOMotorClient
|
5 |
import datetime
|
6 |
from typing import Optional, List
|
7 |
+
from .models import User, FileUpload, Opportunity
|
8 |
+
from bson import Binary, ObjectId
|
9 |
import os
|
10 |
+
import json
|
11 |
|
12 |
# Get MongoDB connection string from environment variable
|
13 |
MONGO_URI = os.getenv("MONGODB_URI", "mongodb://localhost:27017")
|
|
|
19 |
# Collections
|
20 |
users_collection = db.users
|
21 |
files_collection = db.files
|
22 |
+
opportunities_collection = db.opportunities
|
23 |
|
24 |
async def get_user_by_username(username: str) -> Optional[User]:
|
25 |
"""
|
|
|
95 |
upsert=True
|
96 |
)
|
97 |
|
98 |
+
async for content in records: #assume csv is the same format for all files
|
99 |
+
opportunity = Opportunity(
|
100 |
+
opportunityId=content["Opportunity ID"],
|
101 |
+
opportunityName=content["Opportunity Name"],
|
102 |
+
opportunityState=content["Opportunity Stage"],
|
103 |
+
opportunityValue=content["Opportunity Value"],
|
104 |
+
customerName=content["Customer Name"],
|
105 |
+
customerContact=content["Customer Contact"],
|
106 |
+
customerContactRole=content["Customer Contact Role"],
|
107 |
+
nextSteps=content["Next Steps"],
|
108 |
+
opportunityDescription=content["Opportunity Description"],
|
109 |
+
activity=content["Activity"],
|
110 |
+
closeDate=content["Close Date"],
|
111 |
+
created_at=current_time,
|
112 |
+
updated_at=current_time,
|
113 |
+
username=username
|
114 |
+
)
|
115 |
+
await create_opportunity(opportunity)
|
116 |
+
|
117 |
return bool(result.modified_count or result.upserted_id)
|
118 |
except Exception as e:
|
119 |
print(f"Error saving file: {e}")
|
|
|
192 |
print(f"Error updating user: {e}")
|
193 |
return False
|
194 |
|
195 |
+
# Opportunities
|
196 |
+
async def get_opportunities(username: str, skip: int = 0, limit: int = 100) -> List[Opportunity]:
|
197 |
+
"""
|
198 |
+
Retrieve opportunities belonging to a user with pagination
|
199 |
+
"""
|
200 |
+
cursor = opportunities_collection.find({"username": username}).skip(skip).limit(limit)
|
201 |
+
opportunities = await cursor.to_list(length=None)
|
202 |
+
return [Opportunity(**doc) for doc in opportunities]
|
203 |
+
|
204 |
+
|
205 |
+
async def get_opportunity_count(username: str) -> int:
|
206 |
+
"""
|
207 |
+
Get the total number of opportunities for a user
|
208 |
+
"""
|
209 |
+
return await opportunities_collection.count_documents({"username": username})
|
210 |
+
|
211 |
+
|
212 |
+
async def create_opportunity(opportunity: Opportunity) -> bool:
|
213 |
+
"""
|
214 |
+
Create a new opportunity
|
215 |
+
"""
|
216 |
+
#opportunity.created_at = datetime.datetime.now(datetime.UTC)
|
217 |
+
#opportunity.updated_at = datetime.datetime.now(datetime.UTC)
|
218 |
+
print("opportunity********", opportunity)
|
219 |
+
await opportunities_collection.insert_one(opportunity.model_dump())
|
220 |
+
return True
|
221 |
+
|
222 |
# Index creation function - call this during application startup
|
223 |
async def create_indexes():
|
224 |
"""
|
|
|
234 |
await files_collection.create_index("created_at")
|
235 |
await files_collection.create_index("updated_at")
|
236 |
|
237 |
+
# Opportunities indexes
|
238 |
+
await opportunities_collection.create_index("username")
|
239 |
+
await opportunities_collection.create_index("created_at")
|
240 |
+
await opportunities_collection.create_index("updated_at")
|
241 |
+
|
242 |
return True
|
243 |
except Exception as e:
|
244 |
print(f"Error creating indexes: {e}")
|
245 |
return False
|
246 |
+
|
247 |
|
248 |
# Optional: Add these to your requirements.txt
|
249 |
# motor==3.3.1
|
backend/app/db/models.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
from pydantic import BaseModel, EmailStr
|
2 |
-
|
3 |
from typing import Optional
|
4 |
from datetime import datetime
|
5 |
|
@@ -20,6 +20,29 @@ class FileUpload(BaseModel):
|
|
20 |
content: list[dict]
|
21 |
created_at: Optional[datetime] = None
|
22 |
updated_at: Optional[datetime] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
|
24 |
class ChatMessage(BaseModel):
|
25 |
message: str
|
|
|
1 |
from pydantic import BaseModel, EmailStr
|
2 |
+
from bson import ObjectId
|
3 |
from typing import Optional
|
4 |
from datetime import datetime
|
5 |
|
|
|
20 |
content: list[dict]
|
21 |
created_at: Optional[datetime] = None
|
22 |
updated_at: Optional[datetime] = None
|
23 |
+
|
24 |
+
class Opportunity(BaseModel):
|
25 |
+
username:str
|
26 |
+
activity: str
|
27 |
+
closeDate: datetime
|
28 |
+
customerContact: str
|
29 |
+
customerContactRole: str
|
30 |
+
customerName: str
|
31 |
+
nextSteps: str
|
32 |
+
opportunityDescription: str
|
33 |
+
opportunityId: str
|
34 |
+
opportunityName: str
|
35 |
+
opportunityState: str
|
36 |
+
opportunityValue: str
|
37 |
+
created_at: Optional[datetime] = None
|
38 |
+
updated_at: Optional[datetime] = None
|
39 |
+
|
40 |
+
class Config:
|
41 |
+
json_encoders = {
|
42 |
+
datetime: lambda v: v.isoformat(),
|
43 |
+
ObjectId: lambda v: str(v)
|
44 |
+
}
|
45 |
+
allow_population_by_field_name = True
|
46 |
|
47 |
class ChatMessage(BaseModel):
|
48 |
message: str
|
backend/app/main.py
CHANGED
@@ -1,25 +1,28 @@
|
|
1 |
import os
|
2 |
-
import base64
|
3 |
import io
|
4 |
-
import csv
|
5 |
import json
|
6 |
import datetime
|
7 |
import pandas as pd
|
8 |
from datetime import timedelta
|
|
|
9 |
from fastapi import FastAPI,WebSocket, Depends, HTTPException, status, UploadFile, File
|
10 |
from fastapi.responses import FileResponse, JSONResponse
|
11 |
from fastapi.requests import Request
|
12 |
from fastapi.staticfiles import StaticFiles
|
13 |
from fastapi.middleware.cors import CORSMiddleware
|
14 |
from fastapi.security import OAuth2PasswordRequestForm
|
|
|
|
|
|
|
|
|
15 |
from .auth import (
|
16 |
get_current_user,
|
17 |
create_access_token,
|
18 |
verify_password,
|
19 |
-
get_password_hash
|
20 |
)
|
21 |
-
from .db.models import User, Token,
|
22 |
-
from .db.database import get_user_by_username, create_user, save_file,
|
23 |
from .websocket import handle_websocket
|
24 |
from .llm_models import invoke_general_model, invoke_customer_search
|
25 |
|
@@ -94,10 +97,6 @@ async def upload_file(
|
|
94 |
# Convert DataFrame to list of dictionaries
|
95 |
records = json.loads(df.to_json(orient='records'))
|
96 |
|
97 |
-
|
98 |
-
# Insert into MongoDB
|
99 |
-
|
100 |
-
|
101 |
if not await save_file(current_user.username, records, file.filename):
|
102 |
raise HTTPException(
|
103 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
@@ -106,16 +105,106 @@ async def upload_file(
|
|
106 |
|
107 |
return {"message": "File uploaded successfully"}
|
108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
@app.get("/api/opportunities")
|
110 |
-
async def
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
116 |
|
117 |
-
|
118 |
-
return {"records": all_records , "success": len(all_records) > 0}
|
119 |
|
120 |
@app.websocket("/ws")
|
121 |
async def websocket_endpoint(websocket: WebSocket) -> None:
|
@@ -125,8 +214,8 @@ async def websocket_endpoint(websocket: WebSocket) -> None:
|
|
125 |
async def message(obj: dict, current_user: User = Depends(get_current_user)) -> JSONResponse:
|
126 |
"""Endpoint to handle general incoming messages from the frontend."""
|
127 |
answer = invoke_general_model(obj["message"])
|
128 |
-
|
129 |
-
|
130 |
|
131 |
@app.post("/api/customer_insights")
|
132 |
async def customer_insights(obj: dict) -> JSONResponse:
|
@@ -136,6 +225,19 @@ async def customer_insights(obj: dict) -> JSONResponse:
|
|
136 |
|
137 |
app.mount("/assets", StaticFiles(directory=os.path.join(frontend_dir, "assets")), name="static")
|
138 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
139 |
if __name__ == "__main__":
|
140 |
from fastapi.testclient import TestClient
|
141 |
|
@@ -143,9 +245,10 @@ if __name__ == "__main__":
|
|
143 |
|
144 |
def test_message_endpoint():
|
145 |
# Test that the message endpoint returns answers to questions.
|
|
|
146 |
response = client.post("/api/message", json={"message": "What is MEDDPICC?"})
|
147 |
print(response.json())
|
148 |
-
assert response
|
149 |
assert "AIMessage" in response.json()
|
150 |
|
151 |
test_message_endpoint()
|
|
|
1 |
import os
|
|
|
2 |
import io
|
|
|
3 |
import json
|
4 |
import datetime
|
5 |
import pandas as pd
|
6 |
from datetime import timedelta
|
7 |
+
from datetime import datetime as dt
|
8 |
from fastapi import FastAPI,WebSocket, Depends, HTTPException, status, UploadFile, File
|
9 |
from fastapi.responses import FileResponse, JSONResponse
|
10 |
from fastapi.requests import Request
|
11 |
from fastapi.staticfiles import StaticFiles
|
12 |
from fastapi.middleware.cors import CORSMiddleware
|
13 |
from fastapi.security import OAuth2PasswordRequestForm
|
14 |
+
from pydantic import ValidationError, BaseModel
|
15 |
+
from bson import ObjectId
|
16 |
+
|
17 |
+
|
18 |
from .auth import (
|
19 |
get_current_user,
|
20 |
create_access_token,
|
21 |
verify_password,
|
22 |
+
get_password_hash
|
23 |
)
|
24 |
+
from .db.models import User, Token, Opportunity
|
25 |
+
from .db.database import get_user_by_username, create_user, save_file, create_opportunity, get_opportunities, get_opportunity_count
|
26 |
from .websocket import handle_websocket
|
27 |
from .llm_models import invoke_general_model, invoke_customer_search
|
28 |
|
|
|
97 |
# Convert DataFrame to list of dictionaries
|
98 |
records = json.loads(df.to_json(orient='records'))
|
99 |
|
|
|
|
|
|
|
|
|
100 |
if not await save_file(current_user.username, records, file.filename):
|
101 |
raise HTTPException(
|
102 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
|
105 |
|
106 |
return {"message": "File uploaded successfully"}
|
107 |
|
108 |
+
@app.post("/api/save_opportunity")
|
109 |
+
async def save_opportunity(opportunity_data: dict, current_user: User = Depends(get_current_user)) -> dict:
|
110 |
+
try:
|
111 |
+
opportunity_data= {
|
112 |
+
**opportunity_data,
|
113 |
+
"username":current_user.username,
|
114 |
+
"created_at":datetime.datetime.now(datetime.UTC),
|
115 |
+
"updated_at":datetime.datetime.now(datetime.UTC)
|
116 |
+
}
|
117 |
+
print("data********", opportunity_data)
|
118 |
+
opportunity = Opportunity(**opportunity_data)
|
119 |
+
if not await create_opportunity(opportunity):
|
120 |
+
raise HTTPException(
|
121 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
122 |
+
detail="Could not save opportunity"
|
123 |
+
)
|
124 |
+
return {"message": "Opportunity saved successfully"}
|
125 |
+
except ValidationError as e:
|
126 |
+
print(f"Validation error: {e}")
|
127 |
+
raise HTTPException(
|
128 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
129 |
+
detail="Invalid opportunity data"
|
130 |
+
)
|
131 |
+
|
132 |
@app.get("/api/opportunities")
|
133 |
+
async def retrieve_opportunities(
|
134 |
+
request: Request,
|
135 |
+
page: int = 1,
|
136 |
+
limit: int = 100,
|
137 |
+
current_user: User = Depends(get_current_user)
|
138 |
+
) -> JSONResponse:
|
139 |
+
"""
|
140 |
+
Retrieve paginated opportunities for the current user
|
141 |
+
"""
|
142 |
+
class JSONEncoder(json.JSONEncoder):
|
143 |
+
def default(self, obj):
|
144 |
+
if isinstance(obj, dt):
|
145 |
+
return obj.isoformat()
|
146 |
+
if isinstance(obj, ObjectId):
|
147 |
+
return str(obj)
|
148 |
+
return super().default(obj)
|
149 |
+
|
150 |
+
try:
|
151 |
+
skip = (page - 1) * limit
|
152 |
+
|
153 |
+
# Get paginated records
|
154 |
+
records = await get_opportunities(
|
155 |
+
username=current_user.username,
|
156 |
+
skip=skip,
|
157 |
+
limit=limit
|
158 |
+
)
|
159 |
+
|
160 |
+
# Process records with proper serialization
|
161 |
+
all_records = []
|
162 |
+
for record in records:
|
163 |
+
# Convert MongoDB document to dict and handle ObjectId
|
164 |
+
record_dict = record.dict(by_alias=True)
|
165 |
+
if "_id" in record_dict:
|
166 |
+
record_dict["_id"] = str(record_dict["_id"])
|
167 |
+
|
168 |
+
# Process content if it exists
|
169 |
+
if hasattr(record, 'content') and isinstance(record.content, (list, tuple)):
|
170 |
+
all_records.extend(record.content)
|
171 |
+
else:
|
172 |
+
all_records.append(record_dict)
|
173 |
+
|
174 |
+
# Count total records
|
175 |
+
total_count = await get_opportunity_count(current_user.username)
|
176 |
+
|
177 |
+
# Create response using Pydantic model
|
178 |
+
response_data = PaginatedResponse(
|
179 |
+
page=page,
|
180 |
+
limit=limit,
|
181 |
+
total_records=total_count,
|
182 |
+
total_pages=-(-total_count // limit),
|
183 |
+
has_more=(skip + limit) < total_count,
|
184 |
+
records=all_records
|
185 |
+
)
|
186 |
+
|
187 |
+
# Convert to JSON with custom encoder
|
188 |
+
return JSONResponse(
|
189 |
+
content=json.loads(
|
190 |
+
json.dumps(
|
191 |
+
{
|
192 |
+
"success": True,
|
193 |
+
"data": response_data.model_dump()
|
194 |
+
},
|
195 |
+
cls=JSONEncoder
|
196 |
+
)
|
197 |
+
),
|
198 |
+
status_code=200
|
199 |
+
)
|
200 |
+
|
201 |
+
except Exception as e:
|
202 |
+
print(f"Error retrieving opportunities: {str(e)}")
|
203 |
+
raise HTTPException(
|
204 |
+
status_code=500,
|
205 |
+
detail=f"An error occurred while retrieving opportunities: {str(e)}"
|
206 |
+
)
|
207 |
|
|
|
|
|
208 |
|
209 |
@app.websocket("/ws")
|
210 |
async def websocket_endpoint(websocket: WebSocket) -> None:
|
|
|
214 |
async def message(obj: dict, current_user: User = Depends(get_current_user)) -> JSONResponse:
|
215 |
"""Endpoint to handle general incoming messages from the frontend."""
|
216 |
answer = invoke_general_model(obj["message"])
|
217 |
+
print("answer**********", answer)
|
218 |
+
return JSONResponse(content={"message": json.loads(answer.model_dump_json() )})
|
219 |
|
220 |
@app.post("/api/customer_insights")
|
221 |
async def customer_insights(obj: dict) -> JSONResponse:
|
|
|
225 |
|
226 |
app.mount("/assets", StaticFiles(directory=os.path.join(frontend_dir, "assets")), name="static")
|
227 |
|
228 |
+
class PaginatedResponse(BaseModel):
|
229 |
+
page: int
|
230 |
+
limit: int
|
231 |
+
total_records: int
|
232 |
+
total_pages: int
|
233 |
+
has_more: bool
|
234 |
+
records: list
|
235 |
+
|
236 |
+
class Config:
|
237 |
+
json_encoders = {
|
238 |
+
datetime: lambda v: v.isoformat(),
|
239 |
+
ObjectId: lambda v: str(v)
|
240 |
+
}
|
241 |
if __name__ == "__main__":
|
242 |
from fastapi.testclient import TestClient
|
243 |
|
|
|
245 |
|
246 |
def test_message_endpoint():
|
247 |
# Test that the message endpoint returns answers to questions.
|
248 |
+
# Update test as this api requires a token for authorization to use
|
249 |
response = client.post("/api/message", json={"message": "What is MEDDPICC?"})
|
250 |
print(response.json())
|
251 |
+
assert response["status_code"] == 200
|
252 |
assert "AIMessage" in response.json()
|
253 |
|
254 |
test_message_endpoint()
|
docker-compose.yml
DELETED
@@ -1,22 +0,0 @@
|
|
1 |
-
version: '3.8'
|
2 |
-
|
3 |
-
services:
|
4 |
-
backend:
|
5 |
-
build: ./backend
|
6 |
-
ports:
|
7 |
-
- "8000:8000"
|
8 |
-
environment:
|
9 |
-
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
10 |
-
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
|
11 |
-
- AWS_REGION=${AWS_REGION}
|
12 |
-
- MONGO_URI=${MONGO_URI}
|
13 |
-
command: uvicorn app.main:app --host 0.0.0.0 --port 8000
|
14 |
-
|
15 |
-
frontend:
|
16 |
-
build: ./frontend
|
17 |
-
ports:
|
18 |
-
- "3000:3000"
|
19 |
-
environment:
|
20 |
-
- VITE_WS_URL=ws://localhost:8000
|
21 |
-
depends_on:
|
22 |
-
- backend
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/package.json
CHANGED
@@ -12,6 +12,7 @@
|
|
12 |
"preview": "vite preview"
|
13 |
},
|
14 |
"dependencies": {
|
|
|
15 |
"@radix-ui/react-slot": "^1.0.2",
|
16 |
"class-variance-authority": "^0.7.0",
|
17 |
"clsx": "^2.0.0",
|
|
|
12 |
"preview": "vite preview"
|
13 |
},
|
14 |
"dependencies": {
|
15 |
+
"@radix-ui/react-select": "^2.1.2",
|
16 |
"@radix-ui/react-slot": "^1.0.2",
|
17 |
"class-variance-authority": "^0.7.0",
|
18 |
"clsx": "^2.0.0",
|
frontend/pnpm-lock.yaml
CHANGED
@@ -8,6 +8,9 @@ importers:
|
|
8 |
|
9 |
.:
|
10 |
dependencies:
|
|
|
|
|
|
|
11 |
'@radix-ui/react-slot':
|
12 |
specifier: ^1.0.2
|
13 |
version: 1.0.2(@types/react@18.2.37)(react@18.2.0)
|
@@ -327,6 +330,21 @@ packages:
|
|
327 |
resolution: {integrity: sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==}
|
328 |
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
329 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
330 |
'@humanwhocodes/config-array@0.11.13':
|
331 |
resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==}
|
332 |
engines: {node: '>=10.10.0'}
|
@@ -390,6 +408,38 @@ packages:
|
|
390 |
resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
|
391 |
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
392 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
393 |
'@radix-ui/react-compose-refs@1.0.1':
|
394 |
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
|
395 |
peerDependencies:
|
@@ -399,6 +449,138 @@ packages:
|
|
399 |
'@types/react':
|
400 |
optional: true
|
401 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
402 |
'@radix-ui/react-slot@1.0.2':
|
403 |
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
|
404 |
peerDependencies:
|
@@ -408,6 +590,94 @@ packages:
|
|
408 |
'@types/react':
|
409 |
optional: true
|
410 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
411 |
'@remix-run/router@1.20.0':
|
412 |
resolution: {integrity: sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==}
|
413 |
engines: {node: '>=14.0.0'}
|
@@ -581,6 +851,10 @@ packages:
|
|
581 |
argparse@2.0.1:
|
582 |
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
583 |
|
|
|
|
|
|
|
|
|
584 |
autoprefixer@10.4.16:
|
585 |
resolution: {integrity: sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==}
|
586 |
engines: {node: ^10 || ^12 || >=14}
|
@@ -695,6 +969,9 @@ packages:
|
|
695 |
deep-is@0.1.4:
|
696 |
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
697 |
|
|
|
|
|
|
|
698 |
didyoumean@1.2.2:
|
699 |
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
700 |
|
@@ -846,6 +1123,10 @@ packages:
|
|
846 |
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
847 |
engines: {node: '>=6.9.0'}
|
848 |
|
|
|
|
|
|
|
|
|
849 |
glob-parent@5.1.2:
|
850 |
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
851 |
engines: {node: '>= 6'}
|
@@ -900,6 +1181,9 @@ packages:
|
|
900 |
inherits@2.0.4:
|
901 |
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
902 |
|
|
|
|
|
|
|
903 |
is-binary-path@2.1.0:
|
904 |
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
905 |
engines: {node: '>=8'}
|
@@ -1164,6 +1448,26 @@ packages:
|
|
1164 |
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
1165 |
engines: {node: '>=0.10.0'}
|
1166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1167 |
react-router-dom@6.27.0:
|
1168 |
resolution: {integrity: sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==}
|
1169 |
engines: {node: '>=14.0.0'}
|
@@ -1177,6 +1481,16 @@ packages:
|
|
1177 |
peerDependencies:
|
1178 |
react: '>=16.8'
|
1179 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1180 |
react@18.2.0:
|
1181 |
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
1182 |
engines: {node: '>=0.10.0'}
|
@@ -1351,6 +1665,26 @@ packages:
|
|
1351 |
uri-js@4.4.1:
|
1352 |
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
1353 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1354 |
util-deprecate@1.0.2:
|
1355 |
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
1356 |
|
@@ -1631,6 +1965,23 @@ snapshots:
|
|
1631 |
|
1632 |
'@eslint/js@8.53.0': {}
|
1633 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1634 |
'@humanwhocodes/config-array@0.11.13':
|
1635 |
dependencies:
|
1636 |
'@humanwhocodes/object-schema': 2.0.1
|
@@ -1691,6 +2042,29 @@ snapshots:
|
|
1691 |
|
1692 |
'@pkgr/core@0.1.1': {}
|
1693 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1694 |
'@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.37)(react@18.2.0)':
|
1695 |
dependencies:
|
1696 |
'@babel/runtime': 7.23.2
|
@@ -1698,6 +2072,127 @@ snapshots:
|
|
1698 |
optionalDependencies:
|
1699 |
'@types/react': 18.2.37
|
1700 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1701 |
'@radix-ui/react-slot@1.0.2(@types/react@18.2.37)(react@18.2.0)':
|
1702 |
dependencies:
|
1703 |
'@babel/runtime': 7.23.2
|
@@ -1706,6 +2201,69 @@ snapshots:
|
|
1706 |
optionalDependencies:
|
1707 |
'@types/react': 18.2.37
|
1708 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1709 |
'@remix-run/router@1.20.0': {}
|
1710 |
|
1711 |
'@rollup/rollup-android-arm-eabi@4.25.0':
|
@@ -1854,6 +2412,10 @@ snapshots:
|
|
1854 |
|
1855 |
argparse@2.0.1: {}
|
1856 |
|
|
|
|
|
|
|
|
|
1857 |
autoprefixer@10.4.16(postcss@8.4.31):
|
1858 |
dependencies:
|
1859 |
browserslist: 4.22.1
|
@@ -1957,6 +2519,8 @@ snapshots:
|
|
1957 |
|
1958 |
deep-is@0.1.4: {}
|
1959 |
|
|
|
|
|
1960 |
didyoumean@1.2.2: {}
|
1961 |
|
1962 |
dlv@1.1.3: {}
|
@@ -2158,6 +2722,8 @@ snapshots:
|
|
2158 |
|
2159 |
gensync@1.0.0-beta.2: {}
|
2160 |
|
|
|
|
|
2161 |
glob-parent@5.1.2:
|
2162 |
dependencies:
|
2163 |
is-glob: 4.0.3
|
@@ -2216,6 +2782,10 @@ snapshots:
|
|
2216 |
|
2217 |
inherits@2.0.4: {}
|
2218 |
|
|
|
|
|
|
|
|
|
2219 |
is-binary-path@2.1.0:
|
2220 |
dependencies:
|
2221 |
binary-extensions: 2.2.0
|
@@ -2429,6 +2999,25 @@ snapshots:
|
|
2429 |
|
2430 |
react-refresh@0.14.2: {}
|
2431 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2432 |
react-router-dom@6.27.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
2433 |
dependencies:
|
2434 |
'@remix-run/router': 1.20.0
|
@@ -2441,6 +3030,15 @@ snapshots:
|
|
2441 |
'@remix-run/router': 1.20.0
|
2442 |
react: 18.2.0
|
2443 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2444 |
react@18.2.0:
|
2445 |
dependencies:
|
2446 |
loose-envify: 1.4.0
|
@@ -2638,6 +3236,21 @@ snapshots:
|
|
2638 |
dependencies:
|
2639 |
punycode: 2.3.1
|
2640 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2641 |
util-deprecate@1.0.2: {}
|
2642 |
|
2643 |
uuid@9.0.1: {}
|
|
|
8 |
|
9 |
.:
|
10 |
dependencies:
|
11 |
+
'@radix-ui/react-select':
|
12 |
+
specifier: ^2.1.2
|
13 |
+
version: 2.1.2(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
14 |
'@radix-ui/react-slot':
|
15 |
specifier: ^1.0.2
|
16 |
version: 1.0.2(@types/react@18.2.37)(react@18.2.0)
|
|
|
330 |
resolution: {integrity: sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==}
|
331 |
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
332 |
|
333 |
+
'@floating-ui/core@1.6.8':
|
334 |
+
resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==}
|
335 |
+
|
336 |
+
'@floating-ui/dom@1.6.12':
|
337 |
+
resolution: {integrity: sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==}
|
338 |
+
|
339 |
+
'@floating-ui/react-dom@2.1.2':
|
340 |
+
resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==}
|
341 |
+
peerDependencies:
|
342 |
+
react: '>=16.8.0'
|
343 |
+
react-dom: '>=16.8.0'
|
344 |
+
|
345 |
+
'@floating-ui/utils@0.2.8':
|
346 |
+
resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==}
|
347 |
+
|
348 |
'@humanwhocodes/config-array@0.11.13':
|
349 |
resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==}
|
350 |
engines: {node: '>=10.10.0'}
|
|
|
408 |
resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
|
409 |
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
410 |
|
411 |
+
'@radix-ui/number@1.1.0':
|
412 |
+
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
|
413 |
+
|
414 |
+
'@radix-ui/primitive@1.1.0':
|
415 |
+
resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==}
|
416 |
+
|
417 |
+
'@radix-ui/react-arrow@1.1.0':
|
418 |
+
resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==}
|
419 |
+
peerDependencies:
|
420 |
+
'@types/react': '*'
|
421 |
+
'@types/react-dom': '*'
|
422 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
423 |
+
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
424 |
+
peerDependenciesMeta:
|
425 |
+
'@types/react':
|
426 |
+
optional: true
|
427 |
+
'@types/react-dom':
|
428 |
+
optional: true
|
429 |
+
|
430 |
+
'@radix-ui/react-collection@1.1.0':
|
431 |
+
resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==}
|
432 |
+
peerDependencies:
|
433 |
+
'@types/react': '*'
|
434 |
+
'@types/react-dom': '*'
|
435 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
436 |
+
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
437 |
+
peerDependenciesMeta:
|
438 |
+
'@types/react':
|
439 |
+
optional: true
|
440 |
+
'@types/react-dom':
|
441 |
+
optional: true
|
442 |
+
|
443 |
'@radix-ui/react-compose-refs@1.0.1':
|
444 |
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
|
445 |
peerDependencies:
|
|
|
449 |
'@types/react':
|
450 |
optional: true
|
451 |
|
452 |
+
'@radix-ui/react-compose-refs@1.1.0':
|
453 |
+
resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==}
|
454 |
+
peerDependencies:
|
455 |
+
'@types/react': '*'
|
456 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
457 |
+
peerDependenciesMeta:
|
458 |
+
'@types/react':
|
459 |
+
optional: true
|
460 |
+
|
461 |
+
'@radix-ui/react-context@1.1.0':
|
462 |
+
resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==}
|
463 |
+
peerDependencies:
|
464 |
+
'@types/react': '*'
|
465 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
466 |
+
peerDependenciesMeta:
|
467 |
+
'@types/react':
|
468 |
+
optional: true
|
469 |
+
|
470 |
+
'@radix-ui/react-context@1.1.1':
|
471 |
+
resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
|
472 |
+
peerDependencies:
|
473 |
+
'@types/react': '*'
|
474 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
475 |
+
peerDependenciesMeta:
|
476 |
+
'@types/react':
|
477 |
+
optional: true
|
478 |
+
|
479 |
+
'@radix-ui/react-direction@1.1.0':
|
480 |
+
resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
|
481 |
+
peerDependencies:
|
482 |
+
'@types/react': '*'
|
483 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
484 |
+
peerDependenciesMeta:
|
485 |
+
'@types/react':
|
486 |
+
optional: true
|
487 |
+
|
488 |
+
'@radix-ui/react-dismissable-layer@1.1.1':
|
489 |
+
resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==}
|
490 |
+
peerDependencies:
|
491 |
+
'@types/react': '*'
|
492 |
+
'@types/react-dom': '*'
|
493 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
494 |
+
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
495 |
+
peerDependenciesMeta:
|
496 |
+
'@types/react':
|
497 |
+
optional: true
|
498 |
+
'@types/react-dom':
|
499 |
+
optional: true
|
500 |
+
|
501 |
+
'@radix-ui/react-focus-guards@1.1.1':
|
502 |
+
resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==}
|
503 |
+
peerDependencies:
|
504 |
+
'@types/react': '*'
|
505 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
506 |
+
peerDependenciesMeta:
|
507 |
+
'@types/react':
|
508 |
+
optional: true
|
509 |
+
|
510 |
+
'@radix-ui/react-focus-scope@1.1.0':
|
511 |
+
resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==}
|
512 |
+
peerDependencies:
|
513 |
+
'@types/react': '*'
|
514 |
+
'@types/react-dom': '*'
|
515 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
516 |
+
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
517 |
+
peerDependenciesMeta:
|
518 |
+
'@types/react':
|
519 |
+
optional: true
|
520 |
+
'@types/react-dom':
|
521 |
+
optional: true
|
522 |
+
|
523 |
+
'@radix-ui/react-id@1.1.0':
|
524 |
+
resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==}
|
525 |
+
peerDependencies:
|
526 |
+
'@types/react': '*'
|
527 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
528 |
+
peerDependenciesMeta:
|
529 |
+
'@types/react':
|
530 |
+
optional: true
|
531 |
+
|
532 |
+
'@radix-ui/react-popper@1.2.0':
|
533 |
+
resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==}
|
534 |
+
peerDependencies:
|
535 |
+
'@types/react': '*'
|
536 |
+
'@types/react-dom': '*'
|
537 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
538 |
+
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
539 |
+
peerDependenciesMeta:
|
540 |
+
'@types/react':
|
541 |
+
optional: true
|
542 |
+
'@types/react-dom':
|
543 |
+
optional: true
|
544 |
+
|
545 |
+
'@radix-ui/react-portal@1.1.2':
|
546 |
+
resolution: {integrity: sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==}
|
547 |
+
peerDependencies:
|
548 |
+
'@types/react': '*'
|
549 |
+
'@types/react-dom': '*'
|
550 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
551 |
+
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
552 |
+
peerDependenciesMeta:
|
553 |
+
'@types/react':
|
554 |
+
optional: true
|
555 |
+
'@types/react-dom':
|
556 |
+
optional: true
|
557 |
+
|
558 |
+
'@radix-ui/react-primitive@2.0.0':
|
559 |
+
resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
|
560 |
+
peerDependencies:
|
561 |
+
'@types/react': '*'
|
562 |
+
'@types/react-dom': '*'
|
563 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
564 |
+
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
565 |
+
peerDependenciesMeta:
|
566 |
+
'@types/react':
|
567 |
+
optional: true
|
568 |
+
'@types/react-dom':
|
569 |
+
optional: true
|
570 |
+
|
571 |
+
'@radix-ui/react-select@2.1.2':
|
572 |
+
resolution: {integrity: sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA==}
|
573 |
+
peerDependencies:
|
574 |
+
'@types/react': '*'
|
575 |
+
'@types/react-dom': '*'
|
576 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
577 |
+
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
578 |
+
peerDependenciesMeta:
|
579 |
+
'@types/react':
|
580 |
+
optional: true
|
581 |
+
'@types/react-dom':
|
582 |
+
optional: true
|
583 |
+
|
584 |
'@radix-ui/react-slot@1.0.2':
|
585 |
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
|
586 |
peerDependencies:
|
|
|
590 |
'@types/react':
|
591 |
optional: true
|
592 |
|
593 |
+
'@radix-ui/react-slot@1.1.0':
|
594 |
+
resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==}
|
595 |
+
peerDependencies:
|
596 |
+
'@types/react': '*'
|
597 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
598 |
+
peerDependenciesMeta:
|
599 |
+
'@types/react':
|
600 |
+
optional: true
|
601 |
+
|
602 |
+
'@radix-ui/react-use-callback-ref@1.1.0':
|
603 |
+
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
|
604 |
+
peerDependencies:
|
605 |
+
'@types/react': '*'
|
606 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
607 |
+
peerDependenciesMeta:
|
608 |
+
'@types/react':
|
609 |
+
optional: true
|
610 |
+
|
611 |
+
'@radix-ui/react-use-controllable-state@1.1.0':
|
612 |
+
resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
|
613 |
+
peerDependencies:
|
614 |
+
'@types/react': '*'
|
615 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
616 |
+
peerDependenciesMeta:
|
617 |
+
'@types/react':
|
618 |
+
optional: true
|
619 |
+
|
620 |
+
'@radix-ui/react-use-escape-keydown@1.1.0':
|
621 |
+
resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
|
622 |
+
peerDependencies:
|
623 |
+
'@types/react': '*'
|
624 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
625 |
+
peerDependenciesMeta:
|
626 |
+
'@types/react':
|
627 |
+
optional: true
|
628 |
+
|
629 |
+
'@radix-ui/react-use-layout-effect@1.1.0':
|
630 |
+
resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
|
631 |
+
peerDependencies:
|
632 |
+
'@types/react': '*'
|
633 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
634 |
+
peerDependenciesMeta:
|
635 |
+
'@types/react':
|
636 |
+
optional: true
|
637 |
+
|
638 |
+
'@radix-ui/react-use-previous@1.1.0':
|
639 |
+
resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==}
|
640 |
+
peerDependencies:
|
641 |
+
'@types/react': '*'
|
642 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
643 |
+
peerDependenciesMeta:
|
644 |
+
'@types/react':
|
645 |
+
optional: true
|
646 |
+
|
647 |
+
'@radix-ui/react-use-rect@1.1.0':
|
648 |
+
resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
|
649 |
+
peerDependencies:
|
650 |
+
'@types/react': '*'
|
651 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
652 |
+
peerDependenciesMeta:
|
653 |
+
'@types/react':
|
654 |
+
optional: true
|
655 |
+
|
656 |
+
'@radix-ui/react-use-size@1.1.0':
|
657 |
+
resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==}
|
658 |
+
peerDependencies:
|
659 |
+
'@types/react': '*'
|
660 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
661 |
+
peerDependenciesMeta:
|
662 |
+
'@types/react':
|
663 |
+
optional: true
|
664 |
+
|
665 |
+
'@radix-ui/react-visually-hidden@1.1.0':
|
666 |
+
resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==}
|
667 |
+
peerDependencies:
|
668 |
+
'@types/react': '*'
|
669 |
+
'@types/react-dom': '*'
|
670 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
671 |
+
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
672 |
+
peerDependenciesMeta:
|
673 |
+
'@types/react':
|
674 |
+
optional: true
|
675 |
+
'@types/react-dom':
|
676 |
+
optional: true
|
677 |
+
|
678 |
+
'@radix-ui/rect@1.1.0':
|
679 |
+
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
|
680 |
+
|
681 |
'@remix-run/router@1.20.0':
|
682 |
resolution: {integrity: sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==}
|
683 |
engines: {node: '>=14.0.0'}
|
|
|
851 |
argparse@2.0.1:
|
852 |
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
853 |
|
854 |
+
aria-hidden@1.2.4:
|
855 |
+
resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
|
856 |
+
engines: {node: '>=10'}
|
857 |
+
|
858 |
autoprefixer@10.4.16:
|
859 |
resolution: {integrity: sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==}
|
860 |
engines: {node: ^10 || ^12 || >=14}
|
|
|
969 |
deep-is@0.1.4:
|
970 |
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
971 |
|
972 |
+
detect-node-es@1.1.0:
|
973 |
+
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
974 |
+
|
975 |
didyoumean@1.2.2:
|
976 |
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
977 |
|
|
|
1123 |
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
1124 |
engines: {node: '>=6.9.0'}
|
1125 |
|
1126 |
+
get-nonce@1.0.1:
|
1127 |
+
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
|
1128 |
+
engines: {node: '>=6'}
|
1129 |
+
|
1130 |
glob-parent@5.1.2:
|
1131 |
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
1132 |
engines: {node: '>= 6'}
|
|
|
1181 |
inherits@2.0.4:
|
1182 |
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
1183 |
|
1184 |
+
invariant@2.2.4:
|
1185 |
+
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
|
1186 |
+
|
1187 |
is-binary-path@2.1.0:
|
1188 |
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
1189 |
engines: {node: '>=8'}
|
|
|
1448 |
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
1449 |
engines: {node: '>=0.10.0'}
|
1450 |
|
1451 |
+
react-remove-scroll-bar@2.3.6:
|
1452 |
+
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
|
1453 |
+
engines: {node: '>=10'}
|
1454 |
+
peerDependencies:
|
1455 |
+
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
1456 |
+
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
1457 |
+
peerDependenciesMeta:
|
1458 |
+
'@types/react':
|
1459 |
+
optional: true
|
1460 |
+
|
1461 |
+
react-remove-scroll@2.6.0:
|
1462 |
+
resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==}
|
1463 |
+
engines: {node: '>=10'}
|
1464 |
+
peerDependencies:
|
1465 |
+
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
1466 |
+
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
1467 |
+
peerDependenciesMeta:
|
1468 |
+
'@types/react':
|
1469 |
+
optional: true
|
1470 |
+
|
1471 |
react-router-dom@6.27.0:
|
1472 |
resolution: {integrity: sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==}
|
1473 |
engines: {node: '>=14.0.0'}
|
|
|
1481 |
peerDependencies:
|
1482 |
react: '>=16.8'
|
1483 |
|
1484 |
+
react-style-singleton@2.2.1:
|
1485 |
+
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
|
1486 |
+
engines: {node: '>=10'}
|
1487 |
+
peerDependencies:
|
1488 |
+
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
1489 |
+
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
1490 |
+
peerDependenciesMeta:
|
1491 |
+
'@types/react':
|
1492 |
+
optional: true
|
1493 |
+
|
1494 |
react@18.2.0:
|
1495 |
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
1496 |
engines: {node: '>=0.10.0'}
|
|
|
1665 |
uri-js@4.4.1:
|
1666 |
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
1667 |
|
1668 |
+
use-callback-ref@1.3.2:
|
1669 |
+
resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==}
|
1670 |
+
engines: {node: '>=10'}
|
1671 |
+
peerDependencies:
|
1672 |
+
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
1673 |
+
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
1674 |
+
peerDependenciesMeta:
|
1675 |
+
'@types/react':
|
1676 |
+
optional: true
|
1677 |
+
|
1678 |
+
use-sidecar@1.1.2:
|
1679 |
+
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
|
1680 |
+
engines: {node: '>=10'}
|
1681 |
+
peerDependencies:
|
1682 |
+
'@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0
|
1683 |
+
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
1684 |
+
peerDependenciesMeta:
|
1685 |
+
'@types/react':
|
1686 |
+
optional: true
|
1687 |
+
|
1688 |
util-deprecate@1.0.2:
|
1689 |
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
1690 |
|
|
|
1965 |
|
1966 |
'@eslint/js@8.53.0': {}
|
1967 |
|
1968 |
+
'@floating-ui/core@1.6.8':
|
1969 |
+
dependencies:
|
1970 |
+
'@floating-ui/utils': 0.2.8
|
1971 |
+
|
1972 |
+
'@floating-ui/dom@1.6.12':
|
1973 |
+
dependencies:
|
1974 |
+
'@floating-ui/core': 1.6.8
|
1975 |
+
'@floating-ui/utils': 0.2.8
|
1976 |
+
|
1977 |
+
'@floating-ui/react-dom@2.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
1978 |
+
dependencies:
|
1979 |
+
'@floating-ui/dom': 1.6.12
|
1980 |
+
react: 18.2.0
|
1981 |
+
react-dom: 18.2.0(react@18.2.0)
|
1982 |
+
|
1983 |
+
'@floating-ui/utils@0.2.8': {}
|
1984 |
+
|
1985 |
'@humanwhocodes/config-array@0.11.13':
|
1986 |
dependencies:
|
1987 |
'@humanwhocodes/object-schema': 2.0.1
|
|
|
2042 |
|
2043 |
'@pkgr/core@0.1.1': {}
|
2044 |
|
2045 |
+
'@radix-ui/number@1.1.0': {}
|
2046 |
+
|
2047 |
+
'@radix-ui/primitive@1.1.0': {}
|
2048 |
+
|
2049 |
+
'@radix-ui/react-arrow@1.1.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
2050 |
+
dependencies:
|
2051 |
+
'@radix-ui/react-primitive': 2.0.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
2052 |
+
react: 18.2.0
|
2053 |
+
react-dom: 18.2.0(react@18.2.0)
|
2054 |
+
optionalDependencies:
|
2055 |
+
'@types/react': 18.2.37
|
2056 |
+
|
2057 |
+
'@radix-ui/react-collection@1.1.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
2058 |
+
dependencies:
|
2059 |
+
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2060 |
+
'@radix-ui/react-context': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2061 |
+
'@radix-ui/react-primitive': 2.0.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
2062 |
+
'@radix-ui/react-slot': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2063 |
+
react: 18.2.0
|
2064 |
+
react-dom: 18.2.0(react@18.2.0)
|
2065 |
+
optionalDependencies:
|
2066 |
+
'@types/react': 18.2.37
|
2067 |
+
|
2068 |
'@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.37)(react@18.2.0)':
|
2069 |
dependencies:
|
2070 |
'@babel/runtime': 7.23.2
|
|
|
2072 |
optionalDependencies:
|
2073 |
'@types/react': 18.2.37
|
2074 |
|
2075 |
+
'@radix-ui/react-compose-refs@1.1.0(@types/react@18.2.37)(react@18.2.0)':
|
2076 |
+
dependencies:
|
2077 |
+
react: 18.2.0
|
2078 |
+
optionalDependencies:
|
2079 |
+
'@types/react': 18.2.37
|
2080 |
+
|
2081 |
+
'@radix-ui/react-context@1.1.0(@types/react@18.2.37)(react@18.2.0)':
|
2082 |
+
dependencies:
|
2083 |
+
react: 18.2.0
|
2084 |
+
optionalDependencies:
|
2085 |
+
'@types/react': 18.2.37
|
2086 |
+
|
2087 |
+
'@radix-ui/react-context@1.1.1(@types/react@18.2.37)(react@18.2.0)':
|
2088 |
+
dependencies:
|
2089 |
+
react: 18.2.0
|
2090 |
+
optionalDependencies:
|
2091 |
+
'@types/react': 18.2.37
|
2092 |
+
|
2093 |
+
'@radix-ui/react-direction@1.1.0(@types/react@18.2.37)(react@18.2.0)':
|
2094 |
+
dependencies:
|
2095 |
+
react: 18.2.0
|
2096 |
+
optionalDependencies:
|
2097 |
+
'@types/react': 18.2.37
|
2098 |
+
|
2099 |
+
'@radix-ui/react-dismissable-layer@1.1.1(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
2100 |
+
dependencies:
|
2101 |
+
'@radix-ui/primitive': 1.1.0
|
2102 |
+
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2103 |
+
'@radix-ui/react-primitive': 2.0.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
2104 |
+
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2105 |
+
'@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2106 |
+
react: 18.2.0
|
2107 |
+
react-dom: 18.2.0(react@18.2.0)
|
2108 |
+
optionalDependencies:
|
2109 |
+
'@types/react': 18.2.37
|
2110 |
+
|
2111 |
+
'@radix-ui/react-focus-guards@1.1.1(@types/react@18.2.37)(react@18.2.0)':
|
2112 |
+
dependencies:
|
2113 |
+
react: 18.2.0
|
2114 |
+
optionalDependencies:
|
2115 |
+
'@types/react': 18.2.37
|
2116 |
+
|
2117 |
+
'@radix-ui/react-focus-scope@1.1.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
2118 |
+
dependencies:
|
2119 |
+
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2120 |
+
'@radix-ui/react-primitive': 2.0.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
2121 |
+
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2122 |
+
react: 18.2.0
|
2123 |
+
react-dom: 18.2.0(react@18.2.0)
|
2124 |
+
optionalDependencies:
|
2125 |
+
'@types/react': 18.2.37
|
2126 |
+
|
2127 |
+
'@radix-ui/react-id@1.1.0(@types/react@18.2.37)(react@18.2.0)':
|
2128 |
+
dependencies:
|
2129 |
+
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2130 |
+
react: 18.2.0
|
2131 |
+
optionalDependencies:
|
2132 |
+
'@types/react': 18.2.37
|
2133 |
+
|
2134 |
+
'@radix-ui/react-popper@1.2.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
2135 |
+
dependencies:
|
2136 |
+
'@floating-ui/react-dom': 2.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
2137 |
+
'@radix-ui/react-arrow': 1.1.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
2138 |
+
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2139 |
+
'@radix-ui/react-context': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2140 |
+
'@radix-ui/react-primitive': 2.0.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
2141 |
+
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2142 |
+
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2143 |
+
'@radix-ui/react-use-rect': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2144 |
+
'@radix-ui/react-use-size': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2145 |
+
'@radix-ui/rect': 1.1.0
|
2146 |
+
react: 18.2.0
|
2147 |
+
react-dom: 18.2.0(react@18.2.0)
|
2148 |
+
optionalDependencies:
|
2149 |
+
'@types/react': 18.2.37
|
2150 |
+
|
2151 |
+
'@radix-ui/react-portal@1.1.2(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
2152 |
+
dependencies:
|
2153 |
+
'@radix-ui/react-primitive': 2.0.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
2154 |
+
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2155 |
+
react: 18.2.0
|
2156 |
+
react-dom: 18.2.0(react@18.2.0)
|
2157 |
+
optionalDependencies:
|
2158 |
+
'@types/react': 18.2.37
|
2159 |
+
|
2160 |
+
'@radix-ui/react-primitive@2.0.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
2161 |
+
dependencies:
|
2162 |
+
'@radix-ui/react-slot': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2163 |
+
react: 18.2.0
|
2164 |
+
react-dom: 18.2.0(react@18.2.0)
|
2165 |
+
optionalDependencies:
|
2166 |
+
'@types/react': 18.2.37
|
2167 |
+
|
2168 |
+
'@radix-ui/react-select@2.1.2(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
2169 |
+
dependencies:
|
2170 |
+
'@radix-ui/number': 1.1.0
|
2171 |
+
'@radix-ui/primitive': 1.1.0
|
2172 |
+
'@radix-ui/react-collection': 1.1.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
2173 |
+
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2174 |
+
'@radix-ui/react-context': 1.1.1(@types/react@18.2.37)(react@18.2.0)
|
2175 |
+
'@radix-ui/react-direction': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2176 |
+
'@radix-ui/react-dismissable-layer': 1.1.1(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
2177 |
+
'@radix-ui/react-focus-guards': 1.1.1(@types/react@18.2.37)(react@18.2.0)
|
2178 |
+
'@radix-ui/react-focus-scope': 1.1.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
2179 |
+
'@radix-ui/react-id': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2180 |
+
'@radix-ui/react-popper': 1.2.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
2181 |
+
'@radix-ui/react-portal': 1.1.2(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
2182 |
+
'@radix-ui/react-primitive': 2.0.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
2183 |
+
'@radix-ui/react-slot': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2184 |
+
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2185 |
+
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2186 |
+
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2187 |
+
'@radix-ui/react-use-previous': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2188 |
+
'@radix-ui/react-visually-hidden': 1.1.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
2189 |
+
aria-hidden: 1.2.4
|
2190 |
+
react: 18.2.0
|
2191 |
+
react-dom: 18.2.0(react@18.2.0)
|
2192 |
+
react-remove-scroll: 2.6.0(@types/react@18.2.37)(react@18.2.0)
|
2193 |
+
optionalDependencies:
|
2194 |
+
'@types/react': 18.2.37
|
2195 |
+
|
2196 |
'@radix-ui/react-slot@1.0.2(@types/react@18.2.37)(react@18.2.0)':
|
2197 |
dependencies:
|
2198 |
'@babel/runtime': 7.23.2
|
|
|
2201 |
optionalDependencies:
|
2202 |
'@types/react': 18.2.37
|
2203 |
|
2204 |
+
'@radix-ui/react-slot@1.1.0(@types/react@18.2.37)(react@18.2.0)':
|
2205 |
+
dependencies:
|
2206 |
+
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2207 |
+
react: 18.2.0
|
2208 |
+
optionalDependencies:
|
2209 |
+
'@types/react': 18.2.37
|
2210 |
+
|
2211 |
+
'@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.2.37)(react@18.2.0)':
|
2212 |
+
dependencies:
|
2213 |
+
react: 18.2.0
|
2214 |
+
optionalDependencies:
|
2215 |
+
'@types/react': 18.2.37
|
2216 |
+
|
2217 |
+
'@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.2.37)(react@18.2.0)':
|
2218 |
+
dependencies:
|
2219 |
+
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2220 |
+
react: 18.2.0
|
2221 |
+
optionalDependencies:
|
2222 |
+
'@types/react': 18.2.37
|
2223 |
+
|
2224 |
+
'@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.2.37)(react@18.2.0)':
|
2225 |
+
dependencies:
|
2226 |
+
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2227 |
+
react: 18.2.0
|
2228 |
+
optionalDependencies:
|
2229 |
+
'@types/react': 18.2.37
|
2230 |
+
|
2231 |
+
'@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.2.37)(react@18.2.0)':
|
2232 |
+
dependencies:
|
2233 |
+
react: 18.2.0
|
2234 |
+
optionalDependencies:
|
2235 |
+
'@types/react': 18.2.37
|
2236 |
+
|
2237 |
+
'@radix-ui/react-use-previous@1.1.0(@types/react@18.2.37)(react@18.2.0)':
|
2238 |
+
dependencies:
|
2239 |
+
react: 18.2.0
|
2240 |
+
optionalDependencies:
|
2241 |
+
'@types/react': 18.2.37
|
2242 |
+
|
2243 |
+
'@radix-ui/react-use-rect@1.1.0(@types/react@18.2.37)(react@18.2.0)':
|
2244 |
+
dependencies:
|
2245 |
+
'@radix-ui/rect': 1.1.0
|
2246 |
+
react: 18.2.0
|
2247 |
+
optionalDependencies:
|
2248 |
+
'@types/react': 18.2.37
|
2249 |
+
|
2250 |
+
'@radix-ui/react-use-size@1.1.0(@types/react@18.2.37)(react@18.2.0)':
|
2251 |
+
dependencies:
|
2252 |
+
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.37)(react@18.2.0)
|
2253 |
+
react: 18.2.0
|
2254 |
+
optionalDependencies:
|
2255 |
+
'@types/react': 18.2.37
|
2256 |
+
|
2257 |
+
'@radix-ui/react-visually-hidden@1.1.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
2258 |
+
dependencies:
|
2259 |
+
'@radix-ui/react-primitive': 2.0.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
2260 |
+
react: 18.2.0
|
2261 |
+
react-dom: 18.2.0(react@18.2.0)
|
2262 |
+
optionalDependencies:
|
2263 |
+
'@types/react': 18.2.37
|
2264 |
+
|
2265 |
+
'@radix-ui/rect@1.1.0': {}
|
2266 |
+
|
2267 |
'@remix-run/router@1.20.0': {}
|
2268 |
|
2269 |
'@rollup/rollup-android-arm-eabi@4.25.0':
|
|
|
2412 |
|
2413 |
argparse@2.0.1: {}
|
2414 |
|
2415 |
+
aria-hidden@1.2.4:
|
2416 |
+
dependencies:
|
2417 |
+
tslib: 2.8.1
|
2418 |
+
|
2419 |
autoprefixer@10.4.16(postcss@8.4.31):
|
2420 |
dependencies:
|
2421 |
browserslist: 4.22.1
|
|
|
2519 |
|
2520 |
deep-is@0.1.4: {}
|
2521 |
|
2522 |
+
detect-node-es@1.1.0: {}
|
2523 |
+
|
2524 |
didyoumean@1.2.2: {}
|
2525 |
|
2526 |
dlv@1.1.3: {}
|
|
|
2722 |
|
2723 |
gensync@1.0.0-beta.2: {}
|
2724 |
|
2725 |
+
get-nonce@1.0.1: {}
|
2726 |
+
|
2727 |
glob-parent@5.1.2:
|
2728 |
dependencies:
|
2729 |
is-glob: 4.0.3
|
|
|
2782 |
|
2783 |
inherits@2.0.4: {}
|
2784 |
|
2785 |
+
invariant@2.2.4:
|
2786 |
+
dependencies:
|
2787 |
+
loose-envify: 1.4.0
|
2788 |
+
|
2789 |
is-binary-path@2.1.0:
|
2790 |
dependencies:
|
2791 |
binary-extensions: 2.2.0
|
|
|
2999 |
|
3000 |
react-refresh@0.14.2: {}
|
3001 |
|
3002 |
+
react-remove-scroll-bar@2.3.6(@types/react@18.2.37)(react@18.2.0):
|
3003 |
+
dependencies:
|
3004 |
+
react: 18.2.0
|
3005 |
+
react-style-singleton: 2.2.1(@types/react@18.2.37)(react@18.2.0)
|
3006 |
+
tslib: 2.8.1
|
3007 |
+
optionalDependencies:
|
3008 |
+
'@types/react': 18.2.37
|
3009 |
+
|
3010 |
+
react-remove-scroll@2.6.0(@types/react@18.2.37)(react@18.2.0):
|
3011 |
+
dependencies:
|
3012 |
+
react: 18.2.0
|
3013 |
+
react-remove-scroll-bar: 2.3.6(@types/react@18.2.37)(react@18.2.0)
|
3014 |
+
react-style-singleton: 2.2.1(@types/react@18.2.37)(react@18.2.0)
|
3015 |
+
tslib: 2.8.1
|
3016 |
+
use-callback-ref: 1.3.2(@types/react@18.2.37)(react@18.2.0)
|
3017 |
+
use-sidecar: 1.1.2(@types/react@18.2.37)(react@18.2.0)
|
3018 |
+
optionalDependencies:
|
3019 |
+
'@types/react': 18.2.37
|
3020 |
+
|
3021 |
react-router-dom@6.27.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
3022 |
dependencies:
|
3023 |
'@remix-run/router': 1.20.0
|
|
|
3030 |
'@remix-run/router': 1.20.0
|
3031 |
react: 18.2.0
|
3032 |
|
3033 |
+
react-style-singleton@2.2.1(@types/react@18.2.37)(react@18.2.0):
|
3034 |
+
dependencies:
|
3035 |
+
get-nonce: 1.0.1
|
3036 |
+
invariant: 2.2.4
|
3037 |
+
react: 18.2.0
|
3038 |
+
tslib: 2.8.1
|
3039 |
+
optionalDependencies:
|
3040 |
+
'@types/react': 18.2.37
|
3041 |
+
|
3042 |
react@18.2.0:
|
3043 |
dependencies:
|
3044 |
loose-envify: 1.4.0
|
|
|
3236 |
dependencies:
|
3237 |
punycode: 2.3.1
|
3238 |
|
3239 |
+
use-callback-ref@1.3.2(@types/react@18.2.37)(react@18.2.0):
|
3240 |
+
dependencies:
|
3241 |
+
react: 18.2.0
|
3242 |
+
tslib: 2.8.1
|
3243 |
+
optionalDependencies:
|
3244 |
+
'@types/react': 18.2.37
|
3245 |
+
|
3246 |
+
use-sidecar@1.1.2(@types/react@18.2.37)(react@18.2.0):
|
3247 |
+
dependencies:
|
3248 |
+
detect-node-es: 1.1.0
|
3249 |
+
react: 18.2.0
|
3250 |
+
tslib: 2.8.1
|
3251 |
+
optionalDependencies:
|
3252 |
+
'@types/react': 18.2.37
|
3253 |
+
|
3254 |
util-deprecate@1.0.2: {}
|
3255 |
|
3256 |
uuid@9.0.1: {}
|
frontend/src/App.jsx
CHANGED
@@ -26,7 +26,7 @@ const PublicRoute = ({ children }) => {
|
|
26 |
return <div className="flex items-center justify-center h-screen">Loading...</div>;
|
27 |
}
|
28 |
|
29 |
-
return !token ? children : <Navigate to=
|
30 |
};
|
31 |
|
32 |
function AppRoutes() {
|
@@ -73,11 +73,11 @@ function AppRoutes() {
|
|
73 |
|
74 |
function App() {
|
75 |
return (
|
76 |
-
<
|
77 |
-
<
|
78 |
<AppRoutes/>
|
79 |
-
</
|
80 |
-
</
|
81 |
);
|
82 |
}
|
83 |
|
|
|
26 |
return <div className="flex items-center justify-center h-screen">Loading...</div>;
|
27 |
}
|
28 |
|
29 |
+
return !token ? children : <Navigate to="/" replace />;
|
30 |
};
|
31 |
|
32 |
function AppRoutes() {
|
|
|
73 |
|
74 |
function App() {
|
75 |
return (
|
76 |
+
<Router>
|
77 |
+
<AuthProvider>
|
78 |
<AppRoutes/>
|
79 |
+
</AuthProvider>
|
80 |
+
</Router>
|
81 |
);
|
82 |
}
|
83 |
|
frontend/src/components/Chat.jsx
CHANGED
@@ -132,26 +132,11 @@ const Chat = () => {
|
|
132 |
// Rest of your component (file upload handler, UI rendering, etc.)
|
133 |
return (
|
134 |
<div className="flex flex-col h-screen bg-gray-100">
|
135 |
-
|
136 |
-
<h1 className="text-xl font-bold">Chat Interface</h1>
|
137 |
-
<div className="flex items-center gap-4">
|
138 |
-
{isReconnecting && <span className="text-yellow-500">Reconnecting...</span>}
|
139 |
-
<span className={`h-3 w-3 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`}></span>
|
140 |
-
<button
|
141 |
-
onClick={() => {
|
142 |
-
logout();
|
143 |
-
navigate('/login');
|
144 |
-
}}
|
145 |
-
className="px-4 py-2 text-white bg-red-500 rounded hover:bg-red-600"
|
146 |
-
>
|
147 |
-
Logout
|
148 |
-
</button>
|
149 |
-
</div>
|
150 |
-
</div>
|
151 |
|
152 |
{/* Messages area */}
|
153 |
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
154 |
-
|
155 |
{(messages || []).map((message, index) => (
|
156 |
<div
|
157 |
key={index}
|
@@ -185,6 +170,17 @@ const Chat = () => {
|
|
185 |
>
|
186 |
Send
|
187 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
188 |
</form>
|
189 |
</div>
|
190 |
</div>
|
|
|
132 |
// Rest of your component (file upload handler, UI rendering, etc.)
|
133 |
return (
|
134 |
<div className="flex flex-col h-screen bg-gray-100">
|
135 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
136 |
|
137 |
{/* Messages area */}
|
138 |
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
139 |
+
{isReconnecting && <span className="text-yellow-500">Reconnecting...</span>}
|
140 |
{(messages || []).map((message, index) => (
|
141 |
<div
|
142 |
key={index}
|
|
|
170 |
>
|
171 |
Send
|
172 |
</button>
|
173 |
+
|
174 |
+
<button
|
175 |
+
onClick={() => {
|
176 |
+
logout();
|
177 |
+
navigate('/login');
|
178 |
+
}}
|
179 |
+
className="px-4 py-2 text-white bg-red-500 rounded hover:bg-red-600"
|
180 |
+
>
|
181 |
+
Logout
|
182 |
+
</button>
|
183 |
+
<span className={`h-3 w-3 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`}></span>
|
184 |
</form>
|
185 |
</div>
|
186 |
</div>
|
frontend/src/components/ChatApi.jsx
ADDED
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState, useEffect, useRef } from 'react';
|
2 |
+
import { useAuth } from '../services/AuthContext';
|
3 |
+
import { useNavigate } from 'react-router-dom';
|
4 |
+
|
5 |
+
const processMessages = (messageObject) => {
|
6 |
+
return (previousMessages) => {
|
7 |
+
if (previousMessages.length > 0 && previousMessages[previousMessages.length - 1].sender === messageObject.sender) {
|
8 |
+
const newMessage = {text:previousMessages[previousMessages.length - 1].text+ ' ' + messageObject.text, sender: messageObject.sender};
|
9 |
+
return [...previousMessages.slice(0, -1), newMessage];
|
10 |
+
} else {
|
11 |
+
return [...previousMessages, messageObject];
|
12 |
+
}
|
13 |
+
}
|
14 |
+
}
|
15 |
+
|
16 |
+
const ChatApi = () => {
|
17 |
+
const { token, logout } = useAuth();
|
18 |
+
const navigate = useNavigate();
|
19 |
+
const [messages, setMessages] = useState([]);
|
20 |
+
const [input, setInput] = useState('');
|
21 |
+
|
22 |
+
|
23 |
+
const sendMessage = (e) => {
|
24 |
+
e.preventDefault();
|
25 |
+
if (!input.trim()) return;
|
26 |
+
|
27 |
+
const message = {
|
28 |
+
type: 'message',
|
29 |
+
content: input
|
30 |
+
};
|
31 |
+
|
32 |
+
setMessages(processMessages({
|
33 |
+
text: input,
|
34 |
+
sender: 'user'
|
35 |
+
}));
|
36 |
+
fetch('http://localhost:8000/api/message', {
|
37 |
+
method: 'POST',
|
38 |
+
headers: {
|
39 |
+
'Authorization': `Bearer ${token}`,
|
40 |
+
'Content-Type': 'application/json',
|
41 |
+
'Accept': 'application/json'
|
42 |
+
},
|
43 |
+
body: JSON.stringify({
|
44 |
+
message: input
|
45 |
+
})
|
46 |
+
}).then(response => response.json()).then(data => {
|
47 |
+
setMessages(processMessages({
|
48 |
+
text: data.message?.content,
|
49 |
+
sender: 'ai'
|
50 |
+
}));
|
51 |
+
});
|
52 |
+
setInput('');
|
53 |
+
};
|
54 |
+
|
55 |
+
// Rest of your component (file upload handler, UI rendering, etc.)
|
56 |
+
return (
|
57 |
+
<div className="flex flex-col bg-gray-100 h-[90vh]">
|
58 |
+
{/* Messages area */}
|
59 |
+
<div className="flex-1 overflow-y-auto p-4 space-y-4" >
|
60 |
+
{(messages || []).map((message, index) => (
|
61 |
+
<div
|
62 |
+
key={index}
|
63 |
+
className={`p-3 rounded-lg max-w-[80%] ${message.sender === 'user'
|
64 |
+
? 'ml-auto bg-blue-500 text-white'
|
65 |
+
: message.sender === 'ai'
|
66 |
+
? 'bg-gray-200'
|
67 |
+
: 'bg-yellow-100 mx-auto'
|
68 |
+
}`}
|
69 |
+
>
|
70 |
+
{message.text}
|
71 |
+
</div>
|
72 |
+
))}
|
73 |
+
</div>
|
74 |
+
|
75 |
+
{/* Input area */}
|
76 |
+
<div className="p-4 bg-white border-t">
|
77 |
+
<form onSubmit={sendMessage} className="flex gap-4">
|
78 |
+
<input
|
79 |
+
type="text"
|
80 |
+
value={input}
|
81 |
+
onChange={(e) => setInput(e.target.value)}
|
82 |
+
placeholder="Type your message..."
|
83 |
+
className="flex-1 p-2 border rounded"
|
84 |
+
/>
|
85 |
+
<button
|
86 |
+
type="submit"
|
87 |
+
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-400"
|
88 |
+
>
|
89 |
+
Send
|
90 |
+
</button>
|
91 |
+
|
92 |
+
<button
|
93 |
+
onClick={() => {
|
94 |
+
logout();
|
95 |
+
navigate('/login');
|
96 |
+
}}
|
97 |
+
className="px-4 py-2 text-white bg-red-500 rounded hover:bg-red-600"
|
98 |
+
>
|
99 |
+
Logout
|
100 |
+
</button>
|
101 |
+
</form>
|
102 |
+
</div>
|
103 |
+
</div>
|
104 |
+
);
|
105 |
+
};
|
106 |
+
|
107 |
+
export default ChatApi;
|
frontend/src/components/Opportunities.jsx
CHANGED
@@ -1,14 +1,17 @@
|
|
1 |
import { useEffect, useState } from 'react';
|
2 |
import Upload from './Upload';
|
3 |
-
import
|
4 |
-
|
5 |
-
|
|
|
6 |
|
7 |
const Opportunities = () => {
|
8 |
|
9 |
-
const [token, setToken] = useState(storedToken);
|
10 |
-
const [isPopupOpen, setIsPopupOpen] = useState(false); //form popup
|
11 |
const [opportunities, setOpportunities] = useState([]);
|
|
|
|
|
|
|
|
|
12 |
useEffect(() => {
|
13 |
const storedToken = localStorage.getItem('token');
|
14 |
console.log('storedToken*******', storedToken)
|
@@ -21,18 +24,12 @@ const Opportunities = () => {
|
|
21 |
}
|
22 |
}).then(response => response.json())
|
23 |
.then(data => {
|
24 |
-
/*if (!data.success) {
|
25 |
-
handleLogout();
|
26 |
-
return
|
27 |
-
}*/
|
28 |
-
console.log('data*******', data, !data.records || data.records.length === 0)
|
29 |
-
if (!data.records || data.records.length === 0) {
|
30 |
-
setIsPopupOpen(true);
|
31 |
-
} else {
|
32 |
-
console.log('data.records*******', data.records)
|
33 |
-
setOpportunities(data.records);
|
34 |
-
}
|
35 |
|
|
|
|
|
|
|
|
|
|
|
36 |
setToken(storedToken);
|
37 |
})
|
38 |
console.log('storedToken', storedToken)
|
@@ -43,11 +40,45 @@ const Opportunities = () => {
|
|
43 |
location = '/'
|
44 |
};
|
45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
return (
|
47 |
<>
|
48 |
-
<
|
49 |
-
|
50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 flex flex-col">
|
52 |
<div className="flex-1 overflow-auto p-6">
|
53 |
<h2 style={{ 'fontSize': 'revert' }}>Opportunities</h2>
|
@@ -71,42 +102,10 @@ const Opportunities = () => {
|
|
71 |
<button onClick={handleLogout} className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-400">Logout</button>
|
72 |
</div>
|
73 |
</div>
|
74 |
-
</div
|
75 |
</>
|
76 |
)
|
77 |
}
|
78 |
|
79 |
-
const Popup = ({ isOpen, onClose, title, children, token }) => {
|
80 |
-
const validate = () => {
|
81 |
-
//add validation of data
|
82 |
-
window.location.reload();
|
83 |
-
}
|
84 |
-
if (!isOpen) return null;
|
85 |
-
|
86 |
-
return (
|
87 |
-
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
88 |
-
{/* Overlay */}
|
89 |
-
<div
|
90 |
-
className="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
|
91 |
-
/>
|
92 |
-
|
93 |
-
{/* Popup Content */}
|
94 |
-
<div className="relative z-50 w-full max-w-lg bg-white rounded-lg shadow-xl">
|
95 |
-
{/* Header */}
|
96 |
-
<div className="flex items-center justify-between p-4 border-b">
|
97 |
-
<h2 className="text-xl font-semibold">{title}</h2>
|
98 |
-
</div>
|
99 |
-
|
100 |
-
{/* Body */}
|
101 |
-
<div className="p-4">
|
102 |
-
{children}
|
103 |
-
</div>
|
104 |
-
<div className="flex items-center space-x-2" style={{ 'margin': '10px' }}>
|
105 |
-
<Upload token={token} validate={validate} />
|
106 |
-
</div>
|
107 |
-
</div>
|
108 |
-
</div>
|
109 |
-
);
|
110 |
-
};
|
111 |
|
112 |
export default Opportunities;
|
|
|
1 |
import { useEffect, useState } from 'react';
|
2 |
import Upload from './Upload';
|
3 |
+
import ChatApi from './ChatApi';
|
4 |
+
import TwoColumnLayout from './layout/TwoColumn';
|
5 |
+
import OpportunityForm from './OpportunityForm';
|
6 |
+
import { Button } from './ui/button';
|
7 |
|
8 |
const Opportunities = () => {
|
9 |
|
|
|
|
|
10 |
const [opportunities, setOpportunities] = useState([]);
|
11 |
+
const [selectedOpportunity, setSelectedOpportunity] = useState(null);
|
12 |
+
const [token, setToken] = useState(null);
|
13 |
+
const [research, setResearch] = useState(null);
|
14 |
+
|
15 |
useEffect(() => {
|
16 |
const storedToken = localStorage.getItem('token');
|
17 |
console.log('storedToken*******', storedToken)
|
|
|
24 |
}
|
25 |
}).then(response => response.json())
|
26 |
.then(data => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
|
28 |
+
console.log('data.records*******', data.success && data.data.records?.length > 0)
|
29 |
+
if (data.success && data.data.records?.length > 0) {
|
30 |
+
setOpportunities(data.data.records);
|
31 |
+
}
|
32 |
+
|
33 |
setToken(storedToken);
|
34 |
})
|
35 |
console.log('storedToken', storedToken)
|
|
|
40 |
location = '/'
|
41 |
};
|
42 |
|
43 |
+
const customerResearch = (opportunity) => {
|
44 |
+
fetch('/api/customer_insights', {
|
45 |
+
method: 'POST',
|
46 |
+
headers: {
|
47 |
+
'Authorization': `Bearer ${token}`,
|
48 |
+
'Content-Type': 'application/json'
|
49 |
+
},
|
50 |
+
body: JSON.stringify({
|
51 |
+
message: opportunity['opportunityName']
|
52 |
+
})
|
53 |
+
}).then(response => response.json())
|
54 |
+
.then(data => {
|
55 |
+
console.log('data*******', data)
|
56 |
+
})
|
57 |
+
}
|
58 |
+
|
59 |
return (
|
60 |
<>
|
61 |
+
<TwoColumnLayout tabs={['Opportunities', 'New Opportunity']}>
|
62 |
+
<ChatApi />
|
63 |
+
<div className="flex flex-col h-[calc(89vh-7px)]">
|
64 |
+
{opportunities.map((opportunity, id) => (
|
65 |
+
<Button key={id} onClick={() => setSelectedOpportunity(opportunity)}>{opportunity.opportunityName}</Button>
|
66 |
+
))}
|
67 |
+
<div className="flex-1 overflow-y-auto p-4 space-y-4 h-[80vh]">
|
68 |
+
{Object.keys(selectedOpportunity || {}).map((key, index) => (
|
69 |
+
<div key={key+'-'+index}>{key}: {selectedOpportunity[key]}</div>
|
70 |
+
))}
|
71 |
+
{research && <div>{research}</div>}
|
72 |
+
</div>
|
73 |
+
|
74 |
+
<div className="p-4 bg-white border-t">
|
75 |
+
<Button style={{'marginRight':'10px'}} onClick={() => {customerResearch(selectedOpportunity); setResearch(null)}}>Prism</Button>
|
76 |
+
<Button>Scout</Button>
|
77 |
+
</div>
|
78 |
+
</div>
|
79 |
+
<OpportunityForm/>
|
80 |
+
</TwoColumnLayout>
|
81 |
+
{/*
|
82 |
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 flex flex-col">
|
83 |
<div className="flex-1 overflow-auto p-6">
|
84 |
<h2 style={{ 'fontSize': 'revert' }}>Opportunities</h2>
|
|
|
102 |
<button onClick={handleLogout} className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-400">Logout</button>
|
103 |
</div>
|
104 |
</div>
|
105 |
+
</div>*/}
|
106 |
</>
|
107 |
)
|
108 |
}
|
109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
110 |
|
111 |
export default Opportunities;
|
frontend/src/components/OpportunityForm.jsx
ADDED
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState } from 'react';
|
2 |
+
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
3 |
+
import { Input } from './ui/input';
|
4 |
+
import { Button } from './ui/button';
|
5 |
+
import {
|
6 |
+
Select,
|
7 |
+
SelectContent,
|
8 |
+
SelectItem,
|
9 |
+
SelectTrigger,
|
10 |
+
SelectValue,
|
11 |
+
} from './ui/select';
|
12 |
+
import { Textarea } from './ui/textarea';
|
13 |
+
import { AlertCircle } from 'lucide-react';
|
14 |
+
import { useAuth } from '../services/AuthContext';
|
15 |
+
|
16 |
+
const OpportunityForm = () => {
|
17 |
+
// Generate a simple ID using timestamp and random number
|
18 |
+
const generateId = () => `opp-${crypto.randomUUID()}`;
|
19 |
+
|
20 |
+
|
21 |
+
const initialFormState = {
|
22 |
+
opportunityId: generateId(),
|
23 |
+
customerName: '',
|
24 |
+
opportunityName: '',
|
25 |
+
opportunityState: '',
|
26 |
+
opportunityDescription: '',
|
27 |
+
opportunityValue: '',
|
28 |
+
closeDate: '',
|
29 |
+
customerContact: '',
|
30 |
+
customerContactRole: '',
|
31 |
+
activity: '',
|
32 |
+
nextSteps: ''
|
33 |
+
};
|
34 |
+
|
35 |
+
const [formData, setFormData] = useState(initialFormState);
|
36 |
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
37 |
+
const [errors, setErrors] = useState({});
|
38 |
+
const { token } = useAuth();
|
39 |
+
|
40 |
+
const handleChange = (e) => {
|
41 |
+
const { name, value } = e.target;
|
42 |
+
setFormData(prev => ({
|
43 |
+
...prev,
|
44 |
+
[name]: value
|
45 |
+
}));
|
46 |
+
// Clear error when field is modified
|
47 |
+
if (errors[name]) {
|
48 |
+
setErrors(prev => ({ ...prev, [name]: '' }));
|
49 |
+
}
|
50 |
+
};
|
51 |
+
|
52 |
+
const handleSelectChange = (value) => {
|
53 |
+
setFormData(prev => ({
|
54 |
+
...prev,
|
55 |
+
opportunityState: value
|
56 |
+
}));
|
57 |
+
if (errors.opportunityState) {
|
58 |
+
setErrors(prev => ({ ...prev, opportunityState: '' }));
|
59 |
+
}
|
60 |
+
};
|
61 |
+
|
62 |
+
const validateForm = () => {
|
63 |
+
const newErrors = {};
|
64 |
+
if (!formData.customerName.trim()) newErrors.customerName = 'Customer name is required';
|
65 |
+
if (!formData.opportunityName.trim()) newErrors.opportunityName = 'Opportunity name is required';
|
66 |
+
if (!formData.opportunityState) newErrors.opportunityState = 'Opportunity state is required';
|
67 |
+
if (!formData.opportunityDescription.trim()) newErrors.opportunityDescription = 'Description is required';
|
68 |
+
if (!formData.opportunityValue) newErrors.opportunityValue = 'Value is required';
|
69 |
+
if (!formData.closeDate) newErrors.closeDate = 'Close date is required';
|
70 |
+
if (!formData.customerContact.trim()) newErrors.customerContact = 'Customer contact is required';
|
71 |
+
if (!formData.customerContactRole.trim()) newErrors.customerContactRole = 'Contact role is required';
|
72 |
+
if (!formData.activity.trim()) newErrors.activity = 'Activity is required';
|
73 |
+
if (!formData.nextSteps.trim()) newErrors.nextSteps = 'Next steps are required';
|
74 |
+
|
75 |
+
setErrors(newErrors);
|
76 |
+
return Object.keys(newErrors).length === 0;
|
77 |
+
};
|
78 |
+
|
79 |
+
const handleSubmit = async (e) => {
|
80 |
+
e.preventDefault();
|
81 |
+
|
82 |
+
if (!validateForm()) {
|
83 |
+
return;
|
84 |
+
}
|
85 |
+
|
86 |
+
setIsSubmitting(true);
|
87 |
+
|
88 |
+
try {
|
89 |
+
const response = await fetch('/api/save_opportunity', {
|
90 |
+
method: 'POST',
|
91 |
+
headers: {
|
92 |
+
'Content-Type': 'application/json',
|
93 |
+
'Authorization': `Bearer ${token}`,
|
94 |
+
},
|
95 |
+
body: JSON.stringify(formData),
|
96 |
+
});
|
97 |
+
|
98 |
+
if (!response.ok) {
|
99 |
+
throw new Error('Submission failed');
|
100 |
+
}
|
101 |
+
|
102 |
+
handleClear();
|
103 |
+
alert('Form submitted successfully!');
|
104 |
+
} catch (error) {
|
105 |
+
alert('Error submitting form: ' + error.message);
|
106 |
+
} finally {
|
107 |
+
setIsSubmitting(false);
|
108 |
+
}
|
109 |
+
};
|
110 |
+
|
111 |
+
const handleClear = () => {
|
112 |
+
setFormData({
|
113 |
+
...initialFormState,
|
114 |
+
opportunityId: generateId()
|
115 |
+
});
|
116 |
+
setErrors({});
|
117 |
+
};
|
118 |
+
|
119 |
+
const FormLabel = ({ children, required }) => (
|
120 |
+
<div className="flex gap-1 text-sm font-medium leading-none mb-2">
|
121 |
+
{children}
|
122 |
+
{required && <span className="text-red-500">*</span>}
|
123 |
+
</div>
|
124 |
+
);
|
125 |
+
|
126 |
+
const ErrorMessage = ({ message }) => message ? (
|
127 |
+
<div className="flex items-center gap-1 text-red-500 text-sm mt-1">
|
128 |
+
<AlertCircle className="w-4 h-4" />
|
129 |
+
<span>{message}</span>
|
130 |
+
</div>
|
131 |
+
) : null;
|
132 |
+
|
133 |
+
return (
|
134 |
+
<Card className="w-full max-w-2xl mx-auto">
|
135 |
+
<CardHeader>
|
136 |
+
<CardTitle>New Opportunity</CardTitle>
|
137 |
+
</CardHeader>
|
138 |
+
<CardContent>
|
139 |
+
<form onSubmit={handleSubmit} className="space-y-4">
|
140 |
+
<input
|
141 |
+
type="hidden"
|
142 |
+
name="opportunityId"
|
143 |
+
value={formData.opportunityId}
|
144 |
+
/>
|
145 |
+
|
146 |
+
<div>
|
147 |
+
<FormLabel required>Customer Name</FormLabel>
|
148 |
+
<Input
|
149 |
+
name="customerName"
|
150 |
+
value={formData.customerName}
|
151 |
+
onChange={handleChange}
|
152 |
+
className={errors.customerName ? 'border-red-500' : ''}
|
153 |
+
/>
|
154 |
+
<ErrorMessage message={errors.customerName} />
|
155 |
+
</div>
|
156 |
+
|
157 |
+
<div>
|
158 |
+
<FormLabel required>Opportunity Name</FormLabel>
|
159 |
+
<Input
|
160 |
+
name="opportunityName"
|
161 |
+
value={formData.opportunityName}
|
162 |
+
onChange={handleChange}
|
163 |
+
className={errors.opportunityName ? 'border-red-500' : ''}
|
164 |
+
/>
|
165 |
+
<ErrorMessage message={errors.opportunityName} />
|
166 |
+
</div>
|
167 |
+
|
168 |
+
<div>
|
169 |
+
<FormLabel required>Opportunity State</FormLabel>
|
170 |
+
<Select
|
171 |
+
value={formData.opportunityState}
|
172 |
+
onValueChange={handleSelectChange}
|
173 |
+
>
|
174 |
+
<SelectTrigger className={errors.opportunityState ? 'border-red-500' : ''}>
|
175 |
+
<SelectValue placeholder="Select state" />
|
176 |
+
</SelectTrigger>
|
177 |
+
<SelectContent>
|
178 |
+
<SelectItem value="proposal">Proposal</SelectItem>
|
179 |
+
<SelectItem value="negotiation">Negotiation</SelectItem>
|
180 |
+
</SelectContent>
|
181 |
+
</Select>
|
182 |
+
<ErrorMessage message={errors.opportunityState} />
|
183 |
+
</div>
|
184 |
+
|
185 |
+
<div>
|
186 |
+
<FormLabel required>Opportunity Description</FormLabel>
|
187 |
+
<Textarea
|
188 |
+
name="opportunityDescription"
|
189 |
+
value={formData.opportunityDescription}
|
190 |
+
onChange={handleChange}
|
191 |
+
className={`h-24 ${errors.opportunityDescription ? 'border-red-500' : ''}`}
|
192 |
+
/>
|
193 |
+
<ErrorMessage message={errors.opportunityDescription} />
|
194 |
+
</div>
|
195 |
+
|
196 |
+
<div>
|
197 |
+
<FormLabel required>Opportunity Value (USD)</FormLabel>
|
198 |
+
<Input
|
199 |
+
type="number"
|
200 |
+
name="opportunityValue"
|
201 |
+
value={formData.opportunityValue}
|
202 |
+
onChange={handleChange}
|
203 |
+
min="0"
|
204 |
+
step="0.01"
|
205 |
+
className={errors.opportunityValue ? 'border-red-500' : ''}
|
206 |
+
/>
|
207 |
+
<ErrorMessage message={errors.opportunityValue} />
|
208 |
+
</div>
|
209 |
+
|
210 |
+
<div>
|
211 |
+
<FormLabel required>Close Date</FormLabel>
|
212 |
+
<Input
|
213 |
+
type="date"
|
214 |
+
name="closeDate"
|
215 |
+
value={formData.closeDate}
|
216 |
+
onChange={handleChange}
|
217 |
+
className={errors.closeDate ? 'border-red-500' : ''}
|
218 |
+
/>
|
219 |
+
<ErrorMessage message={errors.closeDate} />
|
220 |
+
</div>
|
221 |
+
|
222 |
+
<div>
|
223 |
+
<FormLabel required>Customer Contact</FormLabel>
|
224 |
+
<Input
|
225 |
+
name="customerContact"
|
226 |
+
value={formData.customerContact}
|
227 |
+
onChange={handleChange}
|
228 |
+
className={errors.customerContact ? 'border-red-500' : ''}
|
229 |
+
/>
|
230 |
+
<ErrorMessage message={errors.customerContact} />
|
231 |
+
</div>
|
232 |
+
|
233 |
+
<div>
|
234 |
+
<FormLabel required>Customer Contact Role</FormLabel>
|
235 |
+
<Input
|
236 |
+
name="customerContactRole"
|
237 |
+
value={formData.customerContactRole}
|
238 |
+
onChange={handleChange}
|
239 |
+
className={errors.customerContactRole ? 'border-red-500' : ''}
|
240 |
+
/>
|
241 |
+
<ErrorMessage message={errors.customerContactRole} />
|
242 |
+
</div>
|
243 |
+
|
244 |
+
<div>
|
245 |
+
<FormLabel required>Activity</FormLabel>
|
246 |
+
<Textarea
|
247 |
+
name="activity"
|
248 |
+
value={formData.activity}
|
249 |
+
onChange={handleChange}
|
250 |
+
className={`h-24 ${errors.activity ? 'border-red-500' : ''}`}
|
251 |
+
/>
|
252 |
+
<ErrorMessage message={errors.activity} />
|
253 |
+
</div>
|
254 |
+
|
255 |
+
<div>
|
256 |
+
<FormLabel required>Next Steps</FormLabel>
|
257 |
+
<Textarea
|
258 |
+
name="nextSteps"
|
259 |
+
value={formData.nextSteps}
|
260 |
+
onChange={handleChange}
|
261 |
+
className={`h-24 ${errors.nextSteps ? 'border-red-500' : ''}`}
|
262 |
+
/>
|
263 |
+
<ErrorMessage message={errors.nextSteps} />
|
264 |
+
</div>
|
265 |
+
|
266 |
+
<div className="flex gap-4 justify-end">
|
267 |
+
<Button
|
268 |
+
type="button"
|
269 |
+
variant="outline"
|
270 |
+
onClick={handleClear}
|
271 |
+
disabled={isSubmitting}
|
272 |
+
>
|
273 |
+
Clear
|
274 |
+
</Button>
|
275 |
+
<Button
|
276 |
+
type="submit"
|
277 |
+
disabled={isSubmitting}
|
278 |
+
>
|
279 |
+
{isSubmitting ? 'Submitting...' : 'Submit'}
|
280 |
+
</Button>
|
281 |
+
</div>
|
282 |
+
</form>
|
283 |
+
</CardContent>
|
284 |
+
</Card>
|
285 |
+
);
|
286 |
+
};
|
287 |
+
|
288 |
+
export default OpportunityForm;
|
frontend/src/components/layout/TwoColumn.jsx
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { Card, CardHeader, CardContent, CardTitle } from '../ui/card';
|
3 |
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
4 |
+
|
5 |
+
const TwoColumnLayout = ({children, tabs}) => {
|
6 |
+
const left = React.Children.toArray(children)[0];
|
7 |
+
const right = React.Children.toArray(children).slice(1);
|
8 |
+
|
9 |
+
return (
|
10 |
+
<div className="h-screen bg-gray-50">
|
11 |
+
<div className="mx-auto p-2 h-full max-w-[1800px]">
|
12 |
+
<div className="flex flex-col md:flex-row gap-4 h-full">
|
13 |
+
{/* Left Column */}
|
14 |
+
<div className="w-full md:w-[37%] h-full">
|
15 |
+
<Card className="flex flex-col h-full">
|
16 |
+
<CardHeader className="pb-2 shrink-0">
|
17 |
+
<CardTitle>Messages</CardTitle>
|
18 |
+
</CardHeader>
|
19 |
+
<CardContent className="flex-1 min-h-0">
|
20 |
+
<div className="h-full">
|
21 |
+
{left}
|
22 |
+
</div>
|
23 |
+
</CardContent>
|
24 |
+
</Card>
|
25 |
+
</div>
|
26 |
+
|
27 |
+
{/* Right Column */}
|
28 |
+
<div className="w-full md:w-[62%] h-full">
|
29 |
+
<Card className="flex flex-col h-full">
|
30 |
+
<CardContent className="flex flex-col h-full p-0">
|
31 |
+
<Tabs defaultValue={tabs[0]} className="flex flex-col h-full">
|
32 |
+
<TabsList className={`grid w-full grid-cols-3 shrink-0`}>
|
33 |
+
{tabs.map(t => (
|
34 |
+
<TabsTrigger key={t} value={t}>{t}</TabsTrigger>
|
35 |
+
))}
|
36 |
+
</TabsList>
|
37 |
+
|
38 |
+
{tabs.map((tab, index) => (
|
39 |
+
<TabsContent
|
40 |
+
key={tab}
|
41 |
+
value={tab}
|
42 |
+
className="flex-1 overflow-auto min-h-0 p-6"
|
43 |
+
>
|
44 |
+
<div className="h-full flex flex-col">
|
45 |
+
<div className="flex-1 overflow-auto">
|
46 |
+
{right[index]}
|
47 |
+
</div>
|
48 |
+
</div>
|
49 |
+
</TabsContent>
|
50 |
+
))}
|
51 |
+
</Tabs>
|
52 |
+
</CardContent>
|
53 |
+
</Card>
|
54 |
+
</div>
|
55 |
+
</div>
|
56 |
+
</div>
|
57 |
+
</div>
|
58 |
+
);
|
59 |
+
};
|
60 |
+
|
61 |
+
export default TwoColumnLayout;
|
frontend/src/components/ui/button.jsx
CHANGED
@@ -1,7 +1,6 @@
|
|
1 |
import * as React from "react"
|
2 |
import { Slot } from "@radix-ui/react-slot"
|
3 |
import { cva } from "class-variance-authority"
|
4 |
-
|
5 |
import { cn } from "@/lib/utils"
|
6 |
|
7 |
const buttonVariants = cva(
|
@@ -33,7 +32,8 @@ const buttonVariants = cva(
|
|
33 |
}
|
34 |
)
|
35 |
|
36 |
-
const Button =(
|
|
|
37 |
const Comp = asChild ? Slot : "button"
|
38 |
return (
|
39 |
<Comp
|
@@ -43,6 +43,7 @@ const Button =({ className, variant, size, asChild = false, ...props }, ref) =>
|
|
43 |
/>
|
44 |
)
|
45 |
}
|
|
|
46 |
|
47 |
Button.displayName = "Button"
|
48 |
|
|
|
1 |
import * as React from "react"
|
2 |
import { Slot } from "@radix-ui/react-slot"
|
3 |
import { cva } from "class-variance-authority"
|
|
|
4 |
import { cn } from "@/lib/utils"
|
5 |
|
6 |
const buttonVariants = cva(
|
|
|
32 |
}
|
33 |
)
|
34 |
|
35 |
+
const Button = React.forwardRef(
|
36 |
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
37 |
const Comp = asChild ? Slot : "button"
|
38 |
return (
|
39 |
<Comp
|
|
|
43 |
/>
|
44 |
)
|
45 |
}
|
46 |
+
)
|
47 |
|
48 |
Button.displayName = "Button"
|
49 |
|
frontend/src/components/ui/card.jsx
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from "react"
|
2 |
+
import { cn } from "../../lib/utils"
|
3 |
+
|
4 |
+
const Card = React.forwardRef(({ className, ...props }, ref) => (
|
5 |
+
<div
|
6 |
+
ref={ref}
|
7 |
+
className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
|
8 |
+
{...props}
|
9 |
+
/>
|
10 |
+
))
|
11 |
+
Card.displayName = "Card"
|
12 |
+
|
13 |
+
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
|
14 |
+
<div
|
15 |
+
ref={ref}
|
16 |
+
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
17 |
+
{...props}
|
18 |
+
/>
|
19 |
+
))
|
20 |
+
CardHeader.displayName = "CardHeader"
|
21 |
+
|
22 |
+
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
|
23 |
+
<h3
|
24 |
+
ref={ref}
|
25 |
+
className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
|
26 |
+
{...props}
|
27 |
+
/>
|
28 |
+
))
|
29 |
+
CardTitle.displayName = "CardTitle"
|
30 |
+
|
31 |
+
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
|
32 |
+
<p
|
33 |
+
ref={ref}
|
34 |
+
className={cn("text-sm text-muted-foreground", className)}
|
35 |
+
{...props}
|
36 |
+
/>
|
37 |
+
))
|
38 |
+
CardDescription.displayName = "CardDescription"
|
39 |
+
|
40 |
+
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
|
41 |
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
42 |
+
))
|
43 |
+
CardContent.displayName = "CardContent"
|
44 |
+
|
45 |
+
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
|
46 |
+
<div
|
47 |
+
ref={ref}
|
48 |
+
className={cn("flex items-center p-6 pt-0", className)}
|
49 |
+
{...props}
|
50 |
+
/>
|
51 |
+
))
|
52 |
+
CardFooter.displayName = "CardFooter"
|
53 |
+
|
54 |
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
frontend/src/components/ui/input.jsx
CHANGED
@@ -1,8 +1,7 @@
|
|
1 |
import * as React from "react"
|
2 |
import { cn } from "@/lib/utils"
|
3 |
|
4 |
-
|
5 |
-
const Input = (
|
6 |
({ className, type, ...props }, ref) => {
|
7 |
return (
|
8 |
<input
|
|
|
1 |
import * as React from "react"
|
2 |
import { cn } from "@/lib/utils"
|
3 |
|
4 |
+
const Input = React.forwardRef(
|
|
|
5 |
({ className, type, ...props }, ref) => {
|
6 |
return (
|
7 |
<input
|
frontend/src/components/ui/select.jsx
ADDED
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
import * as SelectPrimitive from "@radix-ui/react-select"
|
3 |
+
import { Check, ChevronDown } from "lucide-react"
|
4 |
+
import { cn } from "../../lib/utils"
|
5 |
+
|
6 |
+
const Select = SelectPrimitive.Root
|
7 |
+
|
8 |
+
const SelectGroup = SelectPrimitive.Group
|
9 |
+
|
10 |
+
const SelectValue = SelectPrimitive.Value
|
11 |
+
|
12 |
+
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
|
13 |
+
<SelectPrimitive.Trigger
|
14 |
+
ref={ref}
|
15 |
+
className={cn(
|
16 |
+
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
17 |
+
className
|
18 |
+
)}
|
19 |
+
{...props}
|
20 |
+
>
|
21 |
+
{children}
|
22 |
+
<SelectPrimitive.Icon asChild>
|
23 |
+
<ChevronDown className="h-4 w-4 opacity-50" />
|
24 |
+
</SelectPrimitive.Icon>
|
25 |
+
</SelectPrimitive.Trigger>
|
26 |
+
))
|
27 |
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
28 |
+
|
29 |
+
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
|
30 |
+
<SelectPrimitive.Portal>
|
31 |
+
<SelectPrimitive.Content
|
32 |
+
ref={ref}
|
33 |
+
className={cn(
|
34 |
+
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
35 |
+
position === "popper" &&
|
36 |
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
37 |
+
className
|
38 |
+
)}
|
39 |
+
position={position}
|
40 |
+
{...props}
|
41 |
+
>
|
42 |
+
<SelectPrimitive.Viewport
|
43 |
+
className={cn(
|
44 |
+
"p-1",
|
45 |
+
position === "popper" &&
|
46 |
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
47 |
+
)}
|
48 |
+
>
|
49 |
+
{children}
|
50 |
+
</SelectPrimitive.Viewport>
|
51 |
+
</SelectPrimitive.Content>
|
52 |
+
</SelectPrimitive.Portal>
|
53 |
+
))
|
54 |
+
SelectContent.displayName = SelectPrimitive.Content.displayName
|
55 |
+
|
56 |
+
const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
|
57 |
+
<SelectPrimitive.Label
|
58 |
+
ref={ref}
|
59 |
+
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
60 |
+
{...props}
|
61 |
+
/>
|
62 |
+
))
|
63 |
+
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
64 |
+
|
65 |
+
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
|
66 |
+
<SelectPrimitive.Item
|
67 |
+
ref={ref}
|
68 |
+
className={cn(
|
69 |
+
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
70 |
+
className
|
71 |
+
)}
|
72 |
+
{...props}
|
73 |
+
>
|
74 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
75 |
+
<SelectPrimitive.ItemIndicator>
|
76 |
+
<Check className="h-4 w-4" />
|
77 |
+
</SelectPrimitive.ItemIndicator>
|
78 |
+
</span>
|
79 |
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
80 |
+
</SelectPrimitive.Item>
|
81 |
+
))
|
82 |
+
SelectItem.displayName = SelectPrimitive.Item.displayName
|
83 |
+
|
84 |
+
const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
85 |
+
<SelectPrimitive.Separator
|
86 |
+
ref={ref}
|
87 |
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
88 |
+
{...props}
|
89 |
+
/>
|
90 |
+
))
|
91 |
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
92 |
+
|
93 |
+
export {
|
94 |
+
Select,
|
95 |
+
SelectGroup,
|
96 |
+
SelectValue,
|
97 |
+
SelectTrigger,
|
98 |
+
SelectContent,
|
99 |
+
SelectLabel,
|
100 |
+
SelectItem,
|
101 |
+
SelectSeparator,
|
102 |
+
}
|
frontend/src/components/ui/tabs.jsx
ADDED
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
import { cn } from "../../lib/utils"
|
3 |
+
|
4 |
+
const TabContext = React.createContext({
|
5 |
+
selectedTab: '',
|
6 |
+
setSelectedTab: () => {}
|
7 |
+
})
|
8 |
+
|
9 |
+
const Tabs = React.forwardRef(({ defaultValue, value, onValueChange, className, children, ...props }, ref) => {
|
10 |
+
const [selectedTab, setSelectedTab] = React.useState(value || defaultValue);
|
11 |
+
|
12 |
+
React.useEffect(() => {
|
13 |
+
if (value !== undefined) {
|
14 |
+
setSelectedTab(value);
|
15 |
+
}
|
16 |
+
}, [value]);
|
17 |
+
|
18 |
+
const handleTabChange = (newValue) => {
|
19 |
+
if (value === undefined) {
|
20 |
+
setSelectedTab(newValue);
|
21 |
+
}
|
22 |
+
onValueChange?.(newValue);
|
23 |
+
};
|
24 |
+
|
25 |
+
return (
|
26 |
+
<TabContext.Provider value={{ selectedTab, setSelectedTab: handleTabChange }}>
|
27 |
+
<div ref={ref} className={cn("w-full", className)} {...props}>
|
28 |
+
{children}
|
29 |
+
</div>
|
30 |
+
</TabContext.Provider>
|
31 |
+
);
|
32 |
+
});
|
33 |
+
Tabs.displayName = "Tabs"
|
34 |
+
|
35 |
+
const TabsList = React.forwardRef(({ className, ...props }, ref) => (
|
36 |
+
<div
|
37 |
+
ref={ref}
|
38 |
+
className={cn(
|
39 |
+
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
40 |
+
className
|
41 |
+
)}
|
42 |
+
{...props}
|
43 |
+
/>
|
44 |
+
))
|
45 |
+
TabsList.displayName = "TabsList"
|
46 |
+
|
47 |
+
const TabsTrigger = React.forwardRef(({ className, value, children, ...props }, ref) => {
|
48 |
+
const { selectedTab, setSelectedTab } = React.useContext(TabContext);
|
49 |
+
const isSelected = selectedTab === value;
|
50 |
+
|
51 |
+
return (
|
52 |
+
<button
|
53 |
+
ref={ref}
|
54 |
+
type="button"
|
55 |
+
role="tab"
|
56 |
+
aria-selected={isSelected}
|
57 |
+
data-state={isSelected ? "active" : "inactive"}
|
58 |
+
onClick={() => setSelectedTab(value)}
|
59 |
+
className={cn(
|
60 |
+
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
61 |
+
isSelected && "bg-background text-foreground shadow-sm",
|
62 |
+
className
|
63 |
+
)}
|
64 |
+
{...props}
|
65 |
+
>
|
66 |
+
{children}
|
67 |
+
</button>
|
68 |
+
);
|
69 |
+
});
|
70 |
+
TabsTrigger.displayName = "TabsTrigger"
|
71 |
+
|
72 |
+
const TabsContent = React.forwardRef(({ className, value, children, ...props }, ref) => {
|
73 |
+
const { selectedTab } = React.useContext(TabContext);
|
74 |
+
const isSelected = selectedTab === value;
|
75 |
+
|
76 |
+
if (!isSelected) return null;
|
77 |
+
|
78 |
+
return (
|
79 |
+
<div
|
80 |
+
ref={ref}
|
81 |
+
role="tabpanel"
|
82 |
+
data-state={isSelected ? "active" : "inactive"}
|
83 |
+
className={cn(
|
84 |
+
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
85 |
+
className
|
86 |
+
)}
|
87 |
+
{...props}
|
88 |
+
>
|
89 |
+
{children}
|
90 |
+
</div>
|
91 |
+
);
|
92 |
+
});
|
93 |
+
TabsContent.displayName = "TabsContent"
|
94 |
+
|
95 |
+
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
frontend/src/components/ui/textarea.jsx
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
import { cn } from "../../lib/utils"
|
3 |
+
|
4 |
+
const Textarea = React.forwardRef(({ className, ...props }, ref) => {
|
5 |
+
return (
|
6 |
+
<textarea
|
7 |
+
className={cn(
|
8 |
+
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
9 |
+
className
|
10 |
+
)}
|
11 |
+
ref={ref}
|
12 |
+
{...props}
|
13 |
+
/>
|
14 |
+
)
|
15 |
+
})
|
16 |
+
Textarea.displayName = "Textarea"
|
17 |
+
|
18 |
+
export { Textarea }
|
frontend/src/lib/{utils.ts β utils.js}
RENAMED
@@ -1,6 +1,6 @@
|
|
1 |
-
import {
|
2 |
import { twMerge } from "tailwind-merge"
|
3 |
|
4 |
-
export function cn(...inputs
|
5 |
return twMerge(clsx(inputs))
|
6 |
}
|
|
|
1 |
+
import { clsx } from "clsx"
|
2 |
import { twMerge } from "tailwind-merge"
|
3 |
|
4 |
+
export function cn(...inputs) {
|
5 |
return twMerge(clsx(inputs))
|
6 |
}
|
frontend/tsconfig.json
DELETED
@@ -1,30 +0,0 @@
|
|
1 |
-
{
|
2 |
-
"compilerOptions": {
|
3 |
-
"target": "ES2020",
|
4 |
-
"useDefineForClassFields": true,
|
5 |
-
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
6 |
-
"module": "ESNext",
|
7 |
-
"skipLibCheck": true,
|
8 |
-
|
9 |
-
/* Bundler mode */
|
10 |
-
"moduleResolution": "bundler",
|
11 |
-
"allowImportingTsExtensions": true,
|
12 |
-
"resolveJsonModule": true,
|
13 |
-
"isolatedModules": true,
|
14 |
-
"noEmit": true,
|
15 |
-
"jsx": "react-jsx",
|
16 |
-
|
17 |
-
"baseUrl": ".",
|
18 |
-
"paths": {
|
19 |
-
"@/*": ["./src/*"]
|
20 |
-
},
|
21 |
-
|
22 |
-
/* Linting */
|
23 |
-
"strict": true,
|
24 |
-
"noUnusedLocals": true,
|
25 |
-
"noUnusedParameters": true,
|
26 |
-
"noFallthroughCasesInSwitch": true
|
27 |
-
},
|
28 |
-
"include": ["src"],
|
29 |
-
"references": [{ "path": "./tsconfig.node.json" }]
|
30 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/tsconfig.node.json
DELETED
@@ -1,12 +0,0 @@
|
|
1 |
-
{
|
2 |
-
"compilerOptions": {
|
3 |
-
"composite": true,
|
4 |
-
"skipLibCheck": true,
|
5 |
-
"module": "ESNext",
|
6 |
-
"moduleResolution": "bundler",
|
7 |
-
"allowSyntheticDefaultImports": true
|
8 |
-
},
|
9 |
-
"include": [
|
10 |
-
"vite.config.js"
|
11 |
-
]
|
12 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|