richlai commited on
Commit
3bb94b1
Β·
1 Parent(s): 83f6a8c

added new UI, added form, added api message

Browse files
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
- SalesBuddy for BetterTech
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, FileUpload
22
- from .db.database import get_user_by_username, create_user, save_file, get_user_files
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 get_opportunities(request: Request, current_user: User = Depends(get_current_user)) -> dict:
111
- records = await get_user_files(current_user.username)
112
- print("records", records)
113
- all_records = []
114
- for record in records:
115
- all_records.extend(record.content)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- return JSONResponse(content={"message": answer.model_dump_json()})
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.status_code == 200
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
1695
  dependencies:
1696
  '@babel/runtime': 7.23.2
@@ -1698,6 +2072,127 @@ snapshots:
1698
  optionalDependencies:
1699
  '@types/react': 18.2.37
1700
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1701
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
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
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
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
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
2069
  dependencies:
2070
  '@babel/runtime': 7.23.2
 
2072
  optionalDependencies:
2073
  '@types/react': 18.2.37
2074
 
2075
2076
+ dependencies:
2077
+ react: 18.2.0
2078
+ optionalDependencies:
2079
+ '@types/react': 18.2.37
2080
+
2081
2082
+ dependencies:
2083
+ react: 18.2.0
2084
+ optionalDependencies:
2085
+ '@types/react': 18.2.37
2086
+
2087
2088
+ dependencies:
2089
+ react: 18.2.0
2090
+ optionalDependencies:
2091
+ '@types/react': 18.2.37
2092
+
2093
2094
+ dependencies:
2095
+ react: 18.2.0
2096
+ optionalDependencies:
2097
+ '@types/react': 18.2.37
2098
+
2099
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
2112
+ dependencies:
2113
+ react: 18.2.0
2114
+ optionalDependencies:
2115
+ '@types/react': 18.2.37
2116
+
2117
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
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
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
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
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
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
2197
  dependencies:
2198
  '@babel/runtime': 7.23.2
 
2201
  optionalDependencies:
2202
  '@types/react': 18.2.37
2203
 
2204
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
2212
+ dependencies:
2213
+ react: 18.2.0
2214
+ optionalDependencies:
2215
+ '@types/react': 18.2.37
2216
+
2217
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
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
2232
+ dependencies:
2233
+ react: 18.2.0
2234
+ optionalDependencies:
2235
+ '@types/react': 18.2.37
2236
+
2237
2238
+ dependencies:
2239
+ react: 18.2.0
2240
+ optionalDependencies:
2241
+ '@types/react': 18.2.37
2242
+
2243
2244
+ dependencies:
2245
+ '@radix-ui/rect': 1.1.0
2246
+ react: 18.2.0
2247
+ optionalDependencies:
2248
+ '@types/react': 18.2.37
2249
+
2250
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
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
2523
+
2524
2525
 
2526
 
2722
 
2723
2724
 
2725
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
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
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
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
3240
+ dependencies:
3241
+ react: 18.2.0
3242
+ tslib: 2.8.1
3243
+ optionalDependencies:
3244
+ '@types/react': 18.2.37
3245
+
3246
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={location.state?.from?.pathname || "/"} replace />;
30
  };
31
 
32
  function AppRoutes() {
@@ -73,11 +73,11 @@ function AppRoutes() {
73
 
74
  function App() {
75
  return (
76
- <AuthProvider>
77
- <Router>
78
  <AppRoutes/>
79
- </Router>
80
- </AuthProvider>
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
- <div className="flex justify-between items-center p-4 bg-white shadow">
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 Chat from './Chat';
4
- const storedToken = localStorage.getItem('token');
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
- <Popup isOpen={isPopupOpen} onClose={() => setIsPopupOpen(false)} token={token} title="No Opportunities">
49
- <p>No opportunities found. Please upload a file to get started.</p>
50
- </Popup>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 =({ className, variant, size, asChild = false, ...props }, ref) => {
 
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 { type ClassValue, clsx } from "clsx"
2
  import { twMerge } from "tailwind-merge"
3
 
4
- export function cn(...inputs: ClassValue[]) {
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
- }