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/react@18.2.37)(react@18.2.0)
@@ -327,6 +330,21 @@ packages:
327
  resolution: {integrity: sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==}
328
  engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
329
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  '@humanwhocodes/config-array@0.11.13':
331
  resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==}
332
  engines: {node: '>=10.10.0'}
@@ -390,6 +408,38 @@ packages:
390
  resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
391
  engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
392
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  '@radix-ui/react-compose-refs@1.0.1':
394
  resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
395
  peerDependencies:
@@ -399,6 +449,138 @@ packages:
399
  '@types/react':
400
  optional: true
401
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  '@radix-ui/react-slot@1.0.2':
403
  resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
404
  peerDependencies:
@@ -408,6 +590,94 @@ packages:
408
  '@types/react':
409
  optional: true
410
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  '@remix-run/router@1.20.0':
412
  resolution: {integrity: sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==}
413
  engines: {node: '>=14.0.0'}
@@ -581,6 +851,10 @@ packages:
581
  argparse@2.0.1:
582
  resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
583
 
 
 
 
 
584
  autoprefixer@10.4.16:
585
  resolution: {integrity: sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==}
586
  engines: {node: ^10 || ^12 || >=14}
@@ -695,6 +969,9 @@ packages:
695
  deep-is@0.1.4:
696
  resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
697
 
 
 
 
698
  didyoumean@1.2.2:
699
  resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
700
 
@@ -846,6 +1123,10 @@ packages:
846
  resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
847
  engines: {node: '>=6.9.0'}
848
 
 
 
 
 
849
  glob-parent@5.1.2:
850
  resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
851
  engines: {node: '>= 6'}
@@ -900,6 +1181,9 @@ packages:
900
  inherits@2.0.4:
901
  resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
902
 
 
 
 
903
  is-binary-path@2.1.0:
904
  resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
905
  engines: {node: '>=8'}
@@ -1164,6 +1448,26 @@ packages:
1164
  resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
1165
  engines: {node: '>=0.10.0'}
1166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1167
  react-router-dom@6.27.0:
1168
  resolution: {integrity: sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==}
1169
  engines: {node: '>=14.0.0'}
@@ -1177,6 +1481,16 @@ packages:
1177
  peerDependencies:
1178
  react: '>=16.8'
1179
 
 
 
 
 
 
 
 
 
 
 
1180
  react@18.2.0:
1181
  resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
1182
  engines: {node: '>=0.10.0'}
@@ -1351,6 +1665,26 @@ packages:
1351
  uri-js@4.4.1:
1352
  resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
1353
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1354
  util-deprecate@1.0.2:
1355
  resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
1356
 
@@ -1631,6 +1965,23 @@ snapshots:
1631
 
1632
  '@eslint/js@8.53.0': {}
1633
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1634
  '@humanwhocodes/config-array@0.11.13':
1635
  dependencies:
1636
  '@humanwhocodes/object-schema': 2.0.1
@@ -1691,6 +2042,29 @@ snapshots:
1691
 
1692
  '@pkgr/core@0.1.1': {}
1693
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1694
  '@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.37)(react@18.2.0)':
1695
  dependencies:
1696
  '@babel/runtime': 7.23.2
@@ -1698,6 +2072,127 @@ snapshots:
1698
  optionalDependencies:
1699
  '@types/react': 18.2.37
1700
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1701
  '@radix-ui/react-slot@1.0.2(@types/react@18.2.37)(react@18.2.0)':
1702
  dependencies:
1703
  '@babel/runtime': 7.23.2
@@ -1706,6 +2201,69 @@ snapshots:
1706
  optionalDependencies:
1707
  '@types/react': 18.2.37
1708
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1709
  '@remix-run/router@1.20.0': {}
1710
 
1711
  '@rollup/rollup-android-arm-eabi@4.25.0':
@@ -1854,6 +2412,10 @@ snapshots:
1854
 
1855
  argparse@2.0.1: {}
1856
 
 
 
 
 
1857
  autoprefixer@10.4.16(postcss@8.4.31):
1858
  dependencies:
1859
  browserslist: 4.22.1
@@ -1957,6 +2519,8 @@ snapshots:
1957
 
1958
  deep-is@0.1.4: {}
1959
 
 
 
1960
  didyoumean@1.2.2: {}
1961
 
1962
  dlv@1.1.3: {}
@@ -2158,6 +2722,8 @@ snapshots:
2158
 
2159
  gensync@1.0.0-beta.2: {}
2160
 
 
 
2161
  glob-parent@5.1.2:
2162
  dependencies:
2163
  is-glob: 4.0.3
@@ -2216,6 +2782,10 @@ snapshots:
2216
 
2217
  inherits@2.0.4: {}
2218
 
 
 
 
 
2219
  is-binary-path@2.1.0:
2220
  dependencies:
2221
  binary-extensions: 2.2.0
@@ -2429,6 +2999,25 @@ snapshots:
2429
 
2430
  react-refresh@0.14.2: {}
2431
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2432
  react-router-dom@6.27.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
2433
  dependencies:
2434
  '@remix-run/router': 1.20.0
@@ -2441,6 +3030,15 @@ snapshots:
2441
  '@remix-run/router': 1.20.0
2442
  react: 18.2.0
2443
 
 
 
 
 
 
 
 
 
 
2444
  react@18.2.0:
2445
  dependencies:
2446
  loose-envify: 1.4.0
@@ -2638,6 +3236,21 @@ snapshots:
2638
  dependencies:
2639
  punycode: 2.3.1
2640
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2641
  util-deprecate@1.0.2: {}
2642
 
2643
  uuid@9.0.1: {}
 
8
 
9
  .:
10
  dependencies:
11
+ '@radix-ui/react-select':
12
+ specifier: ^2.1.2
13
+ version: 2.1.2(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
14
  '@radix-ui/react-slot':
15
  specifier: ^1.0.2
16
  version: 1.0.2(@types/react@18.2.37)(react@18.2.0)
 
330
  resolution: {integrity: sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==}
331
  engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
332
 
333
+ '@floating-ui/core@1.6.8':
334
+ resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==}
335
+
336
+ '@floating-ui/dom@1.6.12':
337
+ resolution: {integrity: sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==}
338
+
339
+ '@floating-ui/react-dom@2.1.2':
340
+ resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==}
341
+ peerDependencies:
342
+ react: '>=16.8.0'
343
+ react-dom: '>=16.8.0'
344
+
345
+ '@floating-ui/utils@0.2.8':
346
+ resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==}
347
+
348
  '@humanwhocodes/config-array@0.11.13':
349
  resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==}
350
  engines: {node: '>=10.10.0'}
 
408
  resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
409
  engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
410
 
411
+ '@radix-ui/number@1.1.0':
412
+ resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
413
+
414
+ '@radix-ui/primitive@1.1.0':
415
+ resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==}
416
+
417
+ '@radix-ui/react-arrow@1.1.0':
418
+ resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==}
419
+ peerDependencies:
420
+ '@types/react': '*'
421
+ '@types/react-dom': '*'
422
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
423
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
424
+ peerDependenciesMeta:
425
+ '@types/react':
426
+ optional: true
427
+ '@types/react-dom':
428
+ optional: true
429
+
430
+ '@radix-ui/react-collection@1.1.0':
431
+ resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==}
432
+ peerDependencies:
433
+ '@types/react': '*'
434
+ '@types/react-dom': '*'
435
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
436
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
437
+ peerDependenciesMeta:
438
+ '@types/react':
439
+ optional: true
440
+ '@types/react-dom':
441
+ optional: true
442
+
443
  '@radix-ui/react-compose-refs@1.0.1':
444
  resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
445
  peerDependencies:
 
449
  '@types/react':
450
  optional: true
451
 
452
+ '@radix-ui/react-compose-refs@1.1.0':
453
+ resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==}
454
+ peerDependencies:
455
+ '@types/react': '*'
456
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
457
+ peerDependenciesMeta:
458
+ '@types/react':
459
+ optional: true
460
+
461
+ '@radix-ui/react-context@1.1.0':
462
+ resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==}
463
+ peerDependencies:
464
+ '@types/react': '*'
465
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
466
+ peerDependenciesMeta:
467
+ '@types/react':
468
+ optional: true
469
+
470
+ '@radix-ui/react-context@1.1.1':
471
+ resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
472
+ peerDependencies:
473
+ '@types/react': '*'
474
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
475
+ peerDependenciesMeta:
476
+ '@types/react':
477
+ optional: true
478
+
479
+ '@radix-ui/react-direction@1.1.0':
480
+ resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
481
+ peerDependencies:
482
+ '@types/react': '*'
483
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
484
+ peerDependenciesMeta:
485
+ '@types/react':
486
+ optional: true
487
+
488
+ '@radix-ui/react-dismissable-layer@1.1.1':
489
+ resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==}
490
+ peerDependencies:
491
+ '@types/react': '*'
492
+ '@types/react-dom': '*'
493
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
494
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
495
+ peerDependenciesMeta:
496
+ '@types/react':
497
+ optional: true
498
+ '@types/react-dom':
499
+ optional: true
500
+
501
+ '@radix-ui/react-focus-guards@1.1.1':
502
+ resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==}
503
+ peerDependencies:
504
+ '@types/react': '*'
505
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
506
+ peerDependenciesMeta:
507
+ '@types/react':
508
+ optional: true
509
+
510
+ '@radix-ui/react-focus-scope@1.1.0':
511
+ resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==}
512
+ peerDependencies:
513
+ '@types/react': '*'
514
+ '@types/react-dom': '*'
515
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
516
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
517
+ peerDependenciesMeta:
518
+ '@types/react':
519
+ optional: true
520
+ '@types/react-dom':
521
+ optional: true
522
+
523
+ '@radix-ui/react-id@1.1.0':
524
+ resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==}
525
+ peerDependencies:
526
+ '@types/react': '*'
527
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
528
+ peerDependenciesMeta:
529
+ '@types/react':
530
+ optional: true
531
+
532
+ '@radix-ui/react-popper@1.2.0':
533
+ resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==}
534
+ peerDependencies:
535
+ '@types/react': '*'
536
+ '@types/react-dom': '*'
537
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
538
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
539
+ peerDependenciesMeta:
540
+ '@types/react':
541
+ optional: true
542
+ '@types/react-dom':
543
+ optional: true
544
+
545
+ '@radix-ui/react-portal@1.1.2':
546
+ resolution: {integrity: sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==}
547
+ peerDependencies:
548
+ '@types/react': '*'
549
+ '@types/react-dom': '*'
550
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
551
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
552
+ peerDependenciesMeta:
553
+ '@types/react':
554
+ optional: true
555
+ '@types/react-dom':
556
+ optional: true
557
+
558
+ '@radix-ui/react-primitive@2.0.0':
559
+ resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
560
+ peerDependencies:
561
+ '@types/react': '*'
562
+ '@types/react-dom': '*'
563
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
564
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
565
+ peerDependenciesMeta:
566
+ '@types/react':
567
+ optional: true
568
+ '@types/react-dom':
569
+ optional: true
570
+
571
+ '@radix-ui/react-select@2.1.2':
572
+ resolution: {integrity: sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA==}
573
+ peerDependencies:
574
+ '@types/react': '*'
575
+ '@types/react-dom': '*'
576
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
577
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
578
+ peerDependenciesMeta:
579
+ '@types/react':
580
+ optional: true
581
+ '@types/react-dom':
582
+ optional: true
583
+
584
  '@radix-ui/react-slot@1.0.2':
585
  resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
586
  peerDependencies:
 
590
  '@types/react':
591
  optional: true
592
 
593
+ '@radix-ui/react-slot@1.1.0':
594
+ resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==}
595
+ peerDependencies:
596
+ '@types/react': '*'
597
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
598
+ peerDependenciesMeta:
599
+ '@types/react':
600
+ optional: true
601
+
602
+ '@radix-ui/react-use-callback-ref@1.1.0':
603
+ resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
604
+ peerDependencies:
605
+ '@types/react': '*'
606
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
607
+ peerDependenciesMeta:
608
+ '@types/react':
609
+ optional: true
610
+
611
+ '@radix-ui/react-use-controllable-state@1.1.0':
612
+ resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
613
+ peerDependencies:
614
+ '@types/react': '*'
615
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
616
+ peerDependenciesMeta:
617
+ '@types/react':
618
+ optional: true
619
+
620
+ '@radix-ui/react-use-escape-keydown@1.1.0':
621
+ resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
622
+ peerDependencies:
623
+ '@types/react': '*'
624
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
625
+ peerDependenciesMeta:
626
+ '@types/react':
627
+ optional: true
628
+
629
+ '@radix-ui/react-use-layout-effect@1.1.0':
630
+ resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
631
+ peerDependencies:
632
+ '@types/react': '*'
633
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
634
+ peerDependenciesMeta:
635
+ '@types/react':
636
+ optional: true
637
+
638
+ '@radix-ui/react-use-previous@1.1.0':
639
+ resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==}
640
+ peerDependencies:
641
+ '@types/react': '*'
642
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
643
+ peerDependenciesMeta:
644
+ '@types/react':
645
+ optional: true
646
+
647
+ '@radix-ui/react-use-rect@1.1.0':
648
+ resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
649
+ peerDependencies:
650
+ '@types/react': '*'
651
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
652
+ peerDependenciesMeta:
653
+ '@types/react':
654
+ optional: true
655
+
656
+ '@radix-ui/react-use-size@1.1.0':
657
+ resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==}
658
+ peerDependencies:
659
+ '@types/react': '*'
660
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
661
+ peerDependenciesMeta:
662
+ '@types/react':
663
+ optional: true
664
+
665
+ '@radix-ui/react-visually-hidden@1.1.0':
666
+ resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==}
667
+ peerDependencies:
668
+ '@types/react': '*'
669
+ '@types/react-dom': '*'
670
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
671
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
672
+ peerDependenciesMeta:
673
+ '@types/react':
674
+ optional: true
675
+ '@types/react-dom':
676
+ optional: true
677
+
678
+ '@radix-ui/rect@1.1.0':
679
+ resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
680
+
681
  '@remix-run/router@1.20.0':
682
  resolution: {integrity: sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==}
683
  engines: {node: '>=14.0.0'}
 
851
  argparse@2.0.1:
852
  resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
853
 
854
+ aria-hidden@1.2.4:
855
+ resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
856
+ engines: {node: '>=10'}
857
+
858
  autoprefixer@10.4.16:
859
  resolution: {integrity: sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==}
860
  engines: {node: ^10 || ^12 || >=14}
 
969
  deep-is@0.1.4:
970
  resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
971
 
972
+ detect-node-es@1.1.0:
973
+ resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
974
+
975
  didyoumean@1.2.2:
976
  resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
977
 
 
1123
  resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
1124
  engines: {node: '>=6.9.0'}
1125
 
1126
+ get-nonce@1.0.1:
1127
+ resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
1128
+ engines: {node: '>=6'}
1129
+
1130
  glob-parent@5.1.2:
1131
  resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
1132
  engines: {node: '>= 6'}
 
1181
  inherits@2.0.4:
1182
  resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
1183
 
1184
+ invariant@2.2.4:
1185
+ resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
1186
+
1187
  is-binary-path@2.1.0:
1188
  resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
1189
  engines: {node: '>=8'}
 
1448
  resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
1449
  engines: {node: '>=0.10.0'}
1450
 
1451
+ react-remove-scroll-bar@2.3.6:
1452
+ resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
1453
+ engines: {node: '>=10'}
1454
+ peerDependencies:
1455
+ '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
1456
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
1457
+ peerDependenciesMeta:
1458
+ '@types/react':
1459
+ optional: true
1460
+
1461
+ react-remove-scroll@2.6.0:
1462
+ resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==}
1463
+ engines: {node: '>=10'}
1464
+ peerDependencies:
1465
+ '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
1466
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
1467
+ peerDependenciesMeta:
1468
+ '@types/react':
1469
+ optional: true
1470
+
1471
  react-router-dom@6.27.0:
1472
  resolution: {integrity: sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==}
1473
  engines: {node: '>=14.0.0'}
 
1481
  peerDependencies:
1482
  react: '>=16.8'
1483
 
1484
+ react-style-singleton@2.2.1:
1485
+ resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
1486
+ engines: {node: '>=10'}
1487
+ peerDependencies:
1488
+ '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
1489
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
1490
+ peerDependenciesMeta:
1491
+ '@types/react':
1492
+ optional: true
1493
+
1494
  react@18.2.0:
1495
  resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
1496
  engines: {node: '>=0.10.0'}
 
1665
  uri-js@4.4.1:
1666
  resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
1667
 
1668
+ use-callback-ref@1.3.2:
1669
+ resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==}
1670
+ engines: {node: '>=10'}
1671
+ peerDependencies:
1672
+ '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
1673
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
1674
+ peerDependenciesMeta:
1675
+ '@types/react':
1676
+ optional: true
1677
+
1678
+ use-sidecar@1.1.2:
1679
+ resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
1680
+ engines: {node: '>=10'}
1681
+ peerDependencies:
1682
+ '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0
1683
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
1684
+ peerDependenciesMeta:
1685
+ '@types/react':
1686
+ optional: true
1687
+
1688
  util-deprecate@1.0.2:
1689
  resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
1690
 
 
1965
 
1966
  '@eslint/js@8.53.0': {}
1967
 
1968
+ '@floating-ui/core@1.6.8':
1969
+ dependencies:
1970
+ '@floating-ui/utils': 0.2.8
1971
+
1972
+ '@floating-ui/dom@1.6.12':
1973
+ dependencies:
1974
+ '@floating-ui/core': 1.6.8
1975
+ '@floating-ui/utils': 0.2.8
1976
+
1977
+ '@floating-ui/react-dom@2.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
1978
+ dependencies:
1979
+ '@floating-ui/dom': 1.6.12
1980
+ react: 18.2.0
1981
+ react-dom: 18.2.0(react@18.2.0)
1982
+
1983
+ '@floating-ui/utils@0.2.8': {}
1984
+
1985
  '@humanwhocodes/config-array@0.11.13':
1986
  dependencies:
1987
  '@humanwhocodes/object-schema': 2.0.1
 
2042
 
2043
  '@pkgr/core@0.1.1': {}
2044
 
2045
+ '@radix-ui/number@1.1.0': {}
2046
+
2047
+ '@radix-ui/primitive@1.1.0': {}
2048
+
2049
+ '@radix-ui/react-arrow@1.1.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
2050
+ dependencies:
2051
+ '@radix-ui/react-primitive': 2.0.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
2052
+ react: 18.2.0
2053
+ react-dom: 18.2.0(react@18.2.0)
2054
+ optionalDependencies:
2055
+ '@types/react': 18.2.37
2056
+
2057
+ '@radix-ui/react-collection@1.1.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
2058
+ dependencies:
2059
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2060
+ '@radix-ui/react-context': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2061
+ '@radix-ui/react-primitive': 2.0.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
2062
+ '@radix-ui/react-slot': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2063
+ react: 18.2.0
2064
+ react-dom: 18.2.0(react@18.2.0)
2065
+ optionalDependencies:
2066
+ '@types/react': 18.2.37
2067
+
2068
  '@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.37)(react@18.2.0)':
2069
  dependencies:
2070
  '@babel/runtime': 7.23.2
 
2072
  optionalDependencies:
2073
  '@types/react': 18.2.37
2074
 
2075
+ '@radix-ui/react-compose-refs@1.1.0(@types/react@18.2.37)(react@18.2.0)':
2076
+ dependencies:
2077
+ react: 18.2.0
2078
+ optionalDependencies:
2079
+ '@types/react': 18.2.37
2080
+
2081
+ '@radix-ui/react-context@1.1.0(@types/react@18.2.37)(react@18.2.0)':
2082
+ dependencies:
2083
+ react: 18.2.0
2084
+ optionalDependencies:
2085
+ '@types/react': 18.2.37
2086
+
2087
+ '@radix-ui/react-context@1.1.1(@types/react@18.2.37)(react@18.2.0)':
2088
+ dependencies:
2089
+ react: 18.2.0
2090
+ optionalDependencies:
2091
+ '@types/react': 18.2.37
2092
+
2093
+ '@radix-ui/react-direction@1.1.0(@types/react@18.2.37)(react@18.2.0)':
2094
+ dependencies:
2095
+ react: 18.2.0
2096
+ optionalDependencies:
2097
+ '@types/react': 18.2.37
2098
+
2099
+ '@radix-ui/react-dismissable-layer@1.1.1(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
2100
+ dependencies:
2101
+ '@radix-ui/primitive': 1.1.0
2102
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2103
+ '@radix-ui/react-primitive': 2.0.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
2104
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2105
+ '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2106
+ react: 18.2.0
2107
+ react-dom: 18.2.0(react@18.2.0)
2108
+ optionalDependencies:
2109
+ '@types/react': 18.2.37
2110
+
2111
+ '@radix-ui/react-focus-guards@1.1.1(@types/react@18.2.37)(react@18.2.0)':
2112
+ dependencies:
2113
+ react: 18.2.0
2114
+ optionalDependencies:
2115
+ '@types/react': 18.2.37
2116
+
2117
+ '@radix-ui/react-focus-scope@1.1.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
2118
+ dependencies:
2119
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2120
+ '@radix-ui/react-primitive': 2.0.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
2121
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2122
+ react: 18.2.0
2123
+ react-dom: 18.2.0(react@18.2.0)
2124
+ optionalDependencies:
2125
+ '@types/react': 18.2.37
2126
+
2127
+ '@radix-ui/react-id@1.1.0(@types/react@18.2.37)(react@18.2.0)':
2128
+ dependencies:
2129
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2130
+ react: 18.2.0
2131
+ optionalDependencies:
2132
+ '@types/react': 18.2.37
2133
+
2134
+ '@radix-ui/react-popper@1.2.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
2135
+ dependencies:
2136
+ '@floating-ui/react-dom': 2.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
2137
+ '@radix-ui/react-arrow': 1.1.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
2138
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2139
+ '@radix-ui/react-context': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2140
+ '@radix-ui/react-primitive': 2.0.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
2141
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2142
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2143
+ '@radix-ui/react-use-rect': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2144
+ '@radix-ui/react-use-size': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2145
+ '@radix-ui/rect': 1.1.0
2146
+ react: 18.2.0
2147
+ react-dom: 18.2.0(react@18.2.0)
2148
+ optionalDependencies:
2149
+ '@types/react': 18.2.37
2150
+
2151
+ '@radix-ui/react-portal@1.1.2(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
2152
+ dependencies:
2153
+ '@radix-ui/react-primitive': 2.0.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
2154
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2155
+ react: 18.2.0
2156
+ react-dom: 18.2.0(react@18.2.0)
2157
+ optionalDependencies:
2158
+ '@types/react': 18.2.37
2159
+
2160
+ '@radix-ui/react-primitive@2.0.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
2161
+ dependencies:
2162
+ '@radix-ui/react-slot': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2163
+ react: 18.2.0
2164
+ react-dom: 18.2.0(react@18.2.0)
2165
+ optionalDependencies:
2166
+ '@types/react': 18.2.37
2167
+
2168
+ '@radix-ui/react-select@2.1.2(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
2169
+ dependencies:
2170
+ '@radix-ui/number': 1.1.0
2171
+ '@radix-ui/primitive': 1.1.0
2172
+ '@radix-ui/react-collection': 1.1.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
2173
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2174
+ '@radix-ui/react-context': 1.1.1(@types/react@18.2.37)(react@18.2.0)
2175
+ '@radix-ui/react-direction': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2176
+ '@radix-ui/react-dismissable-layer': 1.1.1(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
2177
+ '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.2.37)(react@18.2.0)
2178
+ '@radix-ui/react-focus-scope': 1.1.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
2179
+ '@radix-ui/react-id': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2180
+ '@radix-ui/react-popper': 1.2.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
2181
+ '@radix-ui/react-portal': 1.1.2(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
2182
+ '@radix-ui/react-primitive': 2.0.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
2183
+ '@radix-ui/react-slot': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2184
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2185
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2186
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2187
+ '@radix-ui/react-use-previous': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2188
+ '@radix-ui/react-visually-hidden': 1.1.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
2189
+ aria-hidden: 1.2.4
2190
+ react: 18.2.0
2191
+ react-dom: 18.2.0(react@18.2.0)
2192
+ react-remove-scroll: 2.6.0(@types/react@18.2.37)(react@18.2.0)
2193
+ optionalDependencies:
2194
+ '@types/react': 18.2.37
2195
+
2196
  '@radix-ui/react-slot@1.0.2(@types/react@18.2.37)(react@18.2.0)':
2197
  dependencies:
2198
  '@babel/runtime': 7.23.2
 
2201
  optionalDependencies:
2202
  '@types/react': 18.2.37
2203
 
2204
+ '@radix-ui/react-slot@1.1.0(@types/react@18.2.37)(react@18.2.0)':
2205
+ dependencies:
2206
+ '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2207
+ react: 18.2.0
2208
+ optionalDependencies:
2209
+ '@types/react': 18.2.37
2210
+
2211
+ '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.2.37)(react@18.2.0)':
2212
+ dependencies:
2213
+ react: 18.2.0
2214
+ optionalDependencies:
2215
+ '@types/react': 18.2.37
2216
+
2217
+ '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.2.37)(react@18.2.0)':
2218
+ dependencies:
2219
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2220
+ react: 18.2.0
2221
+ optionalDependencies:
2222
+ '@types/react': 18.2.37
2223
+
2224
+ '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.2.37)(react@18.2.0)':
2225
+ dependencies:
2226
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2227
+ react: 18.2.0
2228
+ optionalDependencies:
2229
+ '@types/react': 18.2.37
2230
+
2231
+ '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.2.37)(react@18.2.0)':
2232
+ dependencies:
2233
+ react: 18.2.0
2234
+ optionalDependencies:
2235
+ '@types/react': 18.2.37
2236
+
2237
+ '@radix-ui/react-use-previous@1.1.0(@types/react@18.2.37)(react@18.2.0)':
2238
+ dependencies:
2239
+ react: 18.2.0
2240
+ optionalDependencies:
2241
+ '@types/react': 18.2.37
2242
+
2243
+ '@radix-ui/react-use-rect@1.1.0(@types/react@18.2.37)(react@18.2.0)':
2244
+ dependencies:
2245
+ '@radix-ui/rect': 1.1.0
2246
+ react: 18.2.0
2247
+ optionalDependencies:
2248
+ '@types/react': 18.2.37
2249
+
2250
+ '@radix-ui/react-use-size@1.1.0(@types/react@18.2.37)(react@18.2.0)':
2251
+ dependencies:
2252
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.37)(react@18.2.0)
2253
+ react: 18.2.0
2254
+ optionalDependencies:
2255
+ '@types/react': 18.2.37
2256
+
2257
+ '@radix-ui/react-visually-hidden@1.1.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
2258
+ dependencies:
2259
+ '@radix-ui/react-primitive': 2.0.0(@types/react@18.2.37)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
2260
+ react: 18.2.0
2261
+ react-dom: 18.2.0(react@18.2.0)
2262
+ optionalDependencies:
2263
+ '@types/react': 18.2.37
2264
+
2265
+ '@radix-ui/rect@1.1.0': {}
2266
+
2267
  '@remix-run/router@1.20.0': {}
2268
 
2269
  '@rollup/rollup-android-arm-eabi@4.25.0':
 
2412
 
2413
  argparse@2.0.1: {}
2414
 
2415
+ aria-hidden@1.2.4:
2416
+ dependencies:
2417
+ tslib: 2.8.1
2418
+
2419
  autoprefixer@10.4.16(postcss@8.4.31):
2420
  dependencies:
2421
  browserslist: 4.22.1
 
2519
 
2520
  deep-is@0.1.4: {}
2521
 
2522
+ detect-node-es@1.1.0: {}
2523
+
2524
  didyoumean@1.2.2: {}
2525
 
2526
  dlv@1.1.3: {}
 
2722
 
2723
  gensync@1.0.0-beta.2: {}
2724
 
2725
+ get-nonce@1.0.1: {}
2726
+
2727
  glob-parent@5.1.2:
2728
  dependencies:
2729
  is-glob: 4.0.3
 
2782
 
2783
  inherits@2.0.4: {}
2784
 
2785
+ invariant@2.2.4:
2786
+ dependencies:
2787
+ loose-envify: 1.4.0
2788
+
2789
  is-binary-path@2.1.0:
2790
  dependencies:
2791
  binary-extensions: 2.2.0
 
2999
 
3000
  react-refresh@0.14.2: {}
3001
 
3002
+ react-remove-scroll-bar@2.3.6(@types/react@18.2.37)(react@18.2.0):
3003
+ dependencies:
3004
+ react: 18.2.0
3005
+ react-style-singleton: 2.2.1(@types/react@18.2.37)(react@18.2.0)
3006
+ tslib: 2.8.1
3007
+ optionalDependencies:
3008
+ '@types/react': 18.2.37
3009
+
3010
+ react-remove-scroll@2.6.0(@types/react@18.2.37)(react@18.2.0):
3011
+ dependencies:
3012
+ react: 18.2.0
3013
+ react-remove-scroll-bar: 2.3.6(@types/react@18.2.37)(react@18.2.0)
3014
+ react-style-singleton: 2.2.1(@types/react@18.2.37)(react@18.2.0)
3015
+ tslib: 2.8.1
3016
+ use-callback-ref: 1.3.2(@types/react@18.2.37)(react@18.2.0)
3017
+ use-sidecar: 1.1.2(@types/react@18.2.37)(react@18.2.0)
3018
+ optionalDependencies:
3019
+ '@types/react': 18.2.37
3020
+
3021
  react-router-dom@6.27.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
3022
  dependencies:
3023
  '@remix-run/router': 1.20.0
 
3030
  '@remix-run/router': 1.20.0
3031
  react: 18.2.0
3032
 
3033
+ react-style-singleton@2.2.1(@types/react@18.2.37)(react@18.2.0):
3034
+ dependencies:
3035
+ get-nonce: 1.0.1
3036
+ invariant: 2.2.4
3037
+ react: 18.2.0
3038
+ tslib: 2.8.1
3039
+ optionalDependencies:
3040
+ '@types/react': 18.2.37
3041
+
3042
  react@18.2.0:
3043
  dependencies:
3044
  loose-envify: 1.4.0
 
3236
  dependencies:
3237
  punycode: 2.3.1
3238
 
3239
+ use-callback-ref@1.3.2(@types/react@18.2.37)(react@18.2.0):
3240
+ dependencies:
3241
+ react: 18.2.0
3242
+ tslib: 2.8.1
3243
+ optionalDependencies:
3244
+ '@types/react': 18.2.37
3245
+
3246
+ use-sidecar@1.1.2(@types/react@18.2.37)(react@18.2.0):
3247
+ dependencies:
3248
+ detect-node-es: 1.1.0
3249
+ react: 18.2.0
3250
+ tslib: 2.8.1
3251
+ optionalDependencies:
3252
+ '@types/react': 18.2.37
3253
+
3254
  util-deprecate@1.0.2: {}
3255
 
3256
  uuid@9.0.1: {}
frontend/src/App.jsx CHANGED
@@ -26,7 +26,7 @@ const PublicRoute = ({ children }) => {
26
  return <div className="flex items-center justify-center h-screen">Loading...</div>;
27
  }
28
 
29
- return !token ? children : <Navigate to={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
- }