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/[email protected])([email protected])
|
|
@@ -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/[email protected]':
|
| 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/[email protected]':
|
| 394 |
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
|
| 395 |
peerDependencies:
|
|
@@ -399,6 +449,138 @@ packages:
|
|
| 399 |
'@types/react':
|
| 400 |
optional: true
|
| 401 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
'@radix-ui/[email protected]':
|
| 403 |
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
|
| 404 |
peerDependencies:
|
|
@@ -408,6 +590,94 @@ packages:
|
|
| 408 |
'@types/react':
|
| 409 |
optional: true
|
| 410 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
'@remix-run/[email protected]':
|
| 412 |
resolution: {integrity: sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==}
|
| 413 |
engines: {node: '>=14.0.0'}
|
|
@@ -581,6 +851,10 @@ packages:
|
|
| 581 | |
| 582 |
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
| 583 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 584 | |
| 585 |
resolution: {integrity: sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==}
|
| 586 |
engines: {node: ^10 || ^12 || >=14}
|
|
@@ -695,6 +969,9 @@ packages:
|
|
| 695 | |
| 696 |
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
| 697 |
|
|
|
|
|
|
|
|
|
|
| 698 | |
| 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 | |
| 850 |
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
| 851 |
engines: {node: '>= 6'}
|
|
@@ -900,6 +1181,9 @@ packages:
|
|
| 900 | |
| 901 |
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
| 902 |
|
|
|
|
|
|
|
|
|
|
| 903 | |
| 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 | |
| 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 | |
| 1181 |
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
| 1182 |
engines: {node: '>=0.10.0'}
|
|
@@ -1351,6 +1665,26 @@ packages:
|
|
| 1351 | |
| 1352 |
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
| 1353 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1354 | |
| 1355 |
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
| 1356 |
|
|
@@ -1631,6 +1965,23 @@ snapshots:
|
|
| 1631 |
|
| 1632 |
'@eslint/[email protected]': {}
|
| 1633 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1634 |
'@humanwhocodes/[email protected]':
|
| 1635 |
dependencies:
|
| 1636 |
'@humanwhocodes/object-schema': 2.0.1
|
|
@@ -1691,6 +2042,29 @@ snapshots:
|
|
| 1691 |
|
| 1692 |
'@pkgr/[email protected]': {}
|
| 1693 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1694 |
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
| 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/[email protected](@types/[email protected])([email protected])':
|
| 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/[email protected]': {}
|
| 1710 |
|
| 1711 |
'@rollup/[email protected]':
|
|
@@ -1854,6 +2412,10 @@ snapshots:
|
|
| 1854 |
|
| 1855 | |
| 1856 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1857 | |
| 1858 |
dependencies:
|
| 1859 |
browserslist: 4.22.1
|
|
@@ -1957,6 +2519,8 @@ snapshots:
|
|
| 1957 |
|
| 1958 | |
| 1959 |
|
|
|
|
|
|
|
| 1960 | |
| 1961 |
|
| 1962 | |
|
@@ -2158,6 +2722,8 @@ snapshots:
|
|
| 2158 |
|
| 2159 | |
| 2160 |
|
|
|
|
|
|
|
| 2161 | |
| 2162 |
dependencies:
|
| 2163 |
is-glob: 4.0.3
|
|
@@ -2216,6 +2782,10 @@ snapshots:
|
|
| 2216 |
|
| 2217 | |
| 2218 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2219 | |
| 2220 |
dependencies:
|
| 2221 |
binary-extensions: 2.2.0
|
|
@@ -2429,6 +2999,25 @@ snapshots:
|
|
| 2429 |
|
| 2430 | |
| 2431 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2432 | |
| 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 | |
| 2445 |
dependencies:
|
| 2446 |
loose-envify: 1.4.0
|
|
@@ -2638,6 +3236,21 @@ snapshots:
|
|
| 2638 |
dependencies:
|
| 2639 |
punycode: 2.3.1
|
| 2640 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2641 | |
| 2642 |
|
| 2643 |
|
|
|
| 8 |
|
| 9 |
.:
|
| 10 |
dependencies:
|
| 11 |
+
'@radix-ui/react-select':
|
| 12 |
+
specifier: ^2.1.2
|
| 13 |
+
version: 2.1.2(@types/[email protected])([email protected]([email protected]))([email protected])
|
| 14 |
'@radix-ui/react-slot':
|
| 15 |
specifier: ^1.0.2
|
| 16 |
version: 1.0.2(@types/[email protected])([email protected])
|
|
|
|
| 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/[email protected]':
|
| 334 |
+
resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==}
|
| 335 |
+
|
| 336 |
+
'@floating-ui/[email protected]':
|
| 337 |
+
resolution: {integrity: sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==}
|
| 338 |
+
|
| 339 |
+
'@floating-ui/[email protected]':
|
| 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/[email protected]':
|
| 346 |
+
resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==}
|
| 347 |
+
|
| 348 |
'@humanwhocodes/[email protected]':
|
| 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/[email protected]':
|
| 412 |
+
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
|
| 413 |
+
|
| 414 |
+
'@radix-ui/[email protected]':
|
| 415 |
+
resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==}
|
| 416 |
+
|
| 417 |
+
'@radix-ui/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 444 |
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
|
| 445 |
peerDependencies:
|
|
|
|
| 449 |
'@types/react':
|
| 450 |
optional: true
|
| 451 |
|
| 452 |
+
'@radix-ui/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 585 |
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
|
| 586 |
peerDependencies:
|
|
|
|
| 590 |
'@types/react':
|
| 591 |
optional: true
|
| 592 |
|
| 593 |
+
'@radix-ui/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 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/[email protected]':
|
| 679 |
+
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
|
| 680 |
+
|
| 681 |
'@remix-run/[email protected]':
|
| 682 |
resolution: {integrity: sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==}
|
| 683 |
engines: {node: '>=14.0.0'}
|
|
|
|
| 851 | |
| 852 |
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
| 853 |
|
| 854 | |
| 855 |
+
resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
|
| 856 |
+
engines: {node: '>=10'}
|
| 857 |
+
|
| 858 | |
| 859 |
resolution: {integrity: sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==}
|
| 860 |
engines: {node: ^10 || ^12 || >=14}
|
|
|
|
| 969 | |
| 970 |
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
| 971 |
|
| 972 | |
| 973 |
+
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
| 974 |
+
|
| 975 | |
| 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 | |
| 1127 |
+
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
|
| 1128 |
+
engines: {node: '>=6'}
|
| 1129 |
+
|
| 1130 | |
| 1131 |
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
| 1132 |
engines: {node: '>= 6'}
|
|
|
|
| 1181 | |
| 1182 |
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
| 1183 |
|
| 1184 | |
| 1185 |
+
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
|
| 1186 |
+
|
| 1187 | |
| 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 | |
| 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 | |
| 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 | |
| 1472 |
resolution: {integrity: sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==}
|
| 1473 |
engines: {node: '>=14.0.0'}
|
|
|
|
| 1481 |
peerDependencies:
|
| 1482 |
react: '>=16.8'
|
| 1483 |
|
| 1484 | |
| 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 | |
| 1495 |
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
| 1496 |
engines: {node: '>=0.10.0'}
|
|
|
|
| 1665 | |
| 1666 |
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
| 1667 |
|
| 1668 | |
| 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 | |
| 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 | |
| 1689 |
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
| 1690 |
|
|
|
|
| 1965 |
|
| 1966 |
'@eslint/[email protected]': {}
|
| 1967 |
|
| 1968 |
+
'@floating-ui/[email protected]':
|
| 1969 |
+
dependencies:
|
| 1970 |
+
'@floating-ui/utils': 0.2.8
|
| 1971 |
+
|
| 1972 |
+
'@floating-ui/[email protected]':
|
| 1973 |
+
dependencies:
|
| 1974 |
+
'@floating-ui/core': 1.6.8
|
| 1975 |
+
'@floating-ui/utils': 0.2.8
|
| 1976 |
+
|
| 1977 |
+
'@floating-ui/[email protected]([email protected]([email protected]))([email protected])':
|
| 1978 |
+
dependencies:
|
| 1979 |
+
'@floating-ui/dom': 1.6.12
|
| 1980 |
+
react: 18.2.0
|
| 1981 |
+
react-dom: 18.2.0([email protected])
|
| 1982 |
+
|
| 1983 |
+
'@floating-ui/[email protected]': {}
|
| 1984 |
+
|
| 1985 |
'@humanwhocodes/[email protected]':
|
| 1986 |
dependencies:
|
| 1987 |
'@humanwhocodes/object-schema': 2.0.1
|
|
|
|
| 2042 |
|
| 2043 |
'@pkgr/[email protected]': {}
|
| 2044 |
|
| 2045 |
+
'@radix-ui/[email protected]': {}
|
| 2046 |
+
|
| 2047 |
+
'@radix-ui/[email protected]': {}
|
| 2048 |
+
|
| 2049 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected]([email protected]))([email protected])':
|
| 2050 |
+
dependencies:
|
| 2051 |
+
'@radix-ui/react-primitive': 2.0.0(@types/[email protected])([email protected]([email protected]))([email protected])
|
| 2052 |
+
react: 18.2.0
|
| 2053 |
+
react-dom: 18.2.0([email protected])
|
| 2054 |
+
optionalDependencies:
|
| 2055 |
+
'@types/react': 18.2.37
|
| 2056 |
+
|
| 2057 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected]([email protected]))([email protected])':
|
| 2058 |
+
dependencies:
|
| 2059 |
+
'@radix-ui/react-compose-refs': 1.1.0(@types/[email protected])([email protected])
|
| 2060 |
+
'@radix-ui/react-context': 1.1.0(@types/[email protected])([email protected])
|
| 2061 |
+
'@radix-ui/react-primitive': 2.0.0(@types/[email protected])([email protected]([email protected]))([email protected])
|
| 2062 |
+
'@radix-ui/react-slot': 1.1.0(@types/[email protected])([email protected])
|
| 2063 |
+
react: 18.2.0
|
| 2064 |
+
react-dom: 18.2.0([email protected])
|
| 2065 |
+
optionalDependencies:
|
| 2066 |
+
'@types/react': 18.2.37
|
| 2067 |
+
|
| 2068 |
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
| 2069 |
dependencies:
|
| 2070 |
'@babel/runtime': 7.23.2
|
|
|
|
| 2072 |
optionalDependencies:
|
| 2073 |
'@types/react': 18.2.37
|
| 2074 |
|
| 2075 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
| 2076 |
+
dependencies:
|
| 2077 |
+
react: 18.2.0
|
| 2078 |
+
optionalDependencies:
|
| 2079 |
+
'@types/react': 18.2.37
|
| 2080 |
+
|
| 2081 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
| 2082 |
+
dependencies:
|
| 2083 |
+
react: 18.2.0
|
| 2084 |
+
optionalDependencies:
|
| 2085 |
+
'@types/react': 18.2.37
|
| 2086 |
+
|
| 2087 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
| 2088 |
+
dependencies:
|
| 2089 |
+
react: 18.2.0
|
| 2090 |
+
optionalDependencies:
|
| 2091 |
+
'@types/react': 18.2.37
|
| 2092 |
+
|
| 2093 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
| 2094 |
+
dependencies:
|
| 2095 |
+
react: 18.2.0
|
| 2096 |
+
optionalDependencies:
|
| 2097 |
+
'@types/react': 18.2.37
|
| 2098 |
+
|
| 2099 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected]([email protected]))([email protected])':
|
| 2100 |
+
dependencies:
|
| 2101 |
+
'@radix-ui/primitive': 1.1.0
|
| 2102 |
+
'@radix-ui/react-compose-refs': 1.1.0(@types/[email protected])([email protected])
|
| 2103 |
+
'@radix-ui/react-primitive': 2.0.0(@types/[email protected])([email protected]([email protected]))([email protected])
|
| 2104 |
+
'@radix-ui/react-use-callback-ref': 1.1.0(@types/[email protected])([email protected])
|
| 2105 |
+
'@radix-ui/react-use-escape-keydown': 1.1.0(@types/[email protected])([email protected])
|
| 2106 |
+
react: 18.2.0
|
| 2107 |
+
react-dom: 18.2.0([email protected])
|
| 2108 |
+
optionalDependencies:
|
| 2109 |
+
'@types/react': 18.2.37
|
| 2110 |
+
|
| 2111 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
| 2112 |
+
dependencies:
|
| 2113 |
+
react: 18.2.0
|
| 2114 |
+
optionalDependencies:
|
| 2115 |
+
'@types/react': 18.2.37
|
| 2116 |
+
|
| 2117 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected]([email protected]))([email protected])':
|
| 2118 |
+
dependencies:
|
| 2119 |
+
'@radix-ui/react-compose-refs': 1.1.0(@types/[email protected])([email protected])
|
| 2120 |
+
'@radix-ui/react-primitive': 2.0.0(@types/[email protected])([email protected]([email protected]))([email protected])
|
| 2121 |
+
'@radix-ui/react-use-callback-ref': 1.1.0(@types/[email protected])([email protected])
|
| 2122 |
+
react: 18.2.0
|
| 2123 |
+
react-dom: 18.2.0([email protected])
|
| 2124 |
+
optionalDependencies:
|
| 2125 |
+
'@types/react': 18.2.37
|
| 2126 |
+
|
| 2127 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
| 2128 |
+
dependencies:
|
| 2129 |
+
'@radix-ui/react-use-layout-effect': 1.1.0(@types/[email protected])([email protected])
|
| 2130 |
+
react: 18.2.0
|
| 2131 |
+
optionalDependencies:
|
| 2132 |
+
'@types/react': 18.2.37
|
| 2133 |
+
|
| 2134 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected]([email protected]))([email protected])':
|
| 2135 |
+
dependencies:
|
| 2136 |
+
'@floating-ui/react-dom': 2.1.2([email protected]([email protected]))([email protected])
|
| 2137 |
+
'@radix-ui/react-arrow': 1.1.0(@types/[email protected])([email protected]([email protected]))([email protected])
|
| 2138 |
+
'@radix-ui/react-compose-refs': 1.1.0(@types/[email protected])([email protected])
|
| 2139 |
+
'@radix-ui/react-context': 1.1.0(@types/[email protected])([email protected])
|
| 2140 |
+
'@radix-ui/react-primitive': 2.0.0(@types/[email protected])([email protected]([email protected]))([email protected])
|
| 2141 |
+
'@radix-ui/react-use-callback-ref': 1.1.0(@types/[email protected])([email protected])
|
| 2142 |
+
'@radix-ui/react-use-layout-effect': 1.1.0(@types/[email protected])([email protected])
|
| 2143 |
+
'@radix-ui/react-use-rect': 1.1.0(@types/[email protected])([email protected])
|
| 2144 |
+
'@radix-ui/react-use-size': 1.1.0(@types/[email protected])([email protected])
|
| 2145 |
+
'@radix-ui/rect': 1.1.0
|
| 2146 |
+
react: 18.2.0
|
| 2147 |
+
react-dom: 18.2.0([email protected])
|
| 2148 |
+
optionalDependencies:
|
| 2149 |
+
'@types/react': 18.2.37
|
| 2150 |
+
|
| 2151 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected]([email protected]))([email protected])':
|
| 2152 |
+
dependencies:
|
| 2153 |
+
'@radix-ui/react-primitive': 2.0.0(@types/[email protected])([email protected]([email protected]))([email protected])
|
| 2154 |
+
'@radix-ui/react-use-layout-effect': 1.1.0(@types/[email protected])([email protected])
|
| 2155 |
+
react: 18.2.0
|
| 2156 |
+
react-dom: 18.2.0([email protected])
|
| 2157 |
+
optionalDependencies:
|
| 2158 |
+
'@types/react': 18.2.37
|
| 2159 |
+
|
| 2160 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected]([email protected]))([email protected])':
|
| 2161 |
+
dependencies:
|
| 2162 |
+
'@radix-ui/react-slot': 1.1.0(@types/[email protected])([email protected])
|
| 2163 |
+
react: 18.2.0
|
| 2164 |
+
react-dom: 18.2.0([email protected])
|
| 2165 |
+
optionalDependencies:
|
| 2166 |
+
'@types/react': 18.2.37
|
| 2167 |
+
|
| 2168 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected]([email protected]))([email protected])':
|
| 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/[email protected])([email protected]([email protected]))([email protected])
|
| 2173 |
+
'@radix-ui/react-compose-refs': 1.1.0(@types/[email protected])([email protected])
|
| 2174 |
+
'@radix-ui/react-context': 1.1.1(@types/[email protected])([email protected])
|
| 2175 |
+
'@radix-ui/react-direction': 1.1.0(@types/[email protected])([email protected])
|
| 2176 |
+
'@radix-ui/react-dismissable-layer': 1.1.1(@types/[email protected])([email protected]([email protected]))([email protected])
|
| 2177 |
+
'@radix-ui/react-focus-guards': 1.1.1(@types/[email protected])([email protected])
|
| 2178 |
+
'@radix-ui/react-focus-scope': 1.1.0(@types/[email protected])([email protected]([email protected]))([email protected])
|
| 2179 |
+
'@radix-ui/react-id': 1.1.0(@types/[email protected])([email protected])
|
| 2180 |
+
'@radix-ui/react-popper': 1.2.0(@types/[email protected])([email protected]([email protected]))([email protected])
|
| 2181 |
+
'@radix-ui/react-portal': 1.1.2(@types/[email protected])([email protected]([email protected]))([email protected])
|
| 2182 |
+
'@radix-ui/react-primitive': 2.0.0(@types/[email protected])([email protected]([email protected]))([email protected])
|
| 2183 |
+
'@radix-ui/react-slot': 1.1.0(@types/[email protected])([email protected])
|
| 2184 |
+
'@radix-ui/react-use-callback-ref': 1.1.0(@types/[email protected])([email protected])
|
| 2185 |
+
'@radix-ui/react-use-controllable-state': 1.1.0(@types/[email protected])([email protected])
|
| 2186 |
+
'@radix-ui/react-use-layout-effect': 1.1.0(@types/[email protected])([email protected])
|
| 2187 |
+
'@radix-ui/react-use-previous': 1.1.0(@types/[email protected])([email protected])
|
| 2188 |
+
'@radix-ui/react-visually-hidden': 1.1.0(@types/[email protected])([email protected]([email protected]))([email protected])
|
| 2189 |
+
aria-hidden: 1.2.4
|
| 2190 |
+
react: 18.2.0
|
| 2191 |
+
react-dom: 18.2.0([email protected])
|
| 2192 |
+
react-remove-scroll: 2.6.0(@types/[email protected])([email protected])
|
| 2193 |
+
optionalDependencies:
|
| 2194 |
+
'@types/react': 18.2.37
|
| 2195 |
+
|
| 2196 |
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
| 2197 |
dependencies:
|
| 2198 |
'@babel/runtime': 7.23.2
|
|
|
|
| 2201 |
optionalDependencies:
|
| 2202 |
'@types/react': 18.2.37
|
| 2203 |
|
| 2204 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
| 2205 |
+
dependencies:
|
| 2206 |
+
'@radix-ui/react-compose-refs': 1.1.0(@types/[email protected])([email protected])
|
| 2207 |
+
react: 18.2.0
|
| 2208 |
+
optionalDependencies:
|
| 2209 |
+
'@types/react': 18.2.37
|
| 2210 |
+
|
| 2211 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
| 2212 |
+
dependencies:
|
| 2213 |
+
react: 18.2.0
|
| 2214 |
+
optionalDependencies:
|
| 2215 |
+
'@types/react': 18.2.37
|
| 2216 |
+
|
| 2217 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
| 2218 |
+
dependencies:
|
| 2219 |
+
'@radix-ui/react-use-callback-ref': 1.1.0(@types/[email protected])([email protected])
|
| 2220 |
+
react: 18.2.0
|
| 2221 |
+
optionalDependencies:
|
| 2222 |
+
'@types/react': 18.2.37
|
| 2223 |
+
|
| 2224 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
| 2225 |
+
dependencies:
|
| 2226 |
+
'@radix-ui/react-use-callback-ref': 1.1.0(@types/[email protected])([email protected])
|
| 2227 |
+
react: 18.2.0
|
| 2228 |
+
optionalDependencies:
|
| 2229 |
+
'@types/react': 18.2.37
|
| 2230 |
+
|
| 2231 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
| 2232 |
+
dependencies:
|
| 2233 |
+
react: 18.2.0
|
| 2234 |
+
optionalDependencies:
|
| 2235 |
+
'@types/react': 18.2.37
|
| 2236 |
+
|
| 2237 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
| 2238 |
+
dependencies:
|
| 2239 |
+
react: 18.2.0
|
| 2240 |
+
optionalDependencies:
|
| 2241 |
+
'@types/react': 18.2.37
|
| 2242 |
+
|
| 2243 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
| 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/[email protected](@types/[email protected])([email protected])':
|
| 2251 |
+
dependencies:
|
| 2252 |
+
'@radix-ui/react-use-layout-effect': 1.1.0(@types/[email protected])([email protected])
|
| 2253 |
+
react: 18.2.0
|
| 2254 |
+
optionalDependencies:
|
| 2255 |
+
'@types/react': 18.2.37
|
| 2256 |
+
|
| 2257 |
+
'@radix-ui/[email protected](@types/[email protected])([email protected]([email protected]))([email protected])':
|
| 2258 |
+
dependencies:
|
| 2259 |
+
'@radix-ui/react-primitive': 2.0.0(@types/[email protected])([email protected]([email protected]))([email protected])
|
| 2260 |
+
react: 18.2.0
|
| 2261 |
+
react-dom: 18.2.0([email protected])
|
| 2262 |
+
optionalDependencies:
|
| 2263 |
+
'@types/react': 18.2.37
|
| 2264 |
+
|
| 2265 |
+
'@radix-ui/[email protected]': {}
|
| 2266 |
+
|
| 2267 |
'@remix-run/[email protected]': {}
|
| 2268 |
|
| 2269 |
'@rollup/[email protected]':
|
|
|
|
| 2412 |
|
| 2413 | |
| 2414 |
|
| 2415 | |
| 2416 |
+
dependencies:
|
| 2417 |
+
tslib: 2.8.1
|
| 2418 |
+
|
| 2419 | |
| 2420 |
dependencies:
|
| 2421 |
browserslist: 4.22.1
|
|
|
|
| 2519 |
|
| 2520 | |
| 2521 |
|
| 2522 |
+
[email protected]: {}
|
| 2523 |
+
|
| 2524 | |
| 2525 |
|
| 2526 | |
|
|
|
| 2722 |
|
| 2723 | |
| 2724 |
|
| 2725 |
+
[email protected]: {}
|
| 2726 |
+
|
| 2727 | |
| 2728 |
dependencies:
|
| 2729 |
is-glob: 4.0.3
|
|
|
|
| 2782 |
|
| 2783 | |
| 2784 |
|
| 2785 | |
| 2786 |
+
dependencies:
|
| 2787 |
+
loose-envify: 1.4.0
|
| 2788 |
+
|
| 2789 | |
| 2790 |
dependencies:
|
| 2791 |
binary-extensions: 2.2.0
|
|
|
|
| 2999 |
|
| 3000 | |
| 3001 |
|
| 3002 |
+
[email protected](@types/[email protected])([email protected]):
|
| 3003 |
+
dependencies:
|
| 3004 |
+
react: 18.2.0
|
| 3005 |
+
react-style-singleton: 2.2.1(@types/[email protected])([email protected])
|
| 3006 |
+
tslib: 2.8.1
|
| 3007 |
+
optionalDependencies:
|
| 3008 |
+
'@types/react': 18.2.37
|
| 3009 |
+
|
| 3010 |
+
[email protected](@types/[email protected])([email protected]):
|
| 3011 |
+
dependencies:
|
| 3012 |
+
react: 18.2.0
|
| 3013 |
+
react-remove-scroll-bar: 2.3.6(@types/[email protected])([email protected])
|
| 3014 |
+
react-style-singleton: 2.2.1(@types/[email protected])([email protected])
|
| 3015 |
+
tslib: 2.8.1
|
| 3016 |
+
use-callback-ref: 1.3.2(@types/[email protected])([email protected])
|
| 3017 |
+
use-sidecar: 1.1.2(@types/[email protected])([email protected])
|
| 3018 |
+
optionalDependencies:
|
| 3019 |
+
'@types/react': 18.2.37
|
| 3020 |
+
|
| 3021 | |
| 3022 |
dependencies:
|
| 3023 |
'@remix-run/router': 1.20.0
|
|
|
|
| 3030 |
'@remix-run/router': 1.20.0
|
| 3031 |
react: 18.2.0
|
| 3032 |
|
| 3033 |
+
[email protected](@types/[email protected])([email protected]):
|
| 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 | |
| 3043 |
dependencies:
|
| 3044 |
loose-envify: 1.4.0
|
|
|
|
| 3236 |
dependencies:
|
| 3237 |
punycode: 2.3.1
|
| 3238 |
|
| 3239 |
+
[email protected](@types/[email protected])([email protected]):
|
| 3240 |
+
dependencies:
|
| 3241 |
+
react: 18.2.0
|
| 3242 |
+
tslib: 2.8.1
|
| 3243 |
+
optionalDependencies:
|
| 3244 |
+
'@types/react': 18.2.37
|
| 3245 |
+
|
| 3246 |
+
[email protected](@types/[email protected])([email protected]):
|
| 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 | |
| 3255 |
|
| 3256 |
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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|