khronoz commited on
Commit
fdaf912
·
unverified ·
1 Parent(s): 4892344

V.0.3.0 (#28)

Browse files

* Up Version to 0.3.0 as 2 new features are added

* Updated profile,signout/signin with skeleton loading

* Refactored code

* Added Admin Page & Navlink to Admin Page

* Overhauled Q&A for document upload components & api routes

* Added SweetAlert2 Package

* Added API route for getting user public collections requests

* Update route return error & removed redundant API Calls

* Added handles for buttons and fetching of data

* Upgraded Next.js from 13 to 14 & packages

* Added api route methods for management & refactored code

* Update API route to retrieve user profile data from public schema

* Updated Admin page with menu and pages data

* Removed unused import & updated delete func fetch with correct API endpoint

* Updated API routes naming convention

* Added toast promise for indexing

* Bunch of fixes & added API routes handles for admin sub pages

* Created Backend API route for indexing of user uploaded documents

* Aligned requests to API parameters

* Aligned requests to API parameters

* Fixed logic error in checking valid token

* Updated Python Packages

* Updated Indexer API Router

* Alignment with updated API Routes

* Removed postprocessing scores in search

* Updated search section with back button

* Added redirect to unauthorized page for admin page and api routes

* Added API Router for deleting collections via asyncpg in pgvecottr 'vecs' Schema

* Update middleware to skip is-admin api route

* Added API Route and functions to delete single and multiple user collections

* Updated HF Spaces Metadata

* Updated Sync to HF Hub with new space URL

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .github/workflows/sync-to-hugging-face-hub.yml +1 -1
  2. README.md +1 -1
  3. backend/backend/app/api/routers/chat.py +5 -5
  4. backend/backend/app/api/routers/collections.py +107 -0
  5. backend/backend/app/api/routers/indexer.py +73 -0
  6. backend/backend/app/api/routers/query.py +6 -5
  7. backend/backend/app/api/routers/search.py +11 -7
  8. backend/backend/app/utils/auth.py +9 -8
  9. backend/backend/app/utils/contants.py +1 -1
  10. backend/backend/app/utils/index.py +72 -3
  11. backend/backend/main.py +4 -0
  12. backend/poetry.lock +0 -0
  13. backend/pyproject.toml +2 -0
  14. frontend/.eslintrc.json +3 -0
  15. frontend/app/admin/page.tsx +10 -0
  16. frontend/app/api/admin/collections-requests/approve/route.ts +43 -0
  17. frontend/app/api/admin/collections-requests/reject/route.ts +30 -0
  18. frontend/app/api/admin/collections-requests/route.ts +26 -0
  19. frontend/app/api/admin/collections/route.ts +26 -0
  20. frontend/app/api/admin/is-admin/route.ts +52 -0
  21. frontend/app/api/admin/users/demote/route.ts +28 -0
  22. frontend/app/api/admin/users/promote/route.ts +27 -0
  23. frontend/app/api/admin/users/route.ts +26 -0
  24. frontend/app/api/profile/route.ts +128 -7
  25. frontend/app/api/public/collections/route.ts +26 -0
  26. frontend/app/api/user/collections-requests/route.ts +133 -0
  27. frontend/app/api/user/collections/route.ts +183 -0
  28. frontend/app/components/admin-section.tsx +47 -0
  29. frontend/app/components/chat-section.tsx +13 -8
  30. frontend/app/components/header.tsx +65 -30
  31. frontend/app/components/profile-section.tsx +275 -0
  32. frontend/app/components/query-section.tsx +68 -29
  33. frontend/app/components/search-section.tsx +13 -8
  34. frontend/app/components/ui/admin/admin-collections-requests.tsx +277 -0
  35. frontend/app/components/ui/admin/admin-manage-collections.tsx +283 -0
  36. frontend/app/components/ui/admin/admin-manage-users.tsx +280 -0
  37. frontend/app/components/ui/admin/admin-menu.tsx +56 -0
  38. frontend/app/components/ui/admin/admin.interface.ts +8 -0
  39. frontend/app/components/ui/admin/index.ts +11 -0
  40. frontend/app/components/ui/autofill-prompt/autofill-prompt-dialog.tsx +30 -25
  41. frontend/app/components/ui/autofill-prompt/autofill-search-prompt-dialog.tsx +30 -26
  42. frontend/app/components/ui/chat/chat-selection.tsx +50 -36
  43. frontend/app/components/ui/chat/chat.interface.ts +4 -2
  44. frontend/app/components/ui/chat/use-copy-to-clipboard.tsx +2 -1
  45. frontend/app/components/ui/query/index.ts +6 -0
  46. frontend/app/components/ui/query/query-document-upload.tsx +318 -0
  47. frontend/app/components/ui/query/query-manage.tsx +575 -0
  48. frontend/app/components/ui/query/query-menu.tsx +59 -0
  49. frontend/app/components/ui/query/query-selection.tsx +68 -0
  50. frontend/app/components/ui/query/query.interface.ts +9 -0
.github/workflows/sync-to-hugging-face-hub.yml CHANGED
@@ -21,4 +21,4 @@ jobs:
21
  - name: Push to hub
22
  env:
23
  HF_TOKEN: ${{ secrets.HF_TOKEN }}
24
- run: git push https://khronoz:$HF_TOKEN@huggingface.co/spaces/khronoz/Smart-Retrieval-API main
 
21
  - name: Push to hub
22
  env:
23
  HF_TOKEN: ${{ secrets.HF_TOKEN }}
24
+ run: git push https://JTCSmartRetrieval:$HF_TOKEN@huggingface.co/spaces/SmartRetrieval/Smart-Retrieval-Demo-API main
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Smart Retrieval API
3
  emoji: 📝
4
  colorFrom: blue
5
  colorTo: indigo
 
1
  ---
2
+ title: Smart Retrieval Demo API
3
  emoji: 📝
4
  colorFrom: blue
5
  colorTo: indigo
backend/backend/app/api/routers/chat.py CHANGED
@@ -34,7 +34,7 @@ class _Message(BaseModel):
34
 
35
  class _ChatData(BaseModel):
36
  messages: List[_Message]
37
- document: str
38
 
39
 
40
  # custom prompt template to be used by chat engine
@@ -72,11 +72,11 @@ async def chat(
72
  data: _ChatData = Depends(json_to_model(_ChatData)),
73
  ):
74
  logger = logging.getLogger("uvicorn")
75
- # get the document set selected from the request body
76
- document_set = data.document
77
- logger.info(f"Document Set: {document_set}")
78
  # get the index for the selected document set
79
- index = get_index(collection_name=document_set)
80
  # check preconditions and get last message
81
  if len(data.messages) == 0:
82
  raise HTTPException(
 
34
 
35
  class _ChatData(BaseModel):
36
  messages: List[_Message]
37
+ collection_id: str
38
 
39
 
40
  # custom prompt template to be used by chat engine
 
72
  data: _ChatData = Depends(json_to_model(_ChatData)),
73
  ):
74
  logger = logging.getLogger("uvicorn")
75
+ # get the collection_id from the request body
76
+ collection_id = data.collection_id
77
+ logger.info(f"Chat -> Collection ID: {collection_id}")
78
  # get the index for the selected document set
79
+ index = get_index(collection_name=collection_id)
80
  # check preconditions and get last message
81
  if len(data.messages) == 0:
82
  raise HTTPException(
backend/backend/app/api/routers/collections.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import uuid
4
+ from typing import List
5
+
6
+ import asyncpg
7
+ from asyncpg.exceptions import PostgresError
8
+ from fastapi import APIRouter, Body, Depends, HTTPException
9
+ from pydantic import BaseModel
10
+
11
+ from backend.app.utils import auth
12
+
13
+
14
+ class _CollectionIds(BaseModel):
15
+ collection_ids: List[str]
16
+
17
+
18
+ collections_router = r = APIRouter(dependencies=[Depends(auth.validate_user)])
19
+
20
+ logger = logging.getLogger("uvicorn")
21
+
22
+ schema_name = "vecs"
23
+
24
+ """
25
+ This router is for deleting collections functionality.
26
+ """
27
+
28
+
29
+ def is_valid_uuidv4(uuid_str: str) -> bool:
30
+ try:
31
+ val = uuid.UUID(uuid_str, version=4)
32
+ except ValueError:
33
+ return False
34
+ return str(val) == uuid_str
35
+
36
+
37
+ async def drop_table(conn, collection_id):
38
+ try:
39
+ await conn.execute(
40
+ f'DROP TABLE IF EXISTS "{schema_name}"."{collection_id}" CASCADE'
41
+ )
42
+ return True
43
+ except PostgresError as e:
44
+ logger.error(f"Failed to drop table {collection_id}: {e}")
45
+ return False
46
+
47
+
48
+ @r.post("/delete/single")
49
+ async def delete_single(collection_id: str):
50
+ # Log the received collection_id
51
+ logger.info(f"Delete Collection: {collection_id}")
52
+
53
+ # Validate the collection_id to ensure it's a valid UUIDv4
54
+ if not is_valid_uuidv4(collection_id):
55
+ logger.error(f"Invalid collection_id: {collection_id}")
56
+ raise HTTPException(status_code=400, detail="Invalid collection_id format")
57
+
58
+ # Try to connect to the PostgreSQL database
59
+ db_url: str = os.environ.get("POSTGRES_CONNECTION_STRING")
60
+ if not db_url:
61
+ logger.error("POSTGRES_CONNECTION_STRING environment variable not set")
62
+ raise HTTPException(status_code=500, detail="Database configuration error")
63
+
64
+ try:
65
+ conn = await asyncpg.connect(dsn=db_url)
66
+ result = await drop_table(conn, collection_id)
67
+ except Exception as e:
68
+ logger.error(f"Failed to connect to the database: {e}")
69
+ raise HTTPException(status_code=500, detail="Failed to connect to the database")
70
+ finally:
71
+ await conn.close()
72
+
73
+ logger.debug(f"Delete Collection {collection_id}: {result}")
74
+ return {collection_id: result}
75
+
76
+
77
+ @r.post("/delete/multiple")
78
+ async def delete_multiple(collection_ids: _CollectionIds = Body(...)):
79
+ # Log the received collection_ids
80
+ logger.info(f"Delete Collections: {collection_ids.collection_ids}")
81
+
82
+ # Validate the collection_ids to ensure they are valid UUIDv4s
83
+ for collection_id in collection_ids.collection_ids:
84
+ if not is_valid_uuidv4(collection_id):
85
+ logger.error(f"Invalid collection_id: {collection_id}")
86
+ raise HTTPException(status_code=400, detail="Invalid collection_id format")
87
+
88
+ # Try to connect to the PostgreSQL database
89
+ db_url: str = os.environ.get("POSTGRES_CONNECTION_STRING")
90
+ if not db_url:
91
+ logger.error("POSTGRES_CONNECTION_STRING environment variable not set")
92
+ raise HTTPException(status_code=500, detail="Database configuration error")
93
+
94
+ results = {}
95
+ try:
96
+ conn = await asyncpg.connect(dsn=db_url)
97
+ for collection_id in collection_ids.collection_ids:
98
+ async with conn.transaction():
99
+ results[collection_id] = await drop_table(conn, collection_id)
100
+ except Exception as e:
101
+ logger.error(f"Failed to connect to the database: {e}")
102
+ raise HTTPException(status_code=500, detail="Failed to connect to the database")
103
+ finally:
104
+ await conn.close()
105
+
106
+ logger.debug(f"Delete Collections: {results}")
107
+ return results
backend/backend/app/api/routers/indexer.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import tempfile
4
+ from typing import List
5
+
6
+ from fastapi import APIRouter, Depends, Form, File, HTTPException, UploadFile
7
+ from fastapi.responses import JSONResponse
8
+
9
+ from backend.app.utils import auth, index
10
+
11
+ # Initialize the logger
12
+ logger = logging.getLogger("uvicorn")
13
+
14
+ # Initialize the API Router with dependencies
15
+ indexer_router = r = APIRouter(dependencies=[Depends(auth.validate_user)])
16
+
17
+ """
18
+ This router is for indexing of user uploaded documents functionality.
19
+ A list of files is received by the router and stored in a temporary directory.
20
+ The uploaded documents are indexed and stored in the vecs database.
21
+ """
22
+
23
+
24
+ @r.post("")
25
+ async def indexer(
26
+ collection_id: str = Form(...),
27
+ files: List[UploadFile] = File(...),
28
+ user=Depends(auth.validate_user),
29
+ ):
30
+ logger.info(f"Indexer -> Collection ID: {collection_id}")
31
+ logger.info(
32
+ f"User {user} is uploading {len(files)} files to collection {collection_id}"
33
+ )
34
+
35
+ try:
36
+ with tempfile.TemporaryDirectory() as temp_dir:
37
+ logger.info(f"Created temporary directory at {temp_dir}")
38
+
39
+ file_paths = []
40
+
41
+ for file in files:
42
+ contents = await file.read()
43
+ file_path = os.path.join(temp_dir, file.filename)
44
+ with open(file_path, "wb") as f:
45
+ f.write(contents)
46
+ file_paths.append(file_path)
47
+ logger.info(f"Saved file: {file.filename} at {file_path}")
48
+
49
+ # Call indexing function with the directory and collection_id
50
+ if len(file_paths) == 0:
51
+ raise HTTPException(
52
+ status_code=400, detail="No files uploaded for indexing"
53
+ )
54
+ if collection_id is None:
55
+ raise HTTPException(
56
+ status_code=400, detail="No collection ID provided for indexing"
57
+ )
58
+ if index.index_uploaded_files(temp_dir, collection_id):
59
+ logger.info("Files uploaded and indexed successfully.")
60
+ return JSONResponse(
61
+ status_code=200,
62
+ content={
63
+ "status": "Files uploaded and indexed successfully",
64
+ "filenames": [file.filename for file in files],
65
+ },
66
+ )
67
+ else:
68
+ raise HTTPException(
69
+ status_code=500, detail="Failed to upload and index files"
70
+ )
71
+ except Exception as e:
72
+ logger.error(f"Failed to upload and index files: {str(e)}")
73
+ raise HTTPException(status_code=500, detail="Failed to upload and index files.")
backend/backend/app/api/routers/query.py CHANGED
@@ -17,6 +17,7 @@ query_router = r = APIRouter(dependencies=[Depends(auth.validate_user)])
17
  This router is for query functionality which consist of query engine.
18
  The query engine is used to query the index.
19
  There is no chat memory used here, every query is independent of each other.
 
20
  """
21
 
22
 
@@ -27,7 +28,7 @@ class _Message(BaseModel):
27
 
28
  class _ChatData(BaseModel):
29
  messages: List[_Message]
30
- document: str
31
 
32
 
33
  @r.post("")
@@ -38,11 +39,11 @@ async def query(
38
  data: _ChatData = Depends(json_to_model(_ChatData)),
39
  ):
40
  logger = logging.getLogger("uvicorn")
41
- # get the document set selected from the request body
42
- document_set = data.document
43
- logger.info(f"Document Set: {document_set}")
44
  # get the index for the selected document set
45
- index = get_index(collection_name=document_set)
46
  # check preconditions and get last message which is query
47
  if len(data.messages) == 0:
48
  raise HTTPException(
 
17
  This router is for query functionality which consist of query engine.
18
  The query engine is used to query the index.
19
  There is no chat memory used here, every query is independent of each other.
20
+ // Currently Depreciated - Not used in the current version of the application
21
  """
22
 
23
 
 
28
 
29
  class _ChatData(BaseModel):
30
  messages: List[_Message]
31
+ collection_id: str
32
 
33
 
34
  @r.post("")
 
39
  data: _ChatData = Depends(json_to_model(_ChatData)),
40
  ):
41
  logger = logging.getLogger("uvicorn")
42
+ # get the collection_id selected from the request body
43
+ collection_id = data.collection_id
44
+ logger.info(f"Collection ID: {collection_id}")
45
  # get the index for the selected document set
46
+ index = get_index(collection_name=collection_id)
47
  # check preconditions and get last message which is query
48
  if len(data.messages) == 0:
49
  raise HTTPException(
backend/backend/app/api/routers/search.py CHANGED
@@ -1,7 +1,7 @@
1
  import logging
2
  import re
3
 
4
- from fastapi import APIRouter, Depends, HTTPException, Request, status
5
  from llama_index.postprocessor import SimilarityPostprocessor
6
  from llama_index.retrievers import VectorIndexRetriever
7
 
@@ -20,16 +20,15 @@ Instead it returns the relevant information from the index.
20
 
21
  @r.get("")
22
  async def search(
23
- request: Request,
24
  query: str = None,
25
- docSelected: str = None,
26
  ):
27
  # query = request.query_params.get("query")
28
  logger = logging.getLogger("uvicorn")
29
- logger.info(f"Document Set: {docSelected} | Search: {query}")
30
  # get the index for the selected document set
31
- index = get_index(collection_name=docSelected)
32
- if query is None or docSelected is None:
33
  raise HTTPException(
34
  status_code=status.HTTP_400_BAD_REQUEST,
35
  detail="No search info/document set provided",
@@ -62,6 +61,9 @@ async def search(
62
 
63
  logger.info(f"Filtered Search results similarity score: {filtered_results_scores}")
64
 
 
 
 
65
  response = []
66
  id = 1
67
  for node in filtered_results:
@@ -72,7 +74,9 @@ async def search(
72
  data = {}
73
  data["id"] = id
74
  data["file_name"] = node_metadata["file_name"]
75
- data["page_no"] = node_metadata["page_label"]
 
 
76
  cleaned_text = re.sub(
77
  "^_+ | _+$", "", node_dict["text"]
78
  ) # remove leading and trailing underscores
 
1
  import logging
2
  import re
3
 
4
+ from fastapi import APIRouter, Depends, HTTPException, status
5
  from llama_index.postprocessor import SimilarityPostprocessor
6
  from llama_index.retrievers import VectorIndexRetriever
7
 
 
20
 
21
  @r.get("")
22
  async def search(
 
23
  query: str = None,
24
+ collection_id: str = None,
25
  ):
26
  # query = request.query_params.get("query")
27
  logger = logging.getLogger("uvicorn")
28
+ logger.info(f"Document Set: {collection_id} | Search: {query}")
29
  # get the index for the selected document set
30
+ index = get_index(collection_name=collection_id)
31
+ if query is None or collection_id is None:
32
  raise HTTPException(
33
  status_code=status.HTTP_400_BAD_REQUEST,
34
  detail="No search info/document set provided",
 
61
 
62
  logger.info(f"Filtered Search results similarity score: {filtered_results_scores}")
63
 
64
+ # Skip postprocessing for now
65
+ filtered_results = query_results
66
+
67
  response = []
68
  id = 1
69
  for node in filtered_results:
 
74
  data = {}
75
  data["id"] = id
76
  data["file_name"] = node_metadata["file_name"]
77
+ data["page_no"] = (
78
+ node_metadata["page_label"] if "page_label" in node_metadata else "N/A"
79
+ )
80
  cleaned_text = re.sub(
81
  "^_+ | _+$", "", node_dict["text"]
82
  ) # remove leading and trailing underscores
backend/backend/app/utils/auth.py CHANGED
@@ -75,12 +75,12 @@ def get_user_from_JWT(token: str):
75
  if payload is not None:
76
  user_id = payload["sub"]
77
  # Try to get the user from the database using the user_id
78
- response = supabase.table("users").select("*").eq("id", user_id).execute()
79
  # print(response.data)
80
  if len(response.data) == 0:
81
  return False
82
  else:
83
- return True
84
  else:
85
  return False
86
 
@@ -109,7 +109,7 @@ async def validate_user(
109
  )
110
  else:
111
  logger.info("Validated API key successfully!")
112
- return None
113
  else:
114
  auth_token = (
115
  auth_token.strip()
@@ -125,16 +125,17 @@ async def validate_user(
125
  "Supabase JWT Secret is not set in Backend Service!"
126
  )
127
  if not isBearer:
128
- return (
129
  "Invalid token scheme. Please use the format 'Bearer [token]'"
130
  )
131
  # Verify the JWT token is valid
132
- if verify_jwt(jwtoken=jwtoken):
133
- return "Invalid token. Please provide a valid token."
134
  # Check if the user exists in the database
135
- if get_user_from_JWT(token=jwtoken):
 
136
  logger.info("Validated User's Auth Token successfully!")
137
- return None
138
  else:
139
  raise ValueError("User does not exist in the database!")
140
  else:
 
75
  if payload is not None:
76
  user_id = payload["sub"]
77
  # Try to get the user from the database using the user_id
78
+ response = supabase.table("users").select("id").eq("id", user_id).execute()
79
  # print(response.data)
80
  if len(response.data) == 0:
81
  return False
82
  else:
83
+ return response.data[0]["id"]
84
  else:
85
  return False
86
 
 
109
  )
110
  else:
111
  logger.info("Validated API key successfully!")
112
+ return "Authenticated via API Key"
113
  else:
114
  auth_token = (
115
  auth_token.strip()
 
125
  "Supabase JWT Secret is not set in Backend Service!"
126
  )
127
  if not isBearer:
128
+ raise ValueError(
129
  "Invalid token scheme. Please use the format 'Bearer [token]'"
130
  )
131
  # Verify the JWT token is valid
132
+ if verify_jwt(jwtoken=jwtoken) is False:
133
+ raise ValueError("Invalid token. Please provide a valid token.")
134
  # Check if the user exists in the database
135
+ user = get_user_from_JWT(token=jwtoken)
136
+ if user:
137
  logger.info("Validated User's Auth Token successfully!")
138
+ return user
139
  else:
140
  raise ValueError("User does not exist in the database!")
141
  else:
backend/backend/app/utils/contants.py CHANGED
@@ -34,7 +34,7 @@ EMBED_MODEL_DIMENSIONS = 384 # MiniLM-L6-v2 uses 384 dimensions
34
  DEF_EMBED_MODEL_DIMENSIONS = (
35
  1536 # Default embedding model dimensions used by OpenAI text-embedding-ada-002
36
  )
37
- EMBED_BATCH_SIZE = 100 # batch size for openai embeddings
38
 
39
  # Prompt Helper Constants
40
  # set maximum input size
 
34
  DEF_EMBED_MODEL_DIMENSIONS = (
35
  1536 # Default embedding model dimensions used by OpenAI text-embedding-ada-002
36
  )
37
+ EMBED_BATCH_SIZE = 10 # batch size for openai embeddings
38
 
39
  # Prompt Helper Constants
40
  # set maximum input size
backend/backend/app/utils/index.py CHANGED
@@ -126,7 +126,7 @@ def create_index():
126
  collection_names = os.listdir(DATA_DIR)
127
  # to create each folder as a collection in local storage
128
  for collection_name in collection_names:
129
- logger.info(f"Checking if [{collection_names}] index exists locally...")
130
  # build the new data directory
131
  new_data_dir = os.path.join(DATA_DIR, collection_name)
132
  # build the new storage directory
@@ -137,7 +137,7 @@ def create_index():
137
  or len(os.listdir(new_storage_dir))
138
  < 4 # 4 files should be present if using simplevectorstore
139
  ):
140
- logger.info(f"Creating [{collection_names}] index")
141
  # load the documents and create the index
142
  try:
143
  documents = SimpleDirectoryReader(
@@ -194,7 +194,7 @@ def create_index():
194
  logger.info(f"Finished creating [{collection_name}] vector store")
195
 
196
 
197
- def load_existing_index(collection_name="PSSCOC"):
198
  # load the existing index
199
  if USE_LOCAL_VECTOR_STORE:
200
  # create the storage directory
@@ -237,6 +237,75 @@ def load_existing_index(collection_name="PSSCOC"):
237
  return index
238
 
239
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  def get_index(collection_name):
241
  # load the index from storage
242
  index = load_existing_index(collection_name)
 
126
  collection_names = os.listdir(DATA_DIR)
127
  # to create each folder as a collection in local storage
128
  for collection_name in collection_names:
129
+ logger.info(f"Checking if [{collection_name}] index exists locally...")
130
  # build the new data directory
131
  new_data_dir = os.path.join(DATA_DIR, collection_name)
132
  # build the new storage directory
 
137
  or len(os.listdir(new_storage_dir))
138
  < 4 # 4 files should be present if using simplevectorstore
139
  ):
140
+ logger.info(f"Creating [{collection_name}] index")
141
  # load the documents and create the index
142
  try:
143
  documents = SimpleDirectoryReader(
 
194
  logger.info(f"Finished creating [{collection_name}] vector store")
195
 
196
 
197
+ def load_existing_index(collection_name):
198
  # load the existing index
199
  if USE_LOCAL_VECTOR_STORE:
200
  # create the storage directory
 
237
  return index
238
 
239
 
240
+ # Index the files in the tempfile data directory and store the index in local storage or Supabase
241
+ def index_uploaded_files(data_dir, collection_name):
242
+ # if use local vector store, create & store the index locally
243
+ if USE_LOCAL_VECTOR_STORE:
244
+ logger.info(f"Checking if [{collection_name}] index exists locally...")
245
+ # build the new storage directory
246
+ new_storage_dir = os.path.join(STORAGE_DIR, collection_name)
247
+ # check if storage folder and index files already exists
248
+ if (
249
+ not os.path.exists(new_storage_dir)
250
+ or len(os.listdir(new_storage_dir))
251
+ < 4 # 4 files should be present if using simplevectorstore
252
+ ):
253
+ logger.info(f"Creating [{collection_name}] index")
254
+ # load the documents and create the index
255
+ try:
256
+ documents = SimpleDirectoryReader(
257
+ input_dir=data_dir, recursive=True
258
+ ).load_data()
259
+ except ValueError as e:
260
+ logger.error(f"{e}")
261
+ return False
262
+ index = VectorStoreIndex.from_documents(
263
+ documents=documents,
264
+ service_context=service_context,
265
+ show_progress=True,
266
+ )
267
+ # store it for later
268
+ index.storage_context.persist(STORAGE_DIR)
269
+ logger.info(f"Finished creating new index. Stored in {STORAGE_DIR}")
270
+ return True
271
+ else:
272
+ # do nothing
273
+ logger.info(f"Index already exist at {STORAGE_DIR}...")
274
+ return True
275
+ # else, create & store the index in Supabase pgvector
276
+ else:
277
+ # check if remote storage already exists
278
+ logger.info(f"Checking if [{collection_name}] index exists in Supabase...")
279
+ # set the dimension based on the LLM model used
280
+ dimension = (
281
+ EMBED_MODEL_DIMENSIONS if USE_LOCAL_LLM else DEF_EMBED_MODEL_DIMENSIONS
282
+ )
283
+ # create the vector store, will create the collection if it does not exist
284
+ vector_store = SupabaseVectorStore(
285
+ postgres_connection_string=os.getenv("POSTGRES_CONNECTION_STRING"),
286
+ collection_name=collection_name,
287
+ dimension=dimension,
288
+ )
289
+ # create the storage context
290
+ storage_context = StorageContext.from_defaults(vector_store=vector_store)
291
+ logger.info(f"Creating [{collection_name}] index")
292
+ # load the documents and create the index
293
+ try:
294
+ documents = SimpleDirectoryReader(
295
+ input_dir=data_dir, recursive=True
296
+ ).load_data()
297
+ except ValueError as e:
298
+ logger.error(f"{e}")
299
+ return False
300
+ index = VectorStoreIndex.from_documents(
301
+ documents=documents,
302
+ storage_context=storage_context,
303
+ show_progress=True,
304
+ )
305
+ logger.info(f"Finished creating [{collection_name}] vector store")
306
+ return True
307
+
308
+
309
  def get_index(collection_name):
310
  # load the index from storage
311
  index = load_existing_index(collection_name)
backend/backend/main.py CHANGED
@@ -9,8 +9,10 @@ from torch.cuda import is_available as is_cuda_available
9
 
10
  from backend.app.api.routers.chat import chat_router
11
  from backend.app.api.routers.healthcheck import healthcheck_router
 
12
  from backend.app.api.routers.query import query_router
13
  from backend.app.api.routers.search import search_router
 
14
 
15
  load_dotenv()
16
 
@@ -60,6 +62,8 @@ app.include_router(chat_router, prefix="/api/chat")
60
  app.include_router(query_router, prefix="/api/query")
61
  app.include_router(search_router, prefix="/api/search")
62
  app.include_router(healthcheck_router, prefix="/api/healthcheck")
 
 
63
 
64
 
65
  # Redirect to the /docs endpoint
 
9
 
10
  from backend.app.api.routers.chat import chat_router
11
  from backend.app.api.routers.healthcheck import healthcheck_router
12
+ from backend.app.api.routers.indexer import indexer_router
13
  from backend.app.api.routers.query import query_router
14
  from backend.app.api.routers.search import search_router
15
+ from backend.app.api.routers.collections import collections_router
16
 
17
  load_dotenv()
18
 
 
62
  app.include_router(query_router, prefix="/api/query")
63
  app.include_router(search_router, prefix="/api/search")
64
  app.include_router(healthcheck_router, prefix="/api/healthcheck")
65
+ app.include_router(indexer_router, prefix="/api/indexer")
66
+ app.include_router(collections_router, prefix="/api/collections")
67
 
68
 
69
  # Redirect to the /docs endpoint
backend/poetry.lock CHANGED
The diff for this file is too large to render. See raw diff
 
backend/pyproject.toml CHANGED
@@ -20,6 +20,8 @@ doc2docx = "^0.2.4"
20
  supabase = "^2.4.0"
21
  pyjwt = "^2.8.0"
22
  vecs = "^0.4.3"
 
 
23
 
24
  [tool.poetry.group.dev]
25
  optional = true
 
20
  supabase = "^2.4.0"
21
  pyjwt = "^2.8.0"
22
  vecs = "^0.4.3"
23
+ python-multipart = "^0.0.9"
24
+ asyncpg = "^0.29.0"
25
 
26
  [tool.poetry.group.dev]
27
  optional = true
frontend/.eslintrc.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "extends": "next/core-web-vitals"
3
+ }
frontend/app/admin/page.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import AdminSection from "@/app/components/admin-section";
4
+
5
+ export default function Query() {
6
+
7
+ return (
8
+ <AdminSection />
9
+ );
10
+ }
frontend/app/api/admin/collections-requests/approve/route.ts ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ // POST request to approve the user's public collections request status in the database (Used by admin)
5
+ export async function POST(request: NextRequest) {
6
+ // Create a new Supabase client
7
+ const supabase = createClient(
8
+ process.env.SUPABASE_URL ?? '',
9
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
10
+ { db: { schema: 'public' } },
11
+ );
12
+
13
+ // Retrieve the collection_id from the request body
14
+ const { collection_id, is_make_public } = await request?.json();
15
+
16
+ // Update the user's public collections request data in the database, set is_pending = false
17
+ const { data: updatedUserPubCollectionsReq, error: updatedUserPubCollReqErr } = await supabase
18
+ .from('public_collections_requests')
19
+ .update({ is_pending: false, is_approved: true })
20
+ .eq('collection_id', collection_id);
21
+
22
+ if (updatedUserPubCollReqErr) {
23
+ console.error('Error updating user public collections request data in database:', updatedUserPubCollReqErr.message);
24
+ return NextResponse.json({ error: updatedUserPubCollReqErr.message }, { status: 500 });
25
+ }
26
+
27
+ // Update the user's collections data in the database, set is_public = true
28
+ console.log('is_public:', is_make_public);
29
+ const { data: updatedUserPubCollections, error: updatedUserPubCollErr } = await supabase
30
+ .from('collections')
31
+ .update({ is_public: is_make_public})
32
+ .eq('collection_id', collection_id);
33
+
34
+ if (updatedUserPubCollErr) {
35
+ console.error('Error updating user public collections data in database:', updatedUserPubCollErr.message);
36
+ return NextResponse.json({ error: updatedUserPubCollErr.message }, { status: 500 });
37
+ }
38
+
39
+ // console.log('Admin: User Public Collections Requests:', userPubCollectionsReq);
40
+ // console.log('Admin: User Public Collections:', userPubCollections);
41
+
42
+ return NextResponse.json({ updatedUserPubCollectionsReq, updatedUserPubCollections });
43
+ }
frontend/app/api/admin/collections-requests/reject/route.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ // POST request to reject the user's public collections request status in the database (Used by admin)
5
+ export async function POST(request: NextRequest) {
6
+ // Create a new Supabase client
7
+ const supabase = createClient(
8
+ process.env.SUPABASE_URL ?? '',
9
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
10
+ { db: { schema: 'public' } },
11
+ );
12
+
13
+ // Retrieve the collection_id from the request body
14
+ const { collection_id } = await request?.json();
15
+
16
+ // Update the user's public collections request data in the database, set is_pending = false
17
+ const { data: updatedUserPubCollectionsReq, error: updatedUserPubCollErr } = await supabase
18
+ .from('public_collections_requests')
19
+ .update({ is_pending: false, is_approved: false})
20
+ .eq('collection_id', collection_id);
21
+
22
+ if (updatedUserPubCollErr) {
23
+ console.error('Error updating user public collections request data in database:', updatedUserPubCollErr.message);
24
+ return NextResponse.json({ error: updatedUserPubCollErr.message }, { status: 500 });
25
+ }
26
+
27
+ // console.log('User Public Collections Requests:', userPubCollectionsReq);
28
+
29
+ return NextResponse.json({ updatedUserPubCollectionsReq });
30
+ }
frontend/app/api/admin/collections-requests/route.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ export async function GET(request: NextRequest) {
5
+ // Create a new Supabase client
6
+ const supabase = createClient(
7
+ process.env.SUPABASE_URL ?? '',
8
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
9
+ { db: { schema: 'public' } },
10
+ );
11
+
12
+ // Retrieve the public collections requests data from the database
13
+ const { data: pubCollectionsReq, error: pubCollErr } = await supabase
14
+ .from('public_collections_requests')
15
+ .select('collection_id, is_make_public, is_pending, is_approved, created_at, updated_at, collections (collection_id, id, display_name, description, is_public, users (id, name, email))')
16
+ .eq('is_pending', true);
17
+
18
+ if (pubCollErr) {
19
+ console.error('Error fetching public collection request data from database:', pubCollErr.message);
20
+ return NextResponse.json({ error: pubCollErr.message }, { status: 500 });
21
+ }
22
+
23
+ console.log('Collections Request:', pubCollectionsReq);
24
+
25
+ return NextResponse.json({ pubCollectionsReq: pubCollectionsReq });
26
+ }
frontend/app/api/admin/collections/route.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ // GET request to retrieve the collections data from the database
5
+ export async function GET(request: NextRequest) {
6
+ // Create a new Supabase client
7
+ const supabase = createClient(
8
+ process.env.SUPABASE_URL ?? '',
9
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
10
+ { db: { schema: 'public' } },
11
+ );
12
+
13
+ // Retrieve the collections requests data from the database
14
+ const { data: collections, error: collErr } = await supabase
15
+ .from('collections')
16
+ .select('collection_id, id, display_name, description, is_public, created_at, users (id, name, email)');
17
+
18
+ if (collErr) {
19
+ console.error('Error fetching collections data from database:', collErr.message);
20
+ return NextResponse.json({ error: collErr.message }, { status: 500 });
21
+ }
22
+
23
+ // console.log('Collections:', collections);
24
+
25
+ return NextResponse.json({ collections: collections });
26
+ }
frontend/app/api/admin/is-admin/route.ts ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ // GET request to retrieve the users data from the database
5
+ export async function GET(request: NextRequest) {
6
+ // Retrieve the session token from the request cookies
7
+ const session = request.cookies.get('next-auth.session-token') || request.cookies.get('__Secure-next-auth.session-token');
8
+
9
+ // Create a new Supabase client
10
+ const supabaseAuth = createClient(
11
+ process.env.SUPABASE_URL ?? '',
12
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
13
+ { db: { schema: 'next_auth' } },
14
+ );
15
+
16
+ const supabase = createClient(
17
+ process.env.SUPABASE_URL ?? '',
18
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
19
+ { db: { schema: 'public' } },
20
+ );
21
+
22
+ // Retrieve the user's ID from the session token
23
+ const { data: sessionData, error: sessionError } = await supabaseAuth
24
+ .from('sessions')
25
+ .select('userId')
26
+ .eq('sessionToken', session?.value)
27
+ .single();
28
+
29
+ const userId = sessionData?.userId;
30
+
31
+ if (sessionError) {
32
+ console.error('Error fetching session from database:', sessionError.message);
33
+ return NextResponse.json({ error: sessionError.message }, { status: 500 });
34
+ }
35
+
36
+ // Ensure user is an admin
37
+ const { data: userData, error: userError } = await supabase
38
+ .from('users')
39
+ .select('id, admins (id)')
40
+ .eq('id', userId)
41
+ .single();
42
+
43
+ if (userError) {
44
+ console.error('Error fetching user data from database:', userError.message);
45
+ return NextResponse.json({ error: userError.message }, { status: 500 });
46
+ }
47
+
48
+ // console.log('userData:', userData);
49
+ const isAdmin = userData?.admins.length > 0;
50
+
51
+ return NextResponse.json({ isAdmin: isAdmin });
52
+ }
frontend/app/api/admin/users/demote/route.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ // POST request to demote a user to a normal user
5
+ export async function POST(request: NextRequest) {
6
+ // Create a new Supabase client
7
+
8
+ const supabase = createClient(
9
+ process.env.SUPABASE_URL ?? '',
10
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
11
+ { db: { schema: 'public' } },
12
+ );
13
+
14
+ // Retrieve the user id from the request body
15
+ const { id } = await request?.json();
16
+
17
+ const { data: updatedAdminsData, error: updateAdminsError } = await supabase
18
+ .from('admins')
19
+ .delete()
20
+ .eq('id', id);
21
+
22
+ if (updateAdminsError) {
23
+ console.error('Error removing admin from database:', updateAdminsError.message);
24
+ return NextResponse.json({ error: updateAdminsError.message }, { status: 500 });
25
+ }
26
+
27
+ return NextResponse.json({ updatedAdminsData });
28
+ }
frontend/app/api/admin/users/promote/route.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ // POST request to promote a user to an admin
5
+ export async function POST(request: NextRequest) {
6
+ // Create a new Supabase client
7
+
8
+ const supabase = createClient(
9
+ process.env.SUPABASE_URL ?? '',
10
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
11
+ { db: { schema: 'public' } },
12
+ );
13
+
14
+ // Retrieve the user id from the request body
15
+ const { id } = await request?.json();
16
+
17
+ const { data: updatedAdminsData, error: updateAdminsError } = await supabase
18
+ .from('admins')
19
+ .insert({ id: id })
20
+
21
+ if (updateAdminsError) {
22
+ console.error('Error inserting admin to database:', updateAdminsError.message);
23
+ return NextResponse.json({ error: updateAdminsError.message }, { status: 500 });
24
+ }
25
+
26
+ return NextResponse.json({ updatedAdminsData });
27
+ }
frontend/app/api/admin/users/route.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ // GET request to retrieve the users data from the database
5
+ export async function GET(request: NextRequest) {
6
+ // Create a new Supabase client
7
+
8
+ const supabase = createClient(
9
+ process.env.SUPABASE_URL ?? '',
10
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
11
+ { db: { schema: 'public' } },
12
+ );
13
+
14
+ const { data: usersData, error: usersError } = await supabase
15
+ .from('users')
16
+ .select('id, name, email, admins (id)');
17
+
18
+ if (usersError) {
19
+ console.error('Error fetching users data from database:', usersError.message);
20
+ return NextResponse.json({ error: usersError.message }, { status: 500 });
21
+ }
22
+
23
+ // console.log('usersData:', usersData);
24
+
25
+ return NextResponse.json({ users: usersData });
26
+ }
frontend/app/api/profile/route.ts CHANGED
@@ -1,21 +1,26 @@
1
  import { createClient } from '@supabase/supabase-js';
2
  import { NextRequest, NextResponse } from "next/server";
3
 
 
4
  export async function GET(request: NextRequest) {
5
- const { pathname, origin } = request.nextUrl;
6
- const signinPage = new URL('/sign-in', origin);
7
  // Retrieve the session token from the request cookies
8
  const session = request.cookies.get('next-auth.session-token') || request.cookies.get('__Secure-next-auth.session-token');
9
 
10
  // Create a new Supabase client
11
- const supabase = createClient(
12
  process.env.SUPABASE_URL ?? '',
13
  process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
14
  { db: { schema: 'next_auth' } },
15
  );
16
 
 
 
 
 
 
 
17
  // Retrieve the user's ID from the session token
18
- const { data: sessionData, error: sessionError } = await supabase
19
  .from('sessions')
20
  .select('userId')
21
  .eq('sessionToken', session?.value)
@@ -25,7 +30,7 @@ export async function GET(request: NextRequest) {
25
 
26
  if (sessionError) {
27
  console.error('Error fetching session from database:', sessionError.message);
28
- return NextResponse.redirect(signinPage.href, { status: 302 });
29
  }
30
 
31
  // Retrieve the user's profile data
@@ -37,10 +42,126 @@ export async function GET(request: NextRequest) {
37
 
38
  if (userError) {
39
  console.error('Error fetching user data from database:', userError.message);
40
- return NextResponse.redirect(signinPage.href, { status: 302 });
41
  }
42
 
43
- // console.log('userData:', userData);
44
 
45
  return NextResponse.json({ userData: userData });
46
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { createClient } from '@supabase/supabase-js';
2
  import { NextRequest, NextResponse } from "next/server";
3
 
4
+ // GET request to retrieve the user's profile data
5
  export async function GET(request: NextRequest) {
 
 
6
  // Retrieve the session token from the request cookies
7
  const session = request.cookies.get('next-auth.session-token') || request.cookies.get('__Secure-next-auth.session-token');
8
 
9
  // Create a new Supabase client
10
+ const supabaseAuth = createClient(
11
  process.env.SUPABASE_URL ?? '',
12
  process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
13
  { db: { schema: 'next_auth' } },
14
  );
15
 
16
+ const supabase = createClient(
17
+ process.env.SUPABASE_URL ?? '',
18
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
19
+ { db: { schema: 'public' } },
20
+ );
21
+
22
  // Retrieve the user's ID from the session token
23
+ const { data: sessionData, error: sessionError } = await supabaseAuth
24
  .from('sessions')
25
  .select('userId')
26
  .eq('sessionToken', session?.value)
 
30
 
31
  if (sessionError) {
32
  console.error('Error fetching session from database:', sessionError.message);
33
+ return NextResponse.json({ error: sessionError.message }, { status: 500 });
34
  }
35
 
36
  // Retrieve the user's profile data
 
42
 
43
  if (userError) {
44
  console.error('Error fetching user data from database:', userError.message);
45
+ return NextResponse.json({ error: userError.message }, { status: 500 });
46
  }
47
 
48
+ console.log('userData:', userData);
49
 
50
  return NextResponse.json({ userData: userData });
51
  }
52
+
53
+ // PUT request to update the user's profile data
54
+ export async function PUT(request: NextRequest) {
55
+ // Retrieve the session token from the request cookies
56
+ const session = request.cookies.get('next-auth.session-token') || request.cookies.get('__Secure-next-auth.session-token');
57
+
58
+ // Retrieve the user's data from the request body
59
+ const { userId, name, email, image } = await request.json();
60
+
61
+ // Create a new Supabase client
62
+
63
+ const supabase = createClient(
64
+ process.env.SUPABASE_URL ?? '',
65
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
66
+ { db: { schema: 'public' } },
67
+ );
68
+
69
+ // Update the user's profile data
70
+ const { data: updatedUserData, error: updateError } = await supabase
71
+ .from('users')
72
+ .update({ name, email, image })
73
+ .eq('id', userId);
74
+
75
+ if (updateError) {
76
+ console.error('Error updating user data in database:', updateError.message);
77
+ return NextResponse.json({ error: updateError.message }, { status: 500 });
78
+ }
79
+
80
+ // console.log('updatedUserData:', updatedUserData);
81
+
82
+ return NextResponse.json({ updatedUserData });
83
+ }
84
+
85
+ // DELETE request to delete the user's profile & all data
86
+ export async function DELETE(request: NextRequest) {
87
+ // Create a new Supabase client
88
+ const supabaseAuth = createClient(
89
+ process.env.SUPABASE_URL ?? '',
90
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
91
+ { db: { schema: 'next_auth' } },
92
+ );
93
+
94
+ const supabase = createClient(
95
+ process.env.SUPABASE_URL ?? '',
96
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
97
+ { db: { schema: 'public' } },
98
+ );
99
+
100
+ // Retrieve the user's ID from the request body
101
+ const { userId } = await request?.json();
102
+
103
+ // Retrieve the authorization token from the request headers
104
+ let authorization = request.headers.get('Authorization');
105
+
106
+ // Public API key
107
+ let api_key = null;
108
+
109
+ // If no session, use the public API key
110
+ if (authorization === null || authorization === undefined || authorization.includes('undefined')) {
111
+ console.log('No authorization token found, using public API key');
112
+ api_key = process.env.BACKEND_API_KEY as string;
113
+ authorization = null; // Clear the authorization token
114
+ }
115
+
116
+ // Fetch the collection_ids of the user's collections from the collections table in public schema
117
+ const { data: collectionIds, error: collectionError } = await supabase
118
+ .from('collections')
119
+ .select('collection_id')
120
+ .eq('id', userId);
121
+
122
+ if (collectionError) {
123
+ console.error('Error fetching user collections from database:', collectionError.message);
124
+ return NextResponse.json({ error: collectionError.message }, { status: 500 });
125
+ }
126
+
127
+ // Convert collectionIds to an array of strings with only the collection_id values
128
+ const collectionIdsArray = collectionIds.map((collection: any) => collection.collection_id);
129
+
130
+ console.log('collectionIdsArray:', collectionIdsArray);
131
+
132
+ // Log the request body before sending the POST request
133
+ console.log('Request Body:', JSON.stringify({ collection_ids: collectionIdsArray }));
134
+
135
+ // Delete the user's collection data from vecs schema via POST request to Backend API
136
+ const deleteVecsResponse = await fetch(`${process.env.DELETE_MULTI_COLLECTION_API}`, {
137
+ method: 'POST',
138
+ headers: {
139
+ 'Content-Type': 'application/json',
140
+ 'Authorization': authorization,
141
+ 'X-API-Key': api_key,
142
+ } as any,
143
+ body: JSON.stringify({ collection_ids: collectionIdsArray }), // Send collection IDs as an array
144
+ });
145
+
146
+ if (!deleteVecsResponse.ok) {
147
+ console.error('Error deleting', collectionIdsArray, 'from vecs schema:', deleteVecsResponse.statusText);
148
+ return NextResponse.json({ error: deleteVecsResponse.statusText }, { status: deleteVecsResponse.status });
149
+ }
150
+
151
+ // Delete the user's profile data from users table in next_auth schema (and all related data via cascaded delete for tables in both publicand next_auth schema)
152
+ const { data: deletedUserData, error: deleteError } = await supabaseAuth
153
+ .from('users')
154
+ .delete()
155
+ .eq('id', userId)
156
+
157
+ // TODO: Delete the user's vector collection from the vecs schema
158
+
159
+ if (deleteError) {
160
+ console.error('Error deleting user data from database:', deleteError.message);
161
+ return NextResponse.json({ error: deleteError.message }, { status: 500 });
162
+ }
163
+
164
+ console.log('deletedUserData:', deletedUserData, 'collectionIds:', collectionIdsArray, 'deleteVecsResponse:', deleteVecsResponse);
165
+
166
+ return NextResponse.json({ deletedUserData });
167
+ }
frontend/app/api/public/collections/route.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ export async function GET(request: NextRequest) {
5
+ // Create a new Supabase client
6
+ const supabase = createClient(
7
+ process.env.SUPABASE_URL ?? '',
8
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
9
+ { db: { schema: 'public' } },
10
+ );
11
+
12
+ // Retrieve the collections id and data from the database where is_public = true
13
+ const { data: publicCollections, error: pubCollErr } = await supabase
14
+ .from('collections')
15
+ .select('collection_id, display_name, description, created_at')
16
+ .eq('is_public', true);
17
+
18
+ if (pubCollErr) {
19
+ console.error('Error fetching public collection data from database:', pubCollErr.message);
20
+ return NextResponse.json({ error: pubCollErr.message }, { status: 500 });
21
+ }
22
+
23
+ // console.log('publicCollections:', publicCollections);
24
+
25
+ return NextResponse.json({ publicCollections: publicCollections });
26
+ }
frontend/app/api/user/collections-requests/route.ts ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ // GET request to retrieve the user's public collections requests data from the database
5
+ export async function GET(request: NextRequest) {
6
+ // Retrieve the session token from the request cookies
7
+ const session = request.cookies.get('next-auth.session-token') || request.cookies.get('__Secure-next-auth.session-token');
8
+
9
+ // Create a new Supabase client
10
+ const supabaseAuth = createClient(
11
+ process.env.SUPABASE_URL ?? '',
12
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
13
+ { db: { schema: 'next_auth' } },
14
+ );
15
+
16
+ const supabase = createClient(
17
+ process.env.SUPABASE_URL ?? '',
18
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
19
+ { db: { schema: 'public' } },
20
+ );
21
+
22
+ // Retrieve the user's ID from the session token
23
+ const { data: sessionData, error: sessionError } = await supabaseAuth
24
+ .from('sessions')
25
+ .select('userId')
26
+ .eq('sessionToken', session?.value)
27
+ .single();
28
+
29
+ const userId = sessionData?.userId;
30
+
31
+ if (sessionError) {
32
+ console.error('Error fetching session from database:', sessionError.message);
33
+ return NextResponse.json({ error: sessionError.message }, { status: 500 });
34
+ }
35
+
36
+ // Retrieve the user's collections and public collections requests data via inner join from the database
37
+ const { data: userPubCollectionsReq, error: userPubCollErr } = await supabase
38
+ .from('collections')
39
+ .select('collection_id, display_name, description, is_public, created_at, public_collections_requests (collection_id, is_make_public, is_pending, is_approved, created_at, updated_at)')
40
+ .eq('id', userId);
41
+
42
+ if (userPubCollErr) {
43
+ console.error('Error fetching user public collections requests data from database:', userPubCollErr.message);
44
+ return NextResponse.json({ error: userPubCollErr.message }, { status: 500 });
45
+ }
46
+
47
+ // console.log('User Public Collections Requests:', userPubCollectionsReq.map(item => item.public_collections_requests));
48
+
49
+ return NextResponse.json({ userPubCollectionsReq: userPubCollectionsReq });
50
+ }
51
+
52
+ // POST request to insert the user's public collections request data into the database if not exist (Used by user)
53
+ export async function POST(request: NextRequest) {
54
+ // Create a new Supabase client
55
+ const supabase = createClient(
56
+ process.env.SUPABASE_URL ?? '',
57
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
58
+ { db: { schema: 'public' } },
59
+ );
60
+
61
+ // Retrieve the collection_id from the request body
62
+ const { collection_id, is_make_public } = await request?.json();
63
+
64
+ // Insert the user's public collections request data into the database
65
+ const { data: newUserPubCollectionsReq, error: newUserPubCollErr } = await supabase
66
+ .from('public_collections_requests')
67
+ .insert([{ collection_id, is_make_public }]);
68
+
69
+ if (newUserPubCollErr) {
70
+ console.error('Error inserting user public collections request data into database:', newUserPubCollErr.message);
71
+ return NextResponse.json({ error: newUserPubCollErr.message }, { status: 500 });
72
+ }
73
+
74
+ // console.log('User Public Collections Requests:', userPubCollectionsReq);
75
+
76
+ return NextResponse.json({ newUserPubCollectionsReq });
77
+ }
78
+
79
+ // PUT request to update the user's public collections request data in the database (Used by user)
80
+ export async function PUT(request: NextRequest) {
81
+ // Create a new Supabase client
82
+ const supabase = createClient(
83
+ process.env.SUPABASE_URL ?? '',
84
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
85
+ { db: { schema: 'public' } },
86
+ );
87
+
88
+ // Retrieve the collection_id from the request body
89
+ const { collection_id, is_make_public } = await request?.json();
90
+
91
+ // Update the user's public collections request data in the database, set is_pending = true
92
+ const { data: updatedUserPubCollectionsReq, error: updatedUserPubCollErr } = await supabase
93
+ .from('public_collections_requests')
94
+ .update({ is_make_public: is_make_public, is_pending: true, is_approved: false })
95
+ .eq('collection_id', collection_id);
96
+
97
+ if (updatedUserPubCollErr) {
98
+ console.error('Error updating user public collections request data in database:', updatedUserPubCollErr.message);
99
+ return NextResponse.json({ error: updatedUserPubCollErr.message }, { status: 500 });
100
+ }
101
+
102
+ // console.log('User Public Collections Requests:', userPubCollectionsReq);
103
+
104
+ return NextResponse.json({ updatedUserPubCollectionsReq });
105
+ }
106
+
107
+ // DELETE request to delete the user's public collections request data from the database
108
+ export async function DELETE(request: NextRequest) {
109
+ // Create a new Supabase client
110
+ const supabase = createClient(
111
+ process.env.SUPABASE_URL ?? '',
112
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
113
+ { db: { schema: 'public' } },
114
+ );
115
+
116
+ // Retrieve the collection_id from the request body
117
+ const { collection_id } = await request?.json();
118
+
119
+ // Delete the user's public collections request data from the database
120
+ const { data: deletedUserPubCollectionsReq, error: deletedUserPubCollErr } = await supabase
121
+ .from('public_collections_requests')
122
+ .delete()
123
+ .eq('collection_id', collection_id);
124
+
125
+ if (deletedUserPubCollErr) {
126
+ console.error('Error deleting user public collections request data from database:', deletedUserPubCollErr.message);
127
+ return NextResponse.json({ error: deletedUserPubCollErr.message }, { status: 500 });
128
+ }
129
+
130
+ // console.log('User Public Collections Requests:', userPubCollectionsReq);
131
+
132
+ return NextResponse.json({ deletedUserPubCollectionsReq });
133
+ }
frontend/app/api/user/collections/route.ts ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ // GET request to retrieve the user's collections data from the database
5
+ export async function GET(request: NextRequest) {
6
+ const { pathname, origin } = request.nextUrl;
7
+ const signinPage = new URL('/sign-in', origin);
8
+ // Retrieve the session token from the request cookies
9
+ const session = request.cookies.get('next-auth.session-token') || request.cookies.get('__Secure-next-auth.session-token');
10
+
11
+ // Create a new Supabase client
12
+ const supabaseAuth = createClient(
13
+ process.env.SUPABASE_URL ?? '',
14
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
15
+ { db: { schema: 'next_auth' } },
16
+ );
17
+
18
+ const supabase = createClient(
19
+ process.env.SUPABASE_URL ?? '',
20
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
21
+ { db: { schema: 'public' } },
22
+ );
23
+
24
+ // Retrieve the user's ID from the session token
25
+ const { data: sessionData, error: sessionError } = await supabaseAuth
26
+ .from('sessions')
27
+ .select('userId')
28
+ .eq('sessionToken', session?.value)
29
+ .single();
30
+
31
+ const userId = sessionData?.userId;
32
+
33
+ if (sessionError) {
34
+ console.error('Error fetching session from database:', sessionError.message);
35
+ return NextResponse.redirect(signinPage.href, { status: 302 });
36
+ }
37
+
38
+ // Retrieve the user's collections id and data from the database
39
+ const { data: userCollections, error: userCollErr } = await supabase
40
+ .from('collections')
41
+ .select('collection_id, display_name, description, created_at')
42
+ .eq('id', userId);
43
+
44
+ if (userCollErr) {
45
+ console.error('Error fetching user collection data from database:', userCollErr.message);
46
+ return NextResponse.redirect(signinPage.href, { status: 302 });
47
+ }
48
+
49
+ return NextResponse.json({ userCollections: userCollections });
50
+ }
51
+
52
+ // POST request to insert the user's collection data into the database
53
+ export async function POST(request: NextRequest) {
54
+ // Create a new Supabase client
55
+ const supabaseAuth = createClient(
56
+ process.env.SUPABASE_URL ?? '',
57
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
58
+ { db: { schema: 'next_auth' } },
59
+ );
60
+
61
+ const supabase = createClient(
62
+ process.env.SUPABASE_URL ?? '',
63
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
64
+ { db: { schema: 'public' } },
65
+ );
66
+
67
+ // Retrieve the session token from the request cookies
68
+ const session = request.cookies.get('next-auth.session-token') || request.cookies.get('__Secure-next-auth.session-token');
69
+
70
+ // Retrieve the collection data from the request body
71
+ const { display_name, description } = await request?.json();
72
+
73
+ // Retrieve the user's ID from the session token
74
+ const { data: sessionData, error: sessionError } = await supabaseAuth
75
+ .from('sessions')
76
+ .select('userId')
77
+ .eq('sessionToken', session?.value)
78
+ .single();
79
+
80
+ const userId = sessionData?.userId;
81
+
82
+ if (sessionError) {
83
+ console.error('Error fetching session from database:', sessionError.message);
84
+ return NextResponse.json({ error: sessionError.message }, { status: 500 });
85
+ }
86
+
87
+ // Insert the collection data into the database and return the data
88
+ const { data: insertData, error: insertError } = await supabase
89
+ .from('collections')
90
+ .insert([{ id: userId, display_name, description }])
91
+ .select('collection_id');
92
+
93
+ if (insertError) {
94
+ console.error('Error inserting user collection data into database:', insertError.message);
95
+ return NextResponse.json({ error: insertError.message }, { status: 500 });
96
+ }
97
+
98
+ console.log('Collection data inserted:', insertData);
99
+
100
+ return NextResponse.json({ message: 'Collection data inserted successfully.', collectionId: insertData[0].collection_id });
101
+ }
102
+
103
+ // DELETE request to delete the user's collection data from the database
104
+ export async function DELETE(request: NextRequest) {
105
+ // Create a new Supabase client
106
+ const supabaseAuth = createClient(
107
+ process.env.SUPABASE_URL ?? '',
108
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
109
+ { db: { schema: 'next_auth' } },
110
+ );
111
+
112
+ const supabase = createClient(
113
+ process.env.SUPABASE_URL ?? '',
114
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
115
+ { db: { schema: 'public' } },
116
+ );
117
+
118
+ // Retrieve the session token from the request cookies
119
+ const session = request.cookies.get('next-auth.session-token') || request.cookies.get('__Secure-next-auth.session-token');
120
+
121
+ // Retrieve the authorization token from the request headers
122
+ let authorization = request.headers.get('Authorization');
123
+
124
+ // Public API key
125
+ let api_key = null;
126
+
127
+ // If no session, use the public API key
128
+ if (authorization === null || authorization === undefined || authorization.includes('undefined')) {
129
+ console.log('No authorization token found, using public API key');
130
+ api_key = process.env.BACKEND_API_KEY as string;
131
+ authorization = null; // Clear the authorization token
132
+ }
133
+
134
+ // Retrieve the collection_id from the request body
135
+ const { collection_id } = await request?.json();
136
+
137
+ // Retrieve the user's ID from the session token
138
+ const { data: sessionData, error: sessionError } = await supabaseAuth
139
+ .from('sessions')
140
+ .select('userId')
141
+ .eq('sessionToken', session?.value)
142
+ .single();
143
+
144
+ const userId = sessionData?.userId;
145
+
146
+ if (sessionError) {
147
+ console.error('Error fetching session from database:', sessionError.message);
148
+ return NextResponse.json({ error: sessionError.message }, { status: 500 });
149
+ }
150
+
151
+ // Delete the vector collection from the vecs schema via POST request to Backend API
152
+ const deleteVecsResponse = await fetch(`${process.env.DELETE_SINGLE_COLLECTION_API}?collection_id=${collection_id}`, {
153
+ method: 'POST',
154
+ headers: {
155
+ 'Content-Type': 'application/json',
156
+ 'Authorization': authorization,
157
+ 'X-API-Key': api_key,
158
+ } as any,
159
+ body: JSON.stringify({ collection_id: collection_id }),
160
+ });
161
+
162
+ if (!deleteVecsResponse.ok) {
163
+ console.error('Error deleting', collection_id, 'from vecs schema:', deleteVecsResponse.statusText);
164
+ return NextResponse.json({ error: deleteVecsResponse.statusText }, { status: deleteVecsResponse.status });
165
+ }
166
+
167
+
168
+ // Delete the collection data from the database
169
+ const { data: deleteData, error: deleteError } = await supabase
170
+ .from('collections')
171
+ .delete()
172
+ .eq('id', userId)
173
+ .eq('collection_id', collection_id);
174
+
175
+ if (deleteError) {
176
+ console.error('Error deleting', collection_id, ' from database:', deleteError.message);
177
+ return NextResponse.json({ error: deleteError.message }, { status: 500 });
178
+ }
179
+
180
+ console.log('Delete', collection_id, ':', deleteData, 'deleteVecsResponse:', deleteVecsResponse);
181
+
182
+ return NextResponse.json({ message: 'Collection data deleted successfully.' });
183
+ }
frontend/app/components/admin-section.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // admin-section.tsx
2
+ "use client";
3
+
4
+ import { useState } from "react";
5
+ import { AdminMenu, AdminCollectionsRequests, AdminManageCollections, AdminManageUsers } from "@/app/components/ui/admin";
6
+ import { ToastContainer } from "react-toastify";
7
+ import "react-toastify/dist/ReactToastify.css";
8
+
9
+ const AdminSection: React.FC = () => {
10
+ const [showNewRequest, setShowNewRequest] = useState<boolean>(true);
11
+ const [showUsers, setShowUsers] = useState<boolean>(false);
12
+ const [showCollections, setShowCollections] = useState<boolean>(false);
13
+
14
+ return (
15
+ <div className="max-w-5xl w-full rounded-xl bg-white dark:bg-zinc-700/30 dark:from-inherit shadow-xl space-y-4 px-4 pb-4 pt-4">
16
+ {/* Toast Container */}
17
+ <ToastContainer />
18
+
19
+ {/* Menu Section */}
20
+ <AdminMenu
21
+ showUsers={showUsers}
22
+ setShowUsers={setShowUsers}
23
+ showNewRequest={showNewRequest}
24
+ setShowNewRequest={setShowNewRequest}
25
+ showCollections={showCollections}
26
+ setShowCollections={setShowCollections}
27
+ />
28
+
29
+ {/* New Requests Section */}
30
+ {showNewRequest ? (
31
+ <AdminCollectionsRequests />
32
+ ) : null}
33
+
34
+ {/* Public Collections Section */}
35
+ {showCollections ? (
36
+ <AdminManageCollections />
37
+ ) : null}
38
+
39
+ {/* Users Section */}
40
+ {showUsers ? (
41
+ <AdminManageUsers />
42
+ ) : null}
43
+ </div>
44
+ );
45
+ };
46
+
47
+ export default AdminSection;
frontend/app/components/chat-section.tsx CHANGED
@@ -2,15 +2,16 @@
2
 
3
  import { useChat } from "ai/react";
4
  import { ChatInput, ChatMessages } from "@/app/components/ui/chat";
5
- import ChatSelection from "./ui/chat/chat-selection";
6
- import AutofillQuestion from "@/app/components/ui/autofill-prompt/autofill-prompt-dialog";
7
  import { useSession } from "next-auth/react";
8
  import { useState } from "react";
9
 
10
  export default function ChatSection() {
11
  const { data: session } = useSession();
12
  const supabaseAccessToken = session?.supabaseAccessToken;
13
- const [docSelected, setDocSelected] = useState<string>('');
 
14
  const {
15
  messages,
16
  input,
@@ -27,13 +28,13 @@ export default function ChatSection() {
27
  },
28
  body: {
29
  // Add the selected document to the request body
30
- document: docSelected,
31
  },
32
  });
33
 
34
  return (
35
  <div className="space-y-4 max-w-5xl w-full relative">
36
- {docSelected ?
37
  (
38
  <>
39
  <ChatMessages
@@ -43,11 +44,13 @@ export default function ChatSection() {
43
  stop={stop}
44
  />
45
  <AutofillQuestion
46
- docSelected={docSelected}
 
47
  messages={messages}
48
  isLoading={isLoading}
49
  handleSubmit={handleSubmit}
50
  handleInputChange={handleInputChange}
 
51
  input={input}
52
  />
53
  <ChatInput
@@ -60,8 +63,10 @@ export default function ChatSection() {
60
  )
61
  :
62
  <ChatSelection
63
- docSelected={docSelected}
64
- handleDocSelect={setDocSelected}
 
 
65
  />
66
  }
67
  </div>
 
2
 
3
  import { useChat } from "ai/react";
4
  import { ChatInput, ChatMessages } from "@/app/components/ui/chat";
5
+ import { ChatSelection } from "@/app/components/ui/chat";
6
+ import { AutofillQuestion } from "@/app/components/ui/autofill-prompt";
7
  import { useSession } from "next-auth/react";
8
  import { useState } from "react";
9
 
10
  export default function ChatSection() {
11
  const { data: session } = useSession();
12
  const supabaseAccessToken = session?.supabaseAccessToken;
13
+ const [collSelectedId, setCollSelectedId] = useState<string>('');
14
+ const [collSelectedName, setCollSelectedName] = useState<string>('');
15
  const {
16
  messages,
17
  input,
 
28
  },
29
  body: {
30
  // Add the selected document to the request body
31
+ collection_id: collSelectedId,
32
  },
33
  });
34
 
35
  return (
36
  <div className="space-y-4 max-w-5xl w-full relative">
37
+ {collSelectedId ?
38
  (
39
  <>
40
  <ChatMessages
 
44
  stop={stop}
45
  />
46
  <AutofillQuestion
47
+ collSelectedId={collSelectedId}
48
+ collSelectedName={collSelectedName}
49
  messages={messages}
50
  isLoading={isLoading}
51
  handleSubmit={handleSubmit}
52
  handleInputChange={handleInputChange}
53
+ handleCollIdSelect={setCollSelectedId}
54
  input={input}
55
  />
56
  <ChatInput
 
63
  )
64
  :
65
  <ChatSelection
66
+ collSelectedId={collSelectedId}
67
+ collSelectedName={collSelectedName}
68
+ handleCollIdSelect={setCollSelectedId}
69
+ handleCollNameSelect={setCollSelectedName}
70
  />
71
  }
72
  </div>
frontend/app/components/header.tsx CHANGED
@@ -1,7 +1,7 @@
1
  "use client";
2
 
3
  import Image from 'next/image';
4
- import { Home, InfoIcon, MessageCircle, Search, FileQuestion, Menu, X, User2, LogOut, LogIn } from 'lucide-react';
5
  import { useTheme } from "next-themes";
6
  import { useEffect, useState } from "react";
7
  import { useMedia } from 'react-use';
@@ -12,6 +12,7 @@ import { MobileMenu } from '@/app/components/ui/mobilemenu';
12
  import { IconSpinner } from '@/app/components/ui/icons';
13
  import { useSession, signOut } from 'next-auth/react';
14
  import { usePathname } from 'next/navigation';
 
15
 
16
  const MobileMenuItems = [
17
  {
@@ -41,6 +42,14 @@ const MobileMenuItems = [
41
  },
42
  ];
43
 
 
 
 
 
 
 
 
 
44
  export default function Header() {
45
  const isLargeScreen = useMedia('(min-width: 1024px)', false);
46
  const [mounted, setMounted] = useState(false);
@@ -51,6 +60,8 @@ export default function Header() {
51
  const encodedPath = encodeURIComponent(currentPath);
52
  // Add callbackUrl params to the signinPage URL
53
  const signinPage = "/sign-in?callbackUrl=" + encodedPath;
 
 
54
 
55
  // Get user session for conditional rendering of user profile and logout buttons and for fetching the API status
56
  const { data: session, status } = useSession()
@@ -91,9 +102,22 @@ export default function Header() {
91
  }
92
  }
93
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
  useEffect(() => {
96
  setMounted(true);
 
97
  }, [session]);
98
 
99
  const [isMobileMenuOpen, setMobileMenuOpen] = useState(false);
@@ -183,6 +207,14 @@ export default function Header() {
183
  Search
184
  </div>
185
  </HeaderNavLink>
 
 
 
 
 
 
 
 
186
  </div>
187
  <div className="flex items-center ml-auto">
188
  {/* Status Page Button/Indicator */}
@@ -220,48 +252,51 @@ export default function Header() {
220
  )}
221
  </button>
222
 
223
- <span className="lg:text-lg font-nunito ml-2 mr-2"> </span>
224
 
225
  {/* Conditionally render the user profile and logout buttons based on the user's authentication status */}
226
- {status === 'loading' ? (
227
- <div className="flex items-center ml-2 mr-2 text-xl transition duration-300 ease-in-out transform hover:scale-125">
228
- <IconSpinner className="mr-2 animate-spin" />
229
- </div>
230
- ) : session ? (
231
  <>
232
- {/* User Profile Button */}
233
- <HeaderNavLink href="/profile" title='Profile'>
234
- <div className="flex items-center ml-2 mr-2 text-xl transition duration-300 ease-in-out transform hover:scale-125">
235
- <User2 className="mr-1 h-5 w-5" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  </div>
237
- </HeaderNavLink>
238
-
239
- {/* Sign Out Button */}
240
- <button title='Sign Out'
241
- onClick={
242
- async () => {
243
- await signOut();
244
- }
245
- }>
246
- <div className="flex items-center ml-2 text-xl transition duration-300 ease-in-out transform hover:scale-125">
247
- <LogOut className="mr-1 h-5 w-5" />
248
- </div>
249
- </button>
250
  </>
251
  ) : (
252
- <HeaderNavLink href={signinPage} title='Sign In'>
253
- <div className="flex items-center ml-2 transition duration-300 ease-in-out transform hover:scale-125">
254
- <LogIn className="mr-1 h-5 w-5" />
255
- Sign In
 
 
 
 
256
  </div>
257
- </HeaderNavLink>
258
  )}
259
  </div>
260
  </div >
261
 
262
  {/* Mobile menu component */}
263
  < MobileMenu isOpen={isMobileMenuOpen} onClose={() => setMobileMenuOpen(false)
264
- } logoSrc={logo} items={MobileMenuItems} />
265
  </nav >
266
  </div >
267
  );
 
1
  "use client";
2
 
3
  import Image from 'next/image';
4
+ import { Home, InfoIcon, MessageCircle, Search, FileQuestion, Menu, X, User2, LogOut, LogIn, SlidersHorizontal } from 'lucide-react';
5
  import { useTheme } from "next-themes";
6
  import { useEffect, useState } from "react";
7
  import { useMedia } from 'react-use';
 
12
  import { IconSpinner } from '@/app/components/ui/icons';
13
  import { useSession, signOut } from 'next-auth/react';
14
  import { usePathname } from 'next/navigation';
15
+ import { Skeleton } from "@nextui-org/react";
16
 
17
  const MobileMenuItems = [
18
  {
 
42
  },
43
  ];
44
 
45
+ const AdminMenuItems = [
46
+ {
47
+ href: '/admin',
48
+ icon: <SlidersHorizontal className="mr-2 h-5 w-5" />,
49
+ label: 'Admin',
50
+ },
51
+ ];
52
+
53
  export default function Header() {
54
  const isLargeScreen = useMedia('(min-width: 1024px)', false);
55
  const [mounted, setMounted] = useState(false);
 
60
  const encodedPath = encodeURIComponent(currentPath);
61
  // Add callbackUrl params to the signinPage URL
62
  const signinPage = "/sign-in?callbackUrl=" + encodedPath;
63
+ // Check if the user is an admin
64
+ const [isAdmin, setIsAdmin] = useState(false);
65
 
66
  // Get user session for conditional rendering of user profile and logout buttons and for fetching the API status
67
  const { data: session, status } = useSession()
 
102
  }
103
  }
104
 
105
+ const checkAdminRole = async () => {
106
+ const response = await fetch('/api/admin/is-admin');
107
+ if (!response.ok) {
108
+ console.error('Failed to fetch admin data');
109
+ return;
110
+ }
111
+ const data = await response.json();
112
+ setIsAdmin(data.isAdmin);
113
+ console.log('Admin role fetched successfully! Data:', data);
114
+ };
115
+
116
+ const newMobileMenuItems = isAdmin ? [...MobileMenuItems, ...AdminMenuItems] : MobileMenuItems;
117
 
118
  useEffect(() => {
119
  setMounted(true);
120
+ checkAdminRole();
121
  }, [session]);
122
 
123
  const [isMobileMenuOpen, setMobileMenuOpen] = useState(false);
 
207
  Search
208
  </div>
209
  </HeaderNavLink>
210
+ {isAdmin &&
211
+ <HeaderNavLink href="/admin" title='Admin'>
212
+ <div className="flex items-center transition duration-300 ease-in-out transform hover:scale-125">
213
+ <SlidersHorizontal className="mr-1 h-4 w-4" />
214
+ Admin
215
+ </div>
216
+ </HeaderNavLink>
217
+ }
218
  </div>
219
  <div className="flex items-center ml-auto">
220
  {/* Status Page Button/Indicator */}
 
252
  )}
253
  </button>
254
 
255
+ <span className="lg:text-lg font-nunito ml-6"></span>
256
 
257
  {/* Conditionally render the user profile and logout buttons based on the user's authentication status */}
258
+ {session ? (
 
 
 
 
259
  <>
260
+ <Skeleton isLoaded={status === 'authenticated'} className="rounded-md p-1">
261
+ <div className="flex items-center">
262
+ {/* User Profile Button */}
263
+ <HeaderNavLink href="/profile" title='Profile'>
264
+ <div className="flex items-center mr-6 text-xl transition duration-300 ease-in-out transform hover:scale-125">
265
+ <User2 className="mr-1 h-5 w-5" />
266
+ </div>
267
+ </HeaderNavLink>
268
+ {/* Sign Out Button */}
269
+ <button title='Sign Out'
270
+ onClick={
271
+ async () => {
272
+ await signOut();
273
+ }
274
+ }>
275
+ <div className="flex items-center text-xl transition duration-300 ease-in-out transform hover:scale-125">
276
+ <LogOut className="mr-1 h-5 w-5" />
277
+ </div>
278
+ </button>
279
  </div>
280
+ </Skeleton>
 
 
 
 
 
 
 
 
 
 
 
 
281
  </>
282
  ) : (
283
+ <Skeleton isLoaded={status !== 'loading'} className="rounded-md p-2">
284
+ <div className="flex items-center">
285
+ <HeaderNavLink href={signinPage} title='Sign In'>
286
+ <div className="flex items-center transition duration-300 ease-in-out transform hover:scale-110">
287
+ <LogIn className="mr-1 h-5 w-5" />
288
+ Sign In
289
+ </div>
290
+ </HeaderNavLink>
291
  </div>
292
+ </Skeleton>
293
  )}
294
  </div>
295
  </div >
296
 
297
  {/* Mobile menu component */}
298
  < MobileMenu isOpen={isMobileMenuOpen} onClose={() => setMobileMenuOpen(false)
299
+ } logoSrc={logo} items={newMobileMenuItems} />
300
  </nav >
301
  </div >
302
  );
frontend/app/components/profile-section.tsx ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { Skeleton } from "@nextui-org/react";
5
+ import Image from 'next/image';
6
+ import { User2, SlidersHorizontal, Info, Trash, RefreshCcw } from 'lucide-react';
7
+ import { HeaderNavLink } from '@/app/components/ui/navlink';
8
+ import Swal from 'sweetalert2';
9
+ import { useSession } from 'next-auth/react';
10
+
11
+ const ProfileSection: React.FC = () => {
12
+ const [userId, setUserId] = useState('');
13
+ const [name, setName] = useState('');
14
+ const [email, setEmail] = useState('');
15
+ const [imageURL, setImageURL] = useState('');
16
+ const [isLoaded, setIsLoaded] = useState(false);
17
+ const [isAdmin, setIsAdmin] = useState(false);
18
+ const [initialProfileData, setInitialProfileData] = useState({ name: '', email: '', imageURL: '' });
19
+ const { data: session, status } = useSession();
20
+ const supabaseAccessToken = session?.supabaseAccessToken;
21
+
22
+ const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
23
+ setName(event.target.value);
24
+ };
25
+
26
+ const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
27
+ setEmail(event.target.value);
28
+ };
29
+
30
+ const handleImageURLChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
31
+ setImageURL(event.target.value);
32
+ };
33
+
34
+ const handleSubmit = (event: React.FormEvent) => {
35
+ event.preventDefault();
36
+
37
+ if (!isProfileChanged()) {
38
+ console.log('No changes detected, not submitting the form');
39
+ return;
40
+ }
41
+
42
+ Swal.fire({
43
+ title: 'Are you sure?',
44
+ text: 'Do you want to update your profile data? This action cannot be undone!',
45
+ icon: 'warning',
46
+ showCancelButton: true,
47
+ confirmButtonColor: '#4caf50',
48
+ cancelButtonColor: '#b91c1c',
49
+ confirmButtonText: 'Yes',
50
+ cancelButtonText: 'No',
51
+ }).then(async (result) => {
52
+ if (result.isConfirmed) {
53
+ // Update the profile data in the database
54
+ if (await updateProfileData()) {
55
+ // Show a success message after updating the profile data
56
+ Swal.fire({
57
+ title: 'Profile Updated!',
58
+ text: 'Your profile data has been updated successfully.',
59
+ icon: 'success',
60
+ confirmButtonColor: '#4caf50',
61
+ });
62
+ } else {
63
+ // Show an error message if the profile data update failed
64
+ Swal.fire({
65
+ title: 'Error!',
66
+ text: 'Failed to update your profile data. Please try again later. (Check Console for more details)',
67
+ icon: 'error',
68
+ confirmButtonColor: '#4caf50',
69
+ });
70
+ }
71
+ }
72
+ });
73
+ };
74
+
75
+ const handleDeleteProfile = () => {
76
+ Swal.fire({
77
+ title: 'Are you sure?',
78
+ text: 'Do you want to delete your profile & data? This action cannot be undone, and your data will be deleted forever!',
79
+ icon: 'warning',
80
+ showCancelButton: true,
81
+ confirmButtonColor: '#4caf50',
82
+ cancelButtonColor: '#b91c1c',
83
+ confirmButtonText: 'Yes',
84
+ cancelButtonText: 'No',
85
+ }).then(async (result) => {
86
+ if (result.isConfirmed) {
87
+ // Delete the profile data from the database
88
+ if (await deleteProfileData()) {
89
+ // Show a success message after deleting the profile data
90
+ Swal.fire({
91
+ title: 'Profile Deleted!',
92
+ text: 'Your profile data has been deleted successfully. You will be redirected to the home page.',
93
+ icon: 'success',
94
+ confirmButtonColor: '#4caf50',
95
+ });
96
+ // Redirect to the home page after deleting the profile data
97
+ window.location.href = '/';
98
+ } else {
99
+ // Show an error message if the profile data deletion failed
100
+ Swal.fire({
101
+ title: 'Error!',
102
+ text: 'Failed to delete your profile data. Please try again later. (Check Console for more details)',
103
+ icon: 'error',
104
+ confirmButtonColor: '#4caf50',
105
+ });
106
+ }
107
+ }
108
+ });
109
+ };
110
+
111
+ const handleResetProfile = () => {
112
+ setName(initialProfileData.name);
113
+ setEmail(initialProfileData.email);
114
+ setImageURL(initialProfileData.imageURL);
115
+ }
116
+
117
+ const checkAdminRole = async () => {
118
+ const response = await fetch('/api/admin/is-admin');
119
+ if (!response.ok) {
120
+ console.error('Failed to fetch admin data');
121
+ return;
122
+ }
123
+ const data = await response.json();
124
+ setIsAdmin(data.isAdmin);
125
+ console.log('Admin role fetched successfully! Data:', data);
126
+ };
127
+
128
+ // Update the profile data in the database, via PUT request
129
+ const updateProfileData = async () => {
130
+ const response = await fetch('/api/profile', {
131
+ method: 'PUT',
132
+ headers: {
133
+ 'Content-Type': 'application/json',
134
+ },
135
+ body: JSON.stringify({
136
+ userId: userId,
137
+ name: name,
138
+ email: email,
139
+ image: imageURL,
140
+ }),
141
+ });
142
+ if (!response.ok) {
143
+ console.error('Failed to update profile data:', response.statusText);
144
+ return false;
145
+ }
146
+ console.log('Profile data updated successfully!');
147
+ // Update initial profile data to the new data
148
+ setInitialProfileData({ name, email, imageURL });
149
+ return true;
150
+ };
151
+
152
+ // Fetch the profile data from the database, via GET request
153
+ const fetchProfileData = async () => {
154
+ const response = await fetch('/api/profile');
155
+ if (!response.ok) {
156
+ console.error('Failed to fetch profile data');
157
+ return;
158
+ }
159
+ const data = await response.json();
160
+ const userData = data.userData;
161
+ setUserId(userData.id);
162
+ setName(userData.name);
163
+ setEmail(userData.email);
164
+ setImageURL(userData.image);
165
+ setInitialProfileData({ name: userData.name, email: userData.email, imageURL: userData.image });
166
+ setIsLoaded(true);
167
+ console.log('Profile data fetched successfully! Data:', userData);
168
+ };
169
+
170
+ // Delete the profile data from the database, via DELETE request
171
+ const deleteProfileData = async () => {
172
+ const response = await fetch('/api/profile', {
173
+ method: 'DELETE',
174
+ headers: {
175
+ 'Content-Type': 'application/json',
176
+ 'Authorization': `Bearer ${supabaseAccessToken}`, // Add the Supabase access token in the Authorization header
177
+ },
178
+ body: JSON.stringify({
179
+ userId: userId,
180
+ }),
181
+ });
182
+ if (!response.ok) {
183
+ console.error('Failed to delete profile data:', response.text);
184
+ return false;
185
+ }
186
+ console.log('Profile data deleted successfully!');
187
+ Swal.fire({
188
+ title: 'Profile Deleted!',
189
+ text: 'Your profile data has been deleted successfully.',
190
+ icon: 'success',
191
+ confirmButtonColor: '#4caf50',
192
+ });
193
+ return true;
194
+ };
195
+
196
+ useEffect(() => {
197
+ fetchProfileData();
198
+ checkAdminRole();
199
+ }, []);
200
+
201
+ const isProfileChanged = () => {
202
+ return (
203
+ name !== initialProfileData.name ||
204
+ email !== initialProfileData.email ||
205
+ imageURL !== initialProfileData.imageURL
206
+ );
207
+ };
208
+
209
+ return (
210
+ <div className="rounded-xl shadow-xl p-4 max-w-5xl w-full bg-white dark:bg-zinc-700/30">
211
+ <div className="space-y-2 p-4">
212
+ <div className="flex flex-col w-full justify-center gap-4">
213
+ <div className="flex justify-between items-center">
214
+ <div className='flex flex-col'>
215
+ <h1 className='flex font-bold text-2xl mb-4'>Profile</h1>
216
+ <Skeleton isLoaded={isLoaded} className='rounded-full w-20'>
217
+ {imageURL ? <Image className="rounded-full" src={imageURL} alt={name} width={84} height={84} priority={true} /> : <User2 size={84} />}
218
+ </Skeleton>
219
+ </div>
220
+ <div className="flex flex-col gap-4 justify-between">
221
+ {isAdmin ? (
222
+ <HeaderNavLink href="/admin" title='Admin Page'>
223
+ <button className="flex flex-grow justify-center items-center bg-blue-500 text-white rounded-md px-5 py-3 transition duration-300 ease-in-out transform hover:scale-105">
224
+ <SlidersHorizontal className="mr-1 h-5 w-5" />
225
+ Admin Page
226
+ </button>
227
+ </HeaderNavLink>
228
+ ) : null}
229
+ <button
230
+ className="flex flex-grow justify-center items-center font-bold bg-red-500 text-white rounded-md px-5 py-3 transition duration-300 ease-in-out transform hover:scale-105"
231
+ onClick={handleDeleteProfile}
232
+ >
233
+ <Trash className="mr-1 h-5 w-5" />
234
+ Delete Account & Data
235
+ </button>
236
+ </div>
237
+ </div>
238
+ <form onSubmit={handleSubmit} className='flex flex-col gap-4 max-w-2xl'>
239
+ <label className="flex flex-col">
240
+ <span className='mb-2'>Name:</span>
241
+ <Skeleton isLoaded={isLoaded} className="rounded-lg">
242
+ <input className='h-10 rounded-lg w-full bg-gray-300 dark:bg-zinc-700/65 border px-2' type="text" value={name} onChange={handleNameChange} />
243
+ </Skeleton>
244
+ </label>
245
+ <label className="flex flex-col">
246
+ <span className='mb-2'>Email:</span>
247
+ <Skeleton isLoaded={isLoaded} className="rounded-lg">
248
+ <input className='h-10 rounded-lg w-full bg-gray-300 dark:bg-zinc-700/65 border px-2' type="email" value={email} onChange={handleEmailChange} />
249
+ </Skeleton>
250
+ </label>
251
+ <label className="flex flex-col">
252
+ <span className='mb-2'>Image URL:</span>
253
+ <Skeleton isLoaded={isLoaded} className="rounded-lg">
254
+ <textarea className='h-14 rounded-lg w-full bg-gray-300 dark:bg-zinc-700/65 border px-2' value={imageURL} onChange={handleImageURLChange} />
255
+ </Skeleton>
256
+ <span className='flex items-center text-sm text-gray-500 dark:text-gray-400'>
257
+ <Info className='h-4 w-4 mr-1' />
258
+ GoogleUserContent, Imgur, Gravatar URLs allowed.
259
+ </span>
260
+ </label>
261
+ <div className="flex justify-evenly gap-4">
262
+ <button type="button" onClick={handleResetProfile} className="flex flex-grow justify-center items-center bg-red-500 font-bold text-white rounded-md px-5 py-3 transition duration-300 ease-in-out transform hover:scale-105">
263
+ <RefreshCcw className="mr-1 h-5 w-5" />
264
+ Reset
265
+ </button>
266
+ <button type="submit" disabled={!isProfileChanged()} className="flex flex-grow justify-center items-center text-l disabled:bg-transparent disabled:border disabled:border-gray-500 disabled:text-gray-500 bg-blue-500 text-white px-6 py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:scale-105 disabled:hover:scale-100">Save</button>
267
+ </div>
268
+ </form>
269
+ </div>
270
+ </div>
271
+ </div>
272
+ );
273
+ };
274
+
275
+ export default ProfileSection;
frontend/app/components/query-section.tsx CHANGED
@@ -3,13 +3,20 @@
3
  import { useChat } from "ai/react";
4
  import { ChatInput, ChatMessages } from "@/app/components/ui/chat";
5
  import { AutofillQuestion } from "./ui/autofill-prompt";
 
6
  import { useSession } from "next-auth/react";
7
  import { useState } from "react";
 
 
8
 
9
  export default function QuerySection() {
10
  const { data: session } = useSession();
11
  const supabaseAccessToken = session?.supabaseAccessToken;
12
- const [docSelected, setDocSelected] = useState<string>('');
 
 
 
 
13
  const {
14
  messages,
15
  input,
@@ -19,46 +26,78 @@ export default function QuerySection() {
19
  reload,
20
  stop,
21
  } = useChat({
22
- api: process.env.NEXT_PUBLIC_QUERY_API,
23
  headers: {
24
  // Add the access token to the request headers
25
  'Authorization': `Bearer ${supabaseAccessToken}`,
26
  },
27
  body: {
28
  // Add the selected document to the request body
29
- document: docSelected,
30
  },
31
  });
32
 
33
  return (
34
- <div className="space-y-4 max-w-5xl w-full">
35
- {/* <ChatMessages
36
- messages={messages}
37
- isLoading={isLoading}
38
- reload={reload}
39
- stop={stop}
40
- />
41
- <AutofillQuestion
42
- docSelected="PSSCOC"
43
- messages={messages}
44
- isLoading={isLoading}
45
- handleSubmit={handleSubmit}
46
- handleInputChange={handleInputChange}
47
- input={input}
48
  />
49
- <ChatInput
50
- input={input}
51
- handleSubmit={handleSubmit}
52
- handleInputChange={handleInputChange}
53
- isLoading={isLoading}
54
- /> */}
55
 
56
- {/* Maintenance Page */}
57
- <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative text-center" role="alert">
58
- <strong className="font-bold">A new feature is coming your way!</strong>
59
- <br />
60
- <span className="block sm:inline">The Q&A Page is currently undergoing upgrades. Please check back later.</span>
61
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  </div>
63
  );
64
  }
 
3
  import { useChat } from "ai/react";
4
  import { ChatInput, ChatMessages } from "@/app/components/ui/chat";
5
  import { AutofillQuestion } from "./ui/autofill-prompt";
6
+ import { QueryMenu, QuerySelection, QueryDocumentUpload, QueryCollectionManage } from "./ui/query";
7
  import { useSession } from "next-auth/react";
8
  import { useState } from "react";
9
+ import { ToastContainer } from 'react-toastify';
10
+ import 'react-toastify/dist/ReactToastify.css';
11
 
12
  export default function QuerySection() {
13
  const { data: session } = useSession();
14
  const supabaseAccessToken = session?.supabaseAccessToken;
15
+ const [collSelectedId, setCollSelectedId] = useState<string>('');
16
+ const [collSelectedName, setCollSelectedName] = useState<string>('');
17
+ const [showChat, setShowChat] = useState<boolean>(true);
18
+ const [showUpload, setShowUpload] = useState<boolean>(false);
19
+ const [showManage, setShowManage] = useState<boolean>(false);
20
  const {
21
  messages,
22
  input,
 
26
  reload,
27
  stop,
28
  } = useChat({
29
+ api: process.env.NEXT_PUBLIC_CHAT_API,
30
  headers: {
31
  // Add the access token to the request headers
32
  'Authorization': `Bearer ${supabaseAccessToken}`,
33
  },
34
  body: {
35
  // Add the selected document to the request body
36
+ collection_id: collSelectedId,
37
  },
38
  });
39
 
40
  return (
41
+ <div className="max-w-5xl w-full rounded-xl bg-white dark:bg-zinc-700/30 dark:from-inherit shadow-xl space-y-4 px-4 pb-4">
42
+ {/* Toast Container */}
43
+ <ToastContainer />
44
+
45
+ {/* Menu Section */}
46
+ <QueryMenu
47
+ showUpload={showUpload}
48
+ setShowUpload={setShowUpload}
49
+ showChat={showChat}
50
+ setShowChat={setShowChat}
51
+ showManage={showManage}
52
+ setShowManage={setShowManage}
53
+ setCollSelectedId={setCollSelectedId}
 
54
  />
 
 
 
 
 
 
55
 
56
+ {/* Document Selection/Chat Section */}
57
+ {showChat ?
58
+ (collSelectedId ?
59
+ <>
60
+ {/* Chat Section */}
61
+ <ChatMessages
62
+ messages={messages}
63
+ isLoading={isLoading}
64
+ reload={reload}
65
+ stop={stop}
66
+ />
67
+ <AutofillQuestion
68
+ collSelectedId={collSelectedId}
69
+ collSelectedName={collSelectedName}
70
+ messages={messages}
71
+ isLoading={isLoading}
72
+ handleSubmit={handleSubmit}
73
+ handleInputChange={handleInputChange}
74
+ handleCollIdSelect={setCollSelectedId}
75
+ input={input}
76
+ />
77
+ <ChatInput
78
+ input={input}
79
+ handleSubmit={handleSubmit}
80
+ handleInputChange={handleInputChange}
81
+ isLoading={isLoading}
82
+ />
83
+ </>
84
+ :
85
+ (
86
+ <QuerySelection
87
+ collSelectedId={collSelectedId}
88
+ collSelectedName={collSelectedName}
89
+ handleCollIdSelect={setCollSelectedId}
90
+ handleCollNameSelect={setCollSelectedName}
91
+ />))
92
+ : null
93
+ }
94
+
95
+ {/* Document Upload Section */}
96
+ {showUpload ? <QueryDocumentUpload /> : null}
97
+
98
+ {/* Document Manage Section */}
99
+ {showManage ? <QueryCollectionManage /> : null}
100
+
101
  </div>
102
  );
103
  }
frontend/app/components/search-section.tsx CHANGED
@@ -9,7 +9,8 @@ const SearchSection: React.FC = () => {
9
  const [query, setQuery] = useState("");
10
  const { searchResults, isLoading, handleSearch } = useSearch();
11
  const [searchButtonPressed, setSearchButtonPressed] = useState(false);
12
- const [docSelected, setDocSelected] = useState<string>('');
 
13
 
14
  const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
15
  setQuery(e.target.value);
@@ -19,16 +20,16 @@ const SearchSection: React.FC = () => {
19
  const handleSearchSubmit = (e: FormEvent) => {
20
  e.preventDefault();
21
  setSearchButtonPressed(true);
22
- handleSearch(query, docSelected);
23
  };
24
 
25
  return (
26
  <div className="space-y-4 max-w-5xl w-full">
27
- {docSelected ? (
28
  <>
29
- <h2 className="text-lg text-center font-semibold mb-4">Searching in {docSelected}</h2>
30
  <SearchInput
31
- docSelected={docSelected}
 
32
  query={query}
33
  isLoading={isLoading}
34
  results={searchResults}
@@ -36,12 +37,14 @@ const SearchSection: React.FC = () => {
36
  onSearchSubmit={handleSearchSubmit}
37
  />
38
  <AutofillSearchQuery
39
- docSelected={docSelected}
 
40
  query={query}
41
  isLoading={isLoading}
42
  results={searchResults}
43
  onInputChange={handleInputChange}
44
  onSearchSubmit={handleSearchSubmit}
 
45
  />
46
  <SearchResults
47
  query={query}
@@ -52,8 +55,10 @@ const SearchSection: React.FC = () => {
52
  </>
53
  ) : (
54
  <SearchSelection
55
- docSelected={docSelected}
56
- handleDocSelect={setDocSelected}
 
 
57
  />
58
  )}
59
  </div>
 
9
  const [query, setQuery] = useState("");
10
  const { searchResults, isLoading, handleSearch } = useSearch();
11
  const [searchButtonPressed, setSearchButtonPressed] = useState(false);
12
+ const [collSelectedId, setCollSelectedId] = useState<string>('');
13
+ const [collSelectedName, setCollSelectedName] = useState<string>('');
14
 
15
  const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
16
  setQuery(e.target.value);
 
20
  const handleSearchSubmit = (e: FormEvent) => {
21
  e.preventDefault();
22
  setSearchButtonPressed(true);
23
+ handleSearch(query, collSelectedId);
24
  };
25
 
26
  return (
27
  <div className="space-y-4 max-w-5xl w-full">
28
+ {collSelectedId ? (
29
  <>
 
30
  <SearchInput
31
+ collSelectedId={collSelectedId}
32
+ collSelectedName={collSelectedName}
33
  query={query}
34
  isLoading={isLoading}
35
  results={searchResults}
 
37
  onSearchSubmit={handleSearchSubmit}
38
  />
39
  <AutofillSearchQuery
40
+ collSelectedId={collSelectedId}
41
+ collSelectedName={collSelectedName}
42
  query={query}
43
  isLoading={isLoading}
44
  results={searchResults}
45
  onInputChange={handleInputChange}
46
  onSearchSubmit={handleSearchSubmit}
47
+ handleCollIdSelect={setCollSelectedId}
48
  />
49
  <SearchResults
50
  query={query}
 
55
  </>
56
  ) : (
57
  <SearchSelection
58
+ collSelectedId={collSelectedId}
59
+ collSelectedName={collSelectedName}
60
+ handleCollIdSelect={setCollSelectedId}
61
+ handleCollNameSelect={setCollSelectedName}
62
  />
63
  )}
64
  </div>
frontend/app/components/ui/admin/admin-collections-requests.tsx ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { Eye, EyeOff, X, Check, RefreshCw } from 'lucide-react';
5
+ import { IconSpinner } from '@/app/components/ui/icons';
6
+ import { toast } from 'react-toastify';
7
+ import Swal from 'sweetalert2';
8
+
9
+ export default function AdminCollectionsRequests() {
10
+ const [userRequests, setUserRequests] = useState<any[]>([]);
11
+ const [loading, setLoading] = useState<boolean>(true);
12
+ const [isRefreshed, setIsRefreshed] = useState<boolean>(true); // Track whether the data has been refreshed
13
+
14
+ // Fetch userRequest requests from the server
15
+ const fetchRequests = async () => {
16
+ try {
17
+ setLoading(true);
18
+ const response = await fetch('/api/admin/collections-requests',
19
+ {
20
+ method: 'GET',
21
+ headers: {
22
+ 'Content-Type': 'application/json',
23
+ 'Cache-Control': 'no-cache', // Disable cache to get the latest data
24
+ },
25
+ }
26
+ );
27
+ if (!response.ok) {
28
+ console.error('Error fetching userRequest requests:', response.statusText)
29
+ toast.error('Error fetching userRequest requests:', {
30
+ position: "top-right",
31
+ closeOnClick: true,
32
+ });
33
+ setLoading(false);
34
+ return false;
35
+ }
36
+ const data = await response.json();
37
+ setUserRequests(data.pubCollectionsReq);
38
+ console.log('Collection Requests:', data.pubCollectionsReq);
39
+ } catch (error) {
40
+ console.error('Error fetching userRequest requests:', error);
41
+ toast.error('Error fetching userRequest requests:', {
42
+ position: "top-right",
43
+ closeOnClick: true,
44
+ });
45
+ setLoading(false);
46
+ return false;
47
+ }
48
+ setLoading(false);
49
+ return true;
50
+ };
51
+
52
+ useEffect(() => {
53
+ // Fetch userRequest requests from the server
54
+ fetchRequests();
55
+ }, []);
56
+
57
+ // Handle reject collection request
58
+ const handleReject = async (collectionId: string) => {
59
+ // Show confirmation dialog
60
+ Swal.fire({
61
+ title: 'Reject Request',
62
+ text: "Are you sure you want to reject this collection request?",
63
+ icon: 'question',
64
+ showCancelButton: true,
65
+ confirmButtonText: 'Yes',
66
+ cancelButtonText: 'No',
67
+ confirmButtonColor: '#4caf50',
68
+ cancelButtonColor: '#b91c1c',
69
+ }).then((result) => {
70
+ if (result.isConfirmed) {
71
+ // if user confirms, send request to server
72
+ fetch(`/api/admin/collections-requests/reject`, {
73
+ method: 'POST',
74
+ headers: {
75
+ 'Content-Type': 'application/json',
76
+ },
77
+ body: JSON.stringify({
78
+ collection_id: collectionId,
79
+ }),
80
+ }).then(async (response) => {
81
+ if (!response.ok) {
82
+ console.error('Error rejecting collection request:', response.statusText);
83
+ // Show error dialog
84
+ Swal.fire({
85
+ title: 'Error!',
86
+ text: 'Error rejecting collection request. Please try again later. (Check Console for more details)',
87
+ icon: 'error',
88
+ confirmButtonColor: '#4caf50',
89
+ });
90
+ return;
91
+ }
92
+ const data = await response.json();
93
+ console.log('Collection Request Rejected:', data);
94
+ // Show success dialog
95
+ Swal.fire({
96
+ title: 'Success!',
97
+ text: 'Collection request has been rejected successfully.',
98
+ icon: 'success',
99
+ confirmButtonColor: '#4caf50',
100
+ });
101
+ // Remove approved request from the list
102
+ setUserRequests(userRequests.filter((userRequest) => userRequest.collection_id !== collectionId));
103
+ }).catch((error) => {
104
+ console.error('Error rejecting collection request:', error);
105
+ // Show error dialog
106
+ Swal.fire({
107
+ title: 'Error!',
108
+ text: 'Error rejecting collection request. Please try again later. (Check Console for more details)',
109
+ icon: 'error',
110
+ confirmButtonColor: '#4caf50',
111
+ });
112
+ });
113
+ }
114
+ });
115
+ }
116
+
117
+ // Handle approve collection request
118
+ const handleApprove = async (collectionId: string, is_make_public: boolean) => {
119
+ // Show confirmation dialog
120
+ Swal.fire({
121
+ title: 'Approve Request',
122
+ text: "Are you sure you want to approve this collection request?",
123
+ icon: 'question',
124
+ showCancelButton: true,
125
+ confirmButtonText: 'Yes',
126
+ cancelButtonText: 'No',
127
+ confirmButtonColor: '#4caf50',
128
+ cancelButtonColor: '#b91c1c',
129
+ }).then((result) => {
130
+ if (result.isConfirmed) {
131
+ // if user confirms, send request to server
132
+ fetch(`/api/admin/collections-requests/approve`, {
133
+ method: 'POST',
134
+ headers: {
135
+ 'Content-Type': 'application/json',
136
+ },
137
+ body: JSON.stringify({
138
+ collection_id: collectionId,
139
+ is_make_public: is_make_public,
140
+ }),
141
+ }).then(async (response) => {
142
+ if (!response.ok) {
143
+ console.error('Error approving collection request:', response.statusText);
144
+ // Show error dialog
145
+ Swal.fire({
146
+ title: 'Error!',
147
+ text: 'Error approving collection request. Please try again later. (Check Console for more details)',
148
+ icon: 'error',
149
+ confirmButtonColor: '#4caf50',
150
+ });
151
+ return;
152
+ }
153
+ const data = await response.json();
154
+ console.log('Collection Request Approved:', data);
155
+ // Show success dialog
156
+ Swal.fire({
157
+ title: 'Success!',
158
+ text: 'Collection request has been approved successfully.',
159
+ icon: 'success',
160
+ confirmButtonColor: '#4caf50',
161
+ });
162
+ // Remove approved request from the list
163
+ setUserRequests(userRequests.filter((userRequest) => userRequest.collection_id !== collectionId));
164
+ }).catch((error) => {
165
+ console.error('Error approving collection request:', error);
166
+ // Show error dialog
167
+ Swal.fire({
168
+ title: 'Error!',
169
+ text: 'Error approving collection request. Please try again later. (Check Console for more details)',
170
+ icon: 'error',
171
+ confirmButtonColor: '#4caf50',
172
+ });
173
+ });
174
+ }
175
+ });
176
+ }
177
+
178
+ const handleRefresh = async () => {
179
+ setIsRefreshed(false);
180
+ const timer = setInterval(() => {
181
+ // Timer to simulate a spinner while refreshing data
182
+ setIsRefreshed(true);
183
+ }, 1000); // Adjust the delay time as needed (e.g., 5000 milliseconds = 5 seconds)
184
+ // Display a toast notification
185
+ if (await fetchRequests()) {
186
+ toast('Data refreshed successfully!', {
187
+ type: 'success',
188
+ position: 'top-right',
189
+ autoClose: 3000,
190
+ closeOnClick: true,
191
+ pauseOnHover: true,
192
+ });
193
+ }
194
+ else {
195
+ toast('Error refreshing data. Please try again later.', {
196
+ type: 'error',
197
+ position: 'top-right',
198
+ autoClose: 3000,
199
+ closeOnClick: true,
200
+ pauseOnHover: true,
201
+ });
202
+ }
203
+ return () => clearInterval(timer); // Cleanup the timer on complete
204
+ }
205
+
206
+ return (
207
+ <div className="w-full rounded-xl bg-white dark:bg-zinc-700/30 dark:from-inherit p-4 shadow-xl">
208
+ <div className="rounded-lg pt-5 pr-5 pl-5 flex h-[50vh] flex-col overflow-y-auto pb-4">
209
+ <h1 className='text-center font-bold text-xl mb-4'>New Collection Requests</h1>
210
+ <div className="flex items-center justify-start gap-2 mb-4">
211
+ {/* Refresh Data button */}
212
+ <button onClick={handleRefresh}
213
+ className="flex items-center justify-center hover:text-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 rounded-full p-1 transition duration-300 ease-in-out transform hover:scale-110 hover:bg-blue-500/10 focus:bg-blue-500/10"
214
+ title='Refresh Data'
215
+ >
216
+ {isRefreshed ? <RefreshCw className='w-5 h-5' /> : <RefreshCw className='w-5 h-5 animate-spin' />}
217
+ </button>
218
+ </div>
219
+ {loading ? (
220
+ <IconSpinner className='w-10 h-10 mx-auto my-auto animate-spin' />
221
+ ) : userRequests.length === 0 ? (
222
+ <div className="mx-auto my-auto text-center text-lg text-gray-500 dark:text-gray-400">No New Requests.</div>
223
+ ) : (
224
+ <div className="relative overflow-x-auto rounded-lg">
225
+ <table className="w-full text-xl text-left rtl:text-right text-gray-500 dark:text-gray-400 p-4">
226
+ <thead className="text-sm text-center text-gray-700 uppercase bg-gray-400 dark:bg-gray-700 dark:text-gray-400">
227
+ <tr>
228
+ <th scope="col" className="px-6 py-3">Display Name</th>
229
+ <th scope="col" className="px-6 py-3">Description</th>
230
+ <th scope="col" className="px-6 py-3">Current Visibility</th>
231
+ <th scope="col" className="px-6 py-3">Requestor Name</th>
232
+ <th scope="col" className="px-6 py-3">Requested Visibility</th>
233
+ <th scope="col" className="px-6 py-3">Requested</th>
234
+ <th scope="col" className="px-6 py-3">Actions</th>
235
+ </tr>
236
+ </thead>
237
+ <tbody>
238
+ {userRequests.map((userRequest, index) => (
239
+ <tr className="text-sm text-center item-center bg-gray-100 border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600" key={index}>
240
+ {/* Render table rows */}
241
+ <td className="px-6 py-3">{userRequest.collections.display_name}</td>
242
+ <td className="px-6 py-3">{userRequest.collections.description}</td>
243
+ <td className="px-6 py-3">{userRequest.collections.is_public ? <span className='flex justify-center items-center'><Eye className='w-4 h-4 mr-1' /> Public</span> : <span className='flex justify-center items-center'><EyeOff className='w-4 h-4 mr-1' /> Private</span>}</td>
244
+ <td className="px-6 py-3">{userRequest.collections.users.name}</td>
245
+ <td className="px-6 py-3">{userRequest.is_make_public ? <span className='flex justify-center items-center'><Eye className='w-4 h-4 mr-1' /> Public</span> : <span className='flex justify-center items-center'><EyeOff className='w-4 h-4 mr-1' /> Private</span>}</td>
246
+ <td className="px-6 py-3">{new Date(userRequest.created_at).toLocaleDateString('en-GB', { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true })}</td>
247
+ <td className="px-6 py-3 w-full flex flex-wrap justify-between gap-2">
248
+ {/* Approved or Reject Status */}
249
+ <button onClick={() => handleApprove(userRequest.collection_id, userRequest.is_make_public)}
250
+ title='Approve'
251
+ className="flex flex-grow justify-center items-center text-sm disabled:bg-gray-500 bg-green-500 text-white px-3 py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:bg-green-500/40"
252
+ >
253
+ <span className='flex items-center'>
254
+ <Check className='w-4 h-4 mr-1' />
255
+ <span>Approve</span>
256
+ </span>
257
+ </button>
258
+ <button onClick={() => handleReject(userRequest.collection_id)}
259
+ title='Reject'
260
+ className="flex flex-grow justify-center items-center text-sm disabled:bg-gray-500 bg-red-500 text-white px-3 py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:bg-red-500/40"
261
+ >
262
+ <span className='flex items-center'>
263
+ <X className='w-4 h-4 mr-1' />
264
+ <span>Reject</span>
265
+ </span>
266
+ </button>
267
+ </td>
268
+ </tr>
269
+ ))}
270
+ </tbody>
271
+ </table>
272
+ </div>
273
+ )}
274
+ </div>
275
+ </div>
276
+ );
277
+ }
frontend/app/components/ui/admin/admin-manage-collections.tsx ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { Eye, EyeOff, RefreshCw } from 'lucide-react';
5
+ import { IconSpinner } from '@/app/components/ui/icons';
6
+ import { toast } from 'react-toastify';
7
+ import Swal from 'sweetalert2';
8
+
9
+ export default function AdminManageCollections() {
10
+ const [collectionsData, setCollectionsData] = useState<any[]>([]);
11
+ const [loading, setLoading] = useState<boolean>(true);
12
+ const [isRefreshed, setIsRefreshed] = useState<boolean>(true); // Track whether the data has been refreshed
13
+
14
+ // Fetch collection requests from the server
15
+ const fetchCollections = async () => {
16
+ try {
17
+ setLoading(true);
18
+ const response = await fetch('/api/admin/collections',
19
+ {
20
+ method: 'GET',
21
+ headers: {
22
+ 'Content-Type': 'application/json',
23
+ 'Cache-Control': 'no-cache', // Disable cache to get the latest data
24
+ },
25
+ }
26
+ );
27
+ if (!response.ok) {
28
+ console.error('Error fetching collections:', response.statusText)
29
+ toast.error('Error fetching collections:', {
30
+ position: "top-right",
31
+ closeOnClick: true,
32
+ });
33
+ setLoading(false);
34
+ return false;
35
+ }
36
+ const data = await response.json();
37
+ // Sort the collections by created date in descending order (oldest first)
38
+ const sortedData = data.collections.sort((a: any, b: any) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
39
+ setCollectionsData(sortedData);
40
+ console.log('Collections:', sortedData);
41
+ } catch (error) {
42
+ console.error('Error fetching collections:', error);
43
+ toast.error('Error fetching collections:', {
44
+ position: "top-right",
45
+ closeOnClick: true,
46
+ });
47
+ setLoading(false);
48
+ return false;
49
+ }
50
+ setLoading(false);
51
+ return true;
52
+ };
53
+
54
+ useEffect(() => {
55
+ // Fetch collections from the server
56
+ fetchCollections();
57
+ }, []);
58
+
59
+ // Handle make private a collection
60
+ const handleMakePrivate = async (collectionId: string) => {
61
+ // Show confirmation dialog
62
+ Swal.fire({
63
+ title: 'Private Request Confirmation',
64
+ text: "Are you sure you want to make this collection Private?",
65
+ icon: 'question',
66
+ showCancelButton: true,
67
+ confirmButtonText: 'Yes',
68
+ cancelButtonText: 'No',
69
+ confirmButtonColor: '#4caf50',
70
+ cancelButtonColor: '#b91c1c',
71
+ }).then((result) => {
72
+ if (result.isConfirmed) {
73
+ // if user confirms, send request to server
74
+ fetch(`/api/admin/collections-requests/reject`, {
75
+ method: 'POST',
76
+ headers: {
77
+ 'Content-Type': 'application/json',
78
+ },
79
+ body: JSON.stringify({
80
+ collection_id: collectionId,
81
+ }),
82
+ }).then(async (response) => {
83
+ if (!response.ok) {
84
+ console.error('Error setting collection Private:', response.statusText);
85
+ // Show error dialog
86
+ Swal.fire({
87
+ title: 'Error!',
88
+ text: 'Error setting collection Private. Please try again later. (Check Console for more details)',
89
+ icon: 'error',
90
+ confirmButtonColor: '#4caf50',
91
+ });
92
+ return;
93
+ }
94
+ const data = await response.json();
95
+ console.log('Collection Set to Private Success:', data);
96
+ // Show success dialog
97
+ Swal.fire({
98
+ title: 'Success!',
99
+ text: 'Collection has been set to Private successfully.',
100
+ icon: 'success',
101
+ confirmButtonColor: '#4caf50',
102
+ });
103
+ // Refresh the collections data
104
+ fetchCollections();
105
+ }).catch((error) => {
106
+ console.error('Error setting collection Private:', error);
107
+ // Show error dialog
108
+ Swal.fire({
109
+ title: 'Error!',
110
+ text: 'Error setting collection Private. Please try again later. (Check Console for more details)',
111
+ icon: 'error',
112
+ confirmButtonColor: '#4caf50',
113
+ });
114
+ });
115
+ }
116
+ });
117
+ }
118
+
119
+ // Handle make public a collection
120
+ const handleMakePublic = async (collectionId: string) => {
121
+ // Show confirmation dialog
122
+ Swal.fire({
123
+ title: 'Public Request Confirmation',
124
+ text: "Are you sure you want to make this collection Public?",
125
+ icon: 'question',
126
+ showCancelButton: true,
127
+ confirmButtonText: 'Yes',
128
+ cancelButtonText: 'No',
129
+ confirmButtonColor: '#4caf50',
130
+ cancelButtonColor: '#b91c1c',
131
+ }).then((result) => {
132
+ if (result.isConfirmed) {
133
+ // if user confirms, send request to server
134
+ fetch(`/api/admin/collections-requests/approve`, {
135
+ method: 'POST',
136
+ headers: {
137
+ 'Content-Type': 'application/json',
138
+ },
139
+ body: JSON.stringify({
140
+ collection_id: collectionId,
141
+ }),
142
+ }).then(async (response) => {
143
+ if (!response.ok) {
144
+ console.error('Error setting collection Public:', response.statusText);
145
+ // Show error dialog
146
+ Swal.fire({
147
+ title: 'Error!',
148
+ text: 'Error setting collection Public. Please try again later. (Check Console for more details)',
149
+ icon: 'error',
150
+ confirmButtonColor: '#4caf50',
151
+ });
152
+ return;
153
+ }
154
+ const data = await response.json();
155
+ console.log('Collection Set to Public Success:', data);
156
+ // Show success dialog
157
+ Swal.fire({
158
+ title: 'Success!',
159
+ text: 'Collection has been set to Public successfully.',
160
+ icon: 'success',
161
+ confirmButtonColor: '#4caf50',
162
+ });
163
+ // Remove approved request from the list
164
+ setCollectionsData(collectionsData.filter((collection) => collection.collection_id !== collectionId));
165
+ }).catch((error) => {
166
+ console.error('Error setting collection Public:', error);
167
+ // Show error dialog
168
+ Swal.fire({
169
+ title: 'Error!',
170
+ text: 'Error setting collection Public. Please try again later. (Check Console for more details)',
171
+ icon: 'error',
172
+ confirmButtonColor: '#4caf50',
173
+ });
174
+ });
175
+ }
176
+ });
177
+ }
178
+
179
+ const handleRefresh = async () => {
180
+ setIsRefreshed(false);
181
+ const timer = setInterval(() => {
182
+ // Timer to simulate a spinner while refreshing data
183
+ setIsRefreshed(true);
184
+ }, 1000); // Adjust the delay time as needed (e.g., 5000 milliseconds = 5 seconds)
185
+ // Display a toast notification
186
+ if (await fetchCollections()) {
187
+ toast('Data refreshed successfully!', {
188
+ type: 'success',
189
+ position: 'top-right',
190
+ autoClose: 3000,
191
+ closeOnClick: true,
192
+ pauseOnHover: true,
193
+ });
194
+ }
195
+ else {
196
+ toast('Error refreshing data. Please try again later.', {
197
+ type: 'error',
198
+ position: 'top-right',
199
+ autoClose: 3000,
200
+ closeOnClick: true,
201
+ pauseOnHover: true,
202
+ });
203
+ }
204
+ return () => clearInterval(timer); // Cleanup the timer on complete
205
+ }
206
+
207
+ return (
208
+ <div className="w-full rounded-xl bg-white dark:bg-zinc-700/30 dark:from-inherit p-4 shadow-xl">
209
+ <div className="rounded-lg pt-5 pr-5 pl-5 flex h-[50vh] flex-col overflow-y-auto pb-4">
210
+ <h1 className='text-center font-bold text-xl mb-4'>Manage Collections</h1>
211
+ <div className="flex items-center justify-start gap-2 mb-4">
212
+ {/* Refresh Data button */}
213
+ <button onClick={handleRefresh}
214
+ className="flex items-center justify-center hover:text-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 rounded-full p-1 transition duration-300 ease-in-out transform hover:scale-110 hover:bg-blue-500/10 focus:bg-blue-500/10"
215
+ title='Refresh Data'
216
+ >
217
+ {isRefreshed ? <RefreshCw className='w-5 h-5' /> : <RefreshCw className='w-5 h-5 animate-spin' />}
218
+ </button>
219
+ </div>
220
+ {loading ? (
221
+ <IconSpinner className='w-10 h-10 mx-auto my-auto animate-spin' />
222
+ ) : collectionsData.length === 0 ? (
223
+ <div className="mx-auto my-auto text-center text-lg text-gray-500 dark:text-gray-400">No Collections Found.</div>
224
+ ) : (
225
+ <div className="relative overflow-x-auto rounded-lg">
226
+ <table className="w-full text-xl text-left rtl:text-right text-gray-500 dark:text-gray-400 p-4">
227
+ <thead className="text-sm text-center text-gray-700 uppercase bg-gray-400 dark:bg-gray-700 dark:text-gray-400">
228
+ <tr>
229
+ <th scope="col" className="px-6 py-3">Display Name</th>
230
+ <th scope="col" className="px-6 py-3">Description</th>
231
+ <th scope="col" className="px-6 py-3">Current Visibility</th>
232
+ <th scope="col" className="px-6 py-3">Created</th>
233
+ <th scope="col" className="px-6 py-3">Owner</th>
234
+ <th scope="col" className="px-6 py-3">Email</th>
235
+ <th scope="col" className="px-6 py-3">Actions</th>
236
+ </tr>
237
+ </thead>
238
+ <tbody>
239
+ {collectionsData.map((collection, index) => (
240
+ <tr className="text-sm text-center item-center bg-gray-100 border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600" key={index}>
241
+ {/* Render table rows */}
242
+ <td className="px-6 py-3">{collection.display_name}</td>
243
+ <td className="px-6 py-3">{collection.description}</td>
244
+ <td className="px-6 py-3">{collection.is_public ? <span className='flex justify-center items-center'><Eye className='w-4 h-4 mr-1' /> Public</span> : <span className='flex justify-center items-center'><EyeOff className='w-4 h-4 mr-1' /> Private</span>}</td>
245
+ <td className="px-6 py-3">{new Date(collection.created_at).toLocaleDateString('en-GB', { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true })}</td>
246
+ <td className="px-6 py-3">{collection.users.name}</td>
247
+ <td className="px-6 py-3">{collection.users.email}</td>
248
+ <td className="px-6 py-3 w-full">
249
+ <div className="flex justify-center items-center gap-2">
250
+ {/* Set Public or Private Status */}
251
+ {collection.is_public ? (
252
+ <button onClick={() => handleMakePrivate(collection.collection_id)}
253
+ title='Set Private'
254
+ className="flex flex-grow justify-center items-center text-sm bg-blue-500 text-white px-3 py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:bg-blue-500/40"
255
+ >
256
+ <span className='flex items-center'>
257
+ <EyeOff className='w-4 h-4 mr-1' />
258
+ <span>Set Private</span>
259
+ </span>
260
+ </button>
261
+ ) : (
262
+ <button onClick={() => handleMakePublic(collection.collection_id)}
263
+ title='Set Public'
264
+ className="flex flex-grow justify-center items-center text-sm bg-blue-500 text-white px-3 py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:bg-blue-500/40"
265
+ >
266
+ <span className='flex items-center'>
267
+ <Eye className='w-4 h-4 mr-1' />
268
+ <span>Set Public</span>
269
+ </span>
270
+ </button>
271
+ )}
272
+ </div>
273
+ </td>
274
+ </tr>
275
+ ))}
276
+ </tbody>
277
+ </table>
278
+ </div>
279
+ )}
280
+ </div>
281
+ </div>
282
+ );
283
+ }
frontend/app/components/ui/admin/admin-manage-users.tsx ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { toast } from "react-toastify";
5
+ import { RefreshCw, UserCheck, UserX } from "lucide-react";
6
+ import { IconSpinner } from "@/app/components/ui/icons";
7
+ import Swal from "sweetalert2";
8
+
9
+ export default function AdminManageUsers() {
10
+ const [users, setUsers] = useState<any[]>([]);
11
+ const [loading, setLoading] = useState<boolean>(true);
12
+ const [isRefreshed, setIsRefreshed] = useState<boolean>(true); // Track whether the data has been refreshed
13
+
14
+ // Fetch the users from the database
15
+ const fetchUsers = async () => {
16
+ try {
17
+ setLoading(true);
18
+ const response = await fetch('/api/admin/users',
19
+ {
20
+ method: 'GET',
21
+ headers: {
22
+ 'Content-Type': 'application/json',
23
+ 'Cache-Control': 'no-cache', // Disable cache to get the latest data
24
+ },
25
+ }
26
+ );
27
+ if (!response.ok) {
28
+ console.error('Error fetching users:', response.statusText)
29
+ toast.error('Error fetching users:', {
30
+ position: "top-right",
31
+ closeOnClick: true,
32
+ });
33
+ setLoading(false);
34
+ return false;
35
+ }
36
+ const data = await response.json();
37
+ console.log('Users:', data.users);
38
+ // Retrieve the necessary data from the response
39
+ const formattedUsers = data.users.map((user: any) => ({
40
+ id: user.id,
41
+ name: user.name,
42
+ email: user.email,
43
+ isAdmin: user.admins.length > 0,
44
+ }));
45
+ setUsers(formattedUsers);
46
+ } catch (error) {
47
+ console.error('Error fetching users:', error);
48
+ toast.error('Error fetching users:', {
49
+ position: "top-right",
50
+ closeOnClick: true,
51
+ });
52
+ setLoading(false);
53
+ return false;
54
+ }
55
+ setLoading(false);
56
+ return true;
57
+ };
58
+
59
+ // On component mount, fetch the users from the database
60
+ useEffect(() => {
61
+ fetchUsers();
62
+ }, []);
63
+
64
+ // Handle to promote the user to admin status
65
+ const handlePromote = async (userId: string) => {
66
+ // Display a confirmation dialog
67
+ Swal.fire({
68
+ title: 'Are you sure?',
69
+ text: "You are about to promote this user to admin status.",
70
+ icon: 'warning',
71
+ showCancelButton: true,
72
+ confirmButtonColor: '#4caf50',
73
+ cancelButtonColor: '#b91c1c',
74
+ confirmButtonText: 'Yes',
75
+ cancelButtonText: 'No',
76
+ }).then(async (result) => {
77
+ if (result.isConfirmed) {
78
+ try {
79
+ const response = await fetch('/api/admin/users/promote', {
80
+ method: 'POST',
81
+ headers: {
82
+ 'Content-Type': 'application/json',
83
+ 'Cache-Control': 'no-cache', // Disable cache to get the latest data
84
+ },
85
+ body: JSON.stringify({
86
+ id: userId
87
+ }),
88
+ });
89
+ if (!response.ok) {
90
+ console.error('Error promoting user:', response.statusText);
91
+ // Show error dialog
92
+ Swal.fire({
93
+ title: 'Error!',
94
+ text: 'Error promoting user. Please try again later. (Check Console for more details)',
95
+ icon: 'error',
96
+ confirmButtonColor: '#4caf50',
97
+ });
98
+ return;
99
+ }
100
+ // Show success dialog
101
+ Swal.fire({
102
+ title: 'Success!',
103
+ text: 'User promoted successfully!',
104
+ icon: 'success',
105
+ confirmButtonColor: '#4caf50',
106
+ });
107
+ // Refresh the data
108
+ fetchUsers();
109
+ } catch (error) {
110
+ console.error('Error promoting user:', error);
111
+ // Show error dialog
112
+ Swal.fire({
113
+ title: 'Error!',
114
+ text: 'Error promoting user. Please try again later. (Check Console for more details)',
115
+ icon: 'error',
116
+ confirmButtonColor: '#4caf50',
117
+ });
118
+ }
119
+ }
120
+ });
121
+ }
122
+
123
+ // Handle to demote the user from admin status
124
+ const handleDemote = async (userId: string) => {
125
+ // Display a confirmation dialog
126
+ Swal.fire({
127
+ title: 'Are you sure?',
128
+ text: "You are about to demote this user from admin status.",
129
+ icon: 'warning',
130
+ showCancelButton: true,
131
+ confirmButtonColor: '#4caf50',
132
+ cancelButtonColor: '#b91c1c',
133
+ confirmButtonText: 'Yes',
134
+ cancelButtonText: 'No',
135
+ }).then(async (result) => {
136
+ if (result.isConfirmed) {
137
+ try {
138
+ const response = await fetch('/api/admin/users/demote', {
139
+ method: 'POST',
140
+ headers: {
141
+ 'Content-Type': 'application/json',
142
+ 'Cache-Control': 'no-cache', // Disable cache to get the latest data
143
+ },
144
+ body: JSON.stringify({
145
+ id: userId
146
+ }),
147
+ });
148
+ if (!response.ok) {
149
+ console.error('Error demoting user:', response.statusText);
150
+ // Show error dialog
151
+ Swal.fire({
152
+ title: 'Error!',
153
+ text: 'Error demoting user. Please try again later. (Check Console for more details)',
154
+ icon: 'error',
155
+ confirmButtonColor: '#4caf50',
156
+ });
157
+ return;
158
+ }
159
+ // Show success dialog
160
+ Swal.fire({
161
+ title: 'Success!',
162
+ text: 'User demoted successfully!',
163
+ icon: 'success',
164
+ confirmButtonColor: '#4caf50',
165
+ });
166
+ // Refresh the data
167
+ fetchUsers();
168
+ } catch (error) {
169
+ console.error('Error demoting user:', error);
170
+ // Show error dialog
171
+ Swal.fire({
172
+ title: 'Error!',
173
+ text: 'Error demoting user. Please try again later. (Check Console for more details)',
174
+ icon: 'error',
175
+ confirmButtonColor: '#4caf50',
176
+ });
177
+ }
178
+ }
179
+ });
180
+ }
181
+
182
+ // Handle to refresh the data
183
+ const handleRefresh = async () => {
184
+ setIsRefreshed(false);
185
+ const timer = setInterval(() => {
186
+ // Timer to simulate a spinner while refreshing data
187
+ setIsRefreshed(true);
188
+ }, 1000); // Adjust the delay time as needed (e.g., 5000 milliseconds = 5 seconds)
189
+ // Display a toast notification
190
+ if (await fetchUsers()) {
191
+ toast('Data refreshed successfully!', {
192
+ type: 'success',
193
+ position: 'top-right',
194
+ autoClose: 3000,
195
+ closeOnClick: true,
196
+ pauseOnHover: true,
197
+ });
198
+ }
199
+ else {
200
+ toast('Error refreshing data. Please try again later.', {
201
+ type: 'error',
202
+ position: 'top-right',
203
+ autoClose: 3000,
204
+ closeOnClick: true,
205
+ pauseOnHover: true,
206
+ });
207
+ }
208
+ return () => clearInterval(timer); // Cleanup the timer on complete
209
+ }
210
+
211
+ return (
212
+ <div className="w-full rounded-xl bg-white dark:bg-zinc-700/30 dark:from-inherit p-4 shadow-xl">
213
+ <div className="rounded-lg pt-5 pr-5 pl-5 flex h-[50vh] flex-col overflow-y-auto pb-4">
214
+ <h1 className='text-center font-bold text-xl mb-4'>Manage Users</h1>
215
+ <div className="flex items-center justify-start gap-2 mb-4">
216
+ {/* Refresh Data button */}
217
+ <button onClick={handleRefresh}
218
+ className="flex items-center justify-center hover:text-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 rounded-full p-1 transition duration-300 ease-in-out transform hover:scale-110 hover:bg-blue-500/10 focus:bg-blue-500/10"
219
+ title='Refresh Data'
220
+ >
221
+ {isRefreshed ? <RefreshCw className='w-5 h-5' /> : <RefreshCw className='w-5 h-5 animate-spin' />}
222
+ </button>
223
+ </div>
224
+ {loading ? (
225
+ <IconSpinner className='w-10 h-10 mx-auto my-auto animate-spin' />
226
+ ) : users.length === 0 ? (
227
+ <div className="mx-auto my-auto text-center text-lg text-gray-500 dark:text-gray-400">No Users Found.</div>
228
+ ) : (
229
+ <div className="relative overflow-x-auto rounded-lg">
230
+ <table className="w-full text-xl text-left rtl:text-right text-gray-500 dark:text-gray-400 p-4">
231
+ <thead className="text-sm text-center text-gray-700 uppercase bg-gray-400 dark:bg-gray-700 dark:text-gray-400">
232
+ <tr>
233
+ <th scope="col" className="px-6 py-3">Name</th>
234
+ <th scope="col" className="px-6 py-3">Email</th>
235
+ <th scope="col" className="px-6 py-3">Level</th>
236
+ <th scope="col" className="px-6 py-3">Actions</th>
237
+ </tr>
238
+ </thead>
239
+ <tbody>
240
+ {users.map((user, index) => (
241
+ <tr className="text-sm text-center item-center bg-gray-100 border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600" key={index}>
242
+ {/* Render table rows */}
243
+ <td className="px-6 py-3">{user.name}</td>
244
+ <td className="px-6 py-3">{user.email}</td>
245
+ <td className="px-6 py-3 font-bold">{user.isAdmin ? "Admin" : "User"}</td>
246
+ <td className="px-6 py-3 w-full flex flex-wrap justify-between gap-2">
247
+ {/* Promote/Demute Admin Status */}
248
+ {user.isAdmin ? (
249
+ <button onClick={() => handleDemote(user.id)}
250
+ title='Demote User'
251
+ className="flex flex-grow justify-center items-center text-sm disabled:bg-gray-500 bg-red-500 text-white px-3 py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:bg-red-500/40"
252
+ >
253
+ <span className='flex items-center'>
254
+ <UserX className='w-4 h-4 mr-1' />
255
+ <span>Demote</span>
256
+ </span>
257
+ </button>) : (
258
+ <button onClick={() => handlePromote(user.id)}
259
+ title='Promote User'
260
+ className="flex flex-grow justify-center items-center text-sm disabled:bg-gray-500 bg-green-500 text-white px-3 py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:bg-green-500/40"
261
+ >
262
+ <span className='flex items-center'>
263
+ <UserCheck className='w-4 h-4 mr-1' />
264
+ <span>Promote</span>
265
+ </span>
266
+ </button>
267
+ )
268
+ }
269
+
270
+ </td>
271
+ </tr>
272
+ ))}
273
+ </tbody>
274
+ </table>
275
+ </div>
276
+ )}
277
+ </div>
278
+ </div>
279
+ );
280
+ }
frontend/app/components/ui/admin/admin-menu.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { ListChecks, Users2, LibrarySquare } from 'lucide-react';
4
+ import { AdminMenuHandler } from '@/app/components/ui/admin/admin.interface';
5
+
6
+ export default function Admin(
7
+ props: Pick<AdminMenuHandler, "showUsers" | "setShowUsers" | "showNewRequest" | "setShowNewRequest" | "showCollections" | "setShowCollections">,
8
+ ) {
9
+ const handleShowRequestsTab = () => {
10
+ props.setShowNewRequest(true);
11
+ props.setShowUsers(false);
12
+ props.setShowCollections(false);
13
+ }
14
+ const handleShowUsersTab = () => {
15
+ props.setShowUsers(true);
16
+ props.setShowNewRequest(false);
17
+ props.setShowCollections(false);
18
+ }
19
+
20
+ const handleShowCollectionsTab = () => {
21
+ props.setShowCollections(true);
22
+ props.setShowNewRequest(false);
23
+ props.setShowUsers(false);
24
+ }
25
+
26
+ return (
27
+ <div className="w-full rounded-xl bg-white dark:bg-zinc-700/30 dark:from-inherit shadow-xl">
28
+ <div className="flex rounded-lg px-4 py-2 text-center items-center overflow-y-auto">
29
+ <button
30
+ className={`flex text-center items-center text-l ${props.showNewRequest ? 'text-blue-500' : ''} bg-transparent px-4 py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:scale-105`}
31
+ onClick={() => handleShowRequestsTab()}
32
+ title='New Requests'
33
+ >
34
+ <ListChecks className="mr-1 h-5 w-5" />
35
+ New Requests
36
+ </button>
37
+ <button
38
+ className={`flex text-center items-center text-l ${props.showCollections ? 'text-blue-500' : ''} bg-transparent px-4 py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:scale-105`}
39
+ onClick={() => handleShowCollectionsTab()}
40
+ title='Collections'
41
+ >
42
+ <LibrarySquare className="mr-1 h-5 w-5" />
43
+ Collections
44
+ </button>
45
+ <button
46
+ className={`flex text-center items-center text-l ${props.showUsers ? 'text-blue-500' : ''} bg-transparent px-4 py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:scale-105`}
47
+ onClick={() => handleShowUsersTab()}
48
+ title='Users'
49
+ >
50
+ <Users2 className="mr-1 h-5 w-5" />
51
+ Users
52
+ </button>
53
+ </div>
54
+ </div>
55
+ );
56
+ }
frontend/app/components/ui/admin/admin.interface.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ export interface AdminMenuHandler {
2
+ showNewRequest: boolean,
3
+ setShowNewRequest: (showNewRequest: boolean) => void,
4
+ showUsers: boolean,
5
+ setShowUsers: (showUsers: boolean) => void,
6
+ showCollections: boolean,
7
+ setShowCollections: (showCollections: boolean) => void,
8
+ }
frontend/app/components/ui/admin/index.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import AdminMenu from './admin-menu';
2
+ import AdminManageUsers from './admin-manage-users';
3
+ import AdminCollectionsRequests from './admin-collections-requests';
4
+ import AdminManageCollections from './admin-manage-collections';
5
+
6
+ export {
7
+ AdminMenu,
8
+ AdminCollectionsRequests,
9
+ AdminManageCollections,
10
+ AdminManageUsers
11
+ };
frontend/app/components/ui/autofill-prompt/autofill-prompt-dialog.tsx CHANGED
@@ -1,11 +1,12 @@
1
  import { useEffect, useState } from "react";
2
  import { QuestionsBankProp, psscocQuestionsBank, eirQuestionsBank } from "@/app/components/ui/autofill-prompt/autofill-prompt.interface";
3
  import { ChatHandler } from "@/app/components/ui/chat/chat.interface";
 
4
 
5
  export default function AutofillQuestion(
6
  props: Pick<
7
  ChatHandler,
8
- "docSelected" | "messages" | "isLoading" | "handleSubmit" | "handleInputChange" | "input"
9
  >,
10
  ) {
11
  // Keep track of whether to show the overlay
@@ -31,12 +32,16 @@ export default function AutofillQuestion(
31
  // Randomly select a subset of 3-4 questions
32
  useEffect(() => {
33
  // Select the questions bank based on the document set selected
34
- if (props.docSelected === "EIR") {
35
  setQuestionsBank(eirQuestionsBank);
36
  }
37
- else {
38
  setQuestionsBank(psscocQuestionsBank);
39
  }
 
 
 
 
40
  // Shuffle the questionsBank array
41
  const shuffledQuestions = shuffleArray(questionsBank);
42
  // Get a random subset of 3-4 questions
@@ -46,7 +51,7 @@ export default function AutofillQuestion(
46
  setTimeout(() => {
47
  setRandomQuestions(selectedQuestions);
48
  }, 300);
49
- }, [questionsBank, props.docSelected]);
50
 
51
 
52
  // Hide overlay when there are messages
@@ -59,34 +64,34 @@ export default function AutofillQuestion(
59
  }
60
  }, [props.messages, props.input]);
61
 
62
- // Automatically advance to the next question after a delay
63
- useEffect(() => {
64
- const timer = setInterval(() => {
65
- if (currentQuestionIndex < randomQuestions.length - 1) {
66
- setCurrentQuestionIndex((prevIndex) => prevIndex + 1);
67
- }
68
- else {
69
- clearInterval(timer); // Stop the timer when all questions have been displayed
70
- }
71
- }, 100); // Adjust the delay time as needed (e.g., 5000 milliseconds = 5 seconds)
72
-
73
- return () => clearInterval(timer); // Cleanup the timer on component unmount
74
- }, [currentQuestionIndex, randomQuestions]);
75
-
76
  // Handle autofill questions click
77
  const handleAutofillQuestionClick = (questionInput: string) => {
78
  props.handleInputChange({ target: { name: "message", value: questionInput } } as React.ChangeEvent<HTMLInputElement>);
79
  };
80
 
 
 
 
 
 
81
  return (
82
  <>
83
  {showOverlay && (
84
  <div className="w-full rounded-xl bg-white dark:bg-zinc-700/30 dark:from-inherit p-4 shadow-xl pb-0">
85
- <div className="rounded-lg pt-5 pr-10 pl-10 flex h-[50vh] flex-col divide-y overflow-y-auto pb-4">
86
- <h2 className="text-lg text-center font-semibold mb-4">How can I help you with {props.docSelected} today?</h2>
87
- {randomQuestions.map((question, index) => (
88
- <ul>
89
- <li key={index} className={`p-2 mb-2 border border-zinc-500/30 dark:border-white rounded-lg hover:bg-zinc-500/30 transition duration-300 ease-in-out transform cursor-pointer ${index <= currentQuestionIndex ? 'opacity-100 duration-500' : 'opacity-0'}`}>
 
 
 
 
 
 
 
 
 
90
  <button
91
  className="text-blue-500 w-full text-left"
92
  onClick={() => handleAutofillQuestionClick(question.title)}
@@ -94,8 +99,8 @@ export default function AutofillQuestion(
94
  {question.title}
95
  </button>
96
  </li>
97
- </ul>
98
- ))}
99
  </div>
100
  </div>
101
  )}
 
1
  import { useEffect, useState } from "react";
2
  import { QuestionsBankProp, psscocQuestionsBank, eirQuestionsBank } from "@/app/components/ui/autofill-prompt/autofill-prompt.interface";
3
  import { ChatHandler } from "@/app/components/ui/chat/chat.interface";
4
+ import { Undo2 } from "lucide-react";
5
 
6
  export default function AutofillQuestion(
7
  props: Pick<
8
  ChatHandler,
9
+ "collSelectedId" | "collSelectedName" | "messages" | "isLoading" | "handleSubmit" | "handleInputChange" | "input" | "handleCollIdSelect"
10
  >,
11
  ) {
12
  // Keep track of whether to show the overlay
 
32
  // Randomly select a subset of 3-4 questions
33
  useEffect(() => {
34
  // Select the questions bank based on the document set selected
35
+ if (props.collSelectedName === "EIR") {
36
  setQuestionsBank(eirQuestionsBank);
37
  }
38
+ else if (props.collSelectedName === "PSSCOC") {
39
  setQuestionsBank(psscocQuestionsBank);
40
  }
41
+ else {
42
+ // Do nothing and return
43
+ return;
44
+ }
45
  // Shuffle the questionsBank array
46
  const shuffledQuestions = shuffleArray(questionsBank);
47
  // Get a random subset of 3-4 questions
 
51
  setTimeout(() => {
52
  setRandomQuestions(selectedQuestions);
53
  }, 300);
54
+ }, [questionsBank, props.collSelectedName]);
55
 
56
 
57
  // Hide overlay when there are messages
 
64
  }
65
  }, [props.messages, props.input]);
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  // Handle autofill questions click
68
  const handleAutofillQuestionClick = (questionInput: string) => {
69
  props.handleInputChange({ target: { name: "message", value: questionInput } } as React.ChangeEvent<HTMLInputElement>);
70
  };
71
 
72
+ // Handle back button click
73
+ const handleBackButtonClick = () => {
74
+ props.handleCollIdSelect("");
75
+ };
76
+
77
  return (
78
  <>
79
  {showOverlay && (
80
  <div className="w-full rounded-xl bg-white dark:bg-zinc-700/30 dark:from-inherit p-4 shadow-xl pb-0">
81
+ <div className="rounded-lg pt-5 pr-10 pl-10 flex h-[50vh] flex-col overflow-y-auto pb-4">
82
+ <div className="flex items-center justify-center mb-4 gap-2">
83
+ <h2 className="text-lg text-center font-semibold">How can I help you with {props.collSelectedName} today?</h2>
84
+ <button
85
+ title="Go Back"
86
+ onClick={handleBackButtonClick}
87
+ className="hover:text-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 rounded-full p-1 transition duration-300 ease-in-out transform hover:scale-110 hover:bg-blue-500/10 focus:bg-blue-500/10"
88
+ >
89
+ <Undo2 className="w-6 h-6" />
90
+ </button>
91
+ </div>
92
+ <ul>
93
+ {randomQuestions.map((question, index) => (
94
+ <li key={index} className="p-2 mb-2 border border-zinc-500/30 dark:border-white rounded-lg hover:bg-zinc-500/30 transition duration-300 ease-in-out transform cursor-pointer">
95
  <button
96
  className="text-blue-500 w-full text-left"
97
  onClick={() => handleAutofillQuestionClick(question.title)}
 
99
  {question.title}
100
  </button>
101
  </li>
102
+ ))}
103
+ </ul>
104
  </div>
105
  </div>
106
  )}
frontend/app/components/ui/autofill-prompt/autofill-search-prompt-dialog.tsx CHANGED
@@ -1,11 +1,12 @@
1
  import { useEffect, useState } from "react";
2
  import { QuestionsBankProp, psscocQuestionsBank, eirQuestionsBank } from "@/app/components/ui/autofill-prompt/autofill-prompt.interface";
3
  import { SearchHandler } from "@/app/components/ui/search/search.interface";
 
4
 
5
  export default function AutofillSearchQuery(
6
  props: Pick<
7
  SearchHandler,
8
- "docSelected" | "query" | "isLoading" | "onSearchSubmit" | "onInputChange" | "results" | "searchButtonPressed"
9
  >,
10
  ) {
11
  // Keep track of whether to show the overlay
@@ -31,12 +32,16 @@ export default function AutofillSearchQuery(
31
  // Randomly select a subset of 3-4 questions
32
  useEffect(() => {
33
  // Select the questions bank based on the document set selected
34
- if (props.docSelected === "EIR") {
35
  setQuestionsBank(eirQuestionsBank);
36
  }
37
- else {
38
  setQuestionsBank(psscocQuestionsBank);
39
  }
 
 
 
 
40
  // Shuffle the questionsBank array
41
  const shuffledQuestions = shuffleArray(questionsBank);
42
  // Get a random subset of 3-4 questions
@@ -46,7 +51,7 @@ export default function AutofillSearchQuery(
46
  setTimeout(() => {
47
  setRandomQuestions(selectedQuestions);
48
  }, 300);
49
- }, [questionsBank, props.docSelected]);
50
 
51
 
52
  // Hide overlay when there are query
@@ -59,20 +64,6 @@ export default function AutofillSearchQuery(
59
  }
60
  }, [props.results, props.query]);
61
 
62
- // Automatically advance to the next question after a delay
63
- useEffect(() => {
64
- const timer = setInterval(() => {
65
- if (currentQuestionIndex < randomQuestions.length - 1) {
66
- setCurrentQuestionIndex((prevIndex) => prevIndex + 1);
67
- }
68
- else {
69
- clearInterval(timer); // Stop the timer when all questions have been displayed
70
- }
71
- }, 100); // Adjust the delay time as needed (e.g., 5000 milliseconds = 5 seconds)
72
-
73
- return () => clearInterval(timer); // Cleanup the timer on component unmount
74
- }, [currentQuestionIndex, randomQuestions]);
75
-
76
  // Handle autofill questions click
77
  const handleAutofillQuestionClick = (questionInput: string) => {
78
  if (props.onInputChange) {
@@ -80,16 +71,29 @@ export default function AutofillSearchQuery(
80
  }
81
  };
82
 
 
 
 
 
 
83
  return (
84
  <>
85
  {showOverlay && (
86
  <div className="relative mx-auto">
87
- <div className="rounded-lg pt-5 pr-10 pl-10 flex flex-col divide-y overflow-y-auto pb-4 bg-white dark:bg-zinc-700/30 shadow-xl">
88
- <h2 className="text-lg text-center font-semibold mb-4">How can I help with {props.docSelected} today?</h2>
89
- {/* {dialogMessage && <p className="text-center text-sm text-gray-500 mb-4">{dialogMessage}</p>} */}
90
- {randomQuestions.map((question, index) => (
91
- <ul>
92
- <li key={index} className={`p-2 mb-2 border border-zinc-500/30 dark:border-white rounded-lg hover:bg-zinc-500/30 transition duration-300 ease-in-out transform cursor-pointer ${index <= currentQuestionIndex ? 'opacity-100 duration-500' : 'opacity-0'}`}>
 
 
 
 
 
 
 
 
93
  <button
94
  className="text-blue-500 w-full text-left"
95
  onClick={() => handleAutofillQuestionClick(question.title)}
@@ -97,8 +101,8 @@ export default function AutofillSearchQuery(
97
  {question.title}
98
  </button>
99
  </li>
100
- </ul>
101
- ))}
102
  </div>
103
  </div>
104
  )}
 
1
  import { useEffect, useState } from "react";
2
  import { QuestionsBankProp, psscocQuestionsBank, eirQuestionsBank } from "@/app/components/ui/autofill-prompt/autofill-prompt.interface";
3
  import { SearchHandler } from "@/app/components/ui/search/search.interface";
4
+ import { Undo2 } from "lucide-react";
5
 
6
  export default function AutofillSearchQuery(
7
  props: Pick<
8
  SearchHandler,
9
+ "collSelectedId" | "collSelectedName" | "query" | "isLoading" | "onSearchSubmit" | "onInputChange" | "results" | "searchButtonPressed" | "handleCollIdSelect"
10
  >,
11
  ) {
12
  // Keep track of whether to show the overlay
 
32
  // Randomly select a subset of 3-4 questions
33
  useEffect(() => {
34
  // Select the questions bank based on the document set selected
35
+ if (props.collSelectedName === "EIR") {
36
  setQuestionsBank(eirQuestionsBank);
37
  }
38
+ else if (props.collSelectedName === "PSSCOC") {
39
  setQuestionsBank(psscocQuestionsBank);
40
  }
41
+ else {
42
+ // Do nothing and return
43
+ return;
44
+ }
45
  // Shuffle the questionsBank array
46
  const shuffledQuestions = shuffleArray(questionsBank);
47
  // Get a random subset of 3-4 questions
 
51
  setTimeout(() => {
52
  setRandomQuestions(selectedQuestions);
53
  }, 300);
54
+ }, [questionsBank, props.collSelectedName]);
55
 
56
 
57
  // Hide overlay when there are query
 
64
  }
65
  }, [props.results, props.query]);
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  // Handle autofill questions click
68
  const handleAutofillQuestionClick = (questionInput: string) => {
69
  if (props.onInputChange) {
 
71
  }
72
  };
73
 
74
+ // Handle back button click
75
+ const handleBackButtonClick = () => {
76
+ props.handleCollIdSelect("");
77
+ };
78
+
79
  return (
80
  <>
81
  {showOverlay && (
82
  <div className="relative mx-auto">
83
+ <div className="rounded-lg pt-5 pr-10 pl-10 flex flex-col overflow-y-auto pb-4 bg-white dark:bg-zinc-700/30 shadow-xl">
84
+ <div className="flex items-center justify-center mb-4 gap-2">
85
+ <h2 className="text-lg text-center font-semibold">How can I help you with {props.collSelectedName} today?</h2>
86
+ <button
87
+ title="Go Back"
88
+ onClick={handleBackButtonClick}
89
+ className="hover:text-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 rounded-full p-1 transition duration-300 ease-in-out transform hover:scale-110 hover:bg-blue-500/10 focus:bg-blue-500/10"
90
+ >
91
+ <Undo2 className="w-6 h-6" />
92
+ </button>
93
+ </div>
94
+ <ul>
95
+ {randomQuestions.map((question, index) => (
96
+ <li key={index} className="p-2 mb-2 border border-zinc-500/30 dark:border-white rounded-lg hover:bg-zinc-500/30 transition duration-300 ease-in-out transform cursor-pointer">
97
  <button
98
  className="text-blue-500 w-full text-left"
99
  onClick={() => handleAutofillQuestionClick(question.title)}
 
101
  {question.title}
102
  </button>
103
  </li>
104
+ ))}
105
+ </ul>
106
  </div>
107
  </div>
108
  )}
frontend/app/components/ui/chat/chat-selection.tsx CHANGED
@@ -2,53 +2,67 @@
2
 
3
  import { useState, useEffect } from 'react';
4
  import { ChatHandler } from '@/app/components/ui/chat';
5
-
6
- const DocumentSet = [
7
- 'PSSCOC',
8
- 'EIR',
9
- // Add More Document Set as needed
10
- ];
11
 
12
  export default function ChatSelection(
13
- props: Pick<ChatHandler, "docSelected" | "handleDocSelect">,
14
  ) {
15
- const [currentDocumentIndex, setCurrentDocumentIndex] = useState(0);
 
16
 
17
- const handleDocumentSetChange = (documentSet: string) => {
18
- props.handleDocSelect(documentSet);
 
19
  };
20
 
21
- // Automatically advance to the next document set after a delay
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  useEffect(() => {
23
- const timer = setInterval(() => {
24
- if (currentDocumentIndex < DocumentSet.length - 1) {
25
- setCurrentDocumentIndex((prevIndex) => prevIndex + 1);
26
- }
27
- else {
28
- clearInterval(timer); // Stop the timer when all document set have been displayed
29
- }
30
- }, 100); // Adjust the delay time as needed (e.g., 5000 milliseconds = 5 seconds)
31
-
32
- return () => clearInterval(timer); // Cleanup the timer on component unmount
33
- }, [currentDocumentIndex, DocumentSet]);
34
 
35
  return (
36
  <div className="w-full rounded-xl bg-white dark:bg-zinc-700/30 dark:from-inherit p-4 shadow-xl pb-0">
37
- <div className="rounded-lg pt-5 pr-10 pl-10 flex h-[50vh] flex-col divide-y overflow-y-auto pb-4">
38
  <h2 className="text-lg text-center font-semibold mb-4">Select Document Set to Chat with:</h2>
39
- {/* <p className="text-center text-sm text-gray-500 mb-4">{dialogMessage}</p> */}
40
- {DocumentSet.map((title, index) => (
41
- <ul>
42
- <li key={index} className={`p-2 mb-2 border border-zinc-500/30 dark:border-white rounded-lg hover:bg-zinc-500/30 transition duration-300 ease-in-out transform cursor-pointer ${index <= currentDocumentIndex ? 'opacity-100 duration-500' : 'opacity-0'}`}>
43
- <button
44
- className="text-blue-500 w-full text-left"
45
- onClick={() => handleDocumentSetChange(title)}
46
- >
47
- {title}
48
- </button>
49
- </li>
50
- </ul>
51
- ))}
52
  </div>
53
  </div>
54
  );
 
2
 
3
  import { useState, useEffect } from 'react';
4
  import { ChatHandler } from '@/app/components/ui/chat';
5
+ import { IconSpinner } from '@/app/components/ui/icons';
 
 
 
 
 
6
 
7
  export default function ChatSelection(
8
+ props: Pick<ChatHandler, "collSelectedId" | "collSelectedName" | "handleCollIdSelect" | "handleCollNameSelect">,
9
  ) {
10
+ const [publicCollections, setPublicCollections] = useState<any[]>([]);
11
+ const [isLoading, setIsLoading] = useState<boolean>(true); // Loading state
12
 
13
+ const handleCollectionSelect = (collectionId: string, displayName: string) => {
14
+ props.handleCollIdSelect(collectionId);
15
+ props.handleCollNameSelect(displayName);
16
  };
17
 
18
+ // Retrieve the public collection sets from the database
19
+ const getPublicCollections = async () => {
20
+ setIsLoading(true); // Set loading state to true
21
+ // Fetch the public collection sets from the API
22
+ const response = await fetch('/api/public/collections', {
23
+ method: 'GET',
24
+ headers: {
25
+ 'Content-Type': 'application/json',
26
+ 'Cache-Control': 'no-cache', // Disable caching
27
+ },
28
+ });
29
+
30
+ if (!response.ok) {
31
+ console.error("Error fetching public collections:", response.statusText);
32
+ return;
33
+ }
34
+
35
+ const data = await response.json();
36
+ // Sort the collections by created date in descending order (oldest first)
37
+ const sortedPublicCollections = data.publicCollections.sort((a: any, b: any) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
38
+ setPublicCollections(sortedPublicCollections);
39
+ setIsLoading(false); // Set loading state to false
40
+ }
41
+
42
+ // On component mount, retrieve the public collection sets from the database
43
  useEffect(() => {
44
+ getPublicCollections();
45
+ }, []);
46
+
47
+ // console.log('publicCollections:', publicCollections);
 
 
 
 
 
 
 
48
 
49
  return (
50
  <div className="w-full rounded-xl bg-white dark:bg-zinc-700/30 dark:from-inherit p-4 shadow-xl pb-0">
51
+ <div className="rounded-lg pt-5 pr-10 pl-10 flex h-[50vh] flex-col overflow-y-auto pb-4">
52
  <h2 className="text-lg text-center font-semibold mb-4">Select Document Set to Chat with:</h2>
53
+ {isLoading ? (
54
+ <IconSpinner className='w-10 h-10 mx-auto my-auto animate-spin' />
55
+ ) : publicCollections.length === 0 ? (<div className="mx-auto my-auto text-center text-lg text-gray-500 dark:text-gray-400">No collections found.</div>)
56
+ : publicCollections.map((collection, index) => (
57
+ <ul key={index}>
58
+ <li className="p-2 mb-2 border border-zinc-500/30 dark:border-white rounded-lg hover:bg-zinc-500/30 transition duration-300 ease-in-out transform cursor-pointer">
59
+ <button className="text-blue-500 w-full text-left" onClick={() => handleCollectionSelect(collection.collection_id, collection.display_name)}>
60
+ <div>{collection.display_name}</div>
61
+ <div className="text-sm text-gray-500">{collection.description}</div>
62
+ </button>
63
+ </li>
64
+ </ul>
65
+ ))}
66
  </div>
67
  </div>
68
  );
frontend/app/components/ui/chat/chat.interface.ts CHANGED
@@ -5,8 +5,10 @@ export interface Message {
5
  }
6
 
7
  export interface ChatHandler {
8
- docSelected: string;
9
- handleDocSelect: (doc: string) => void;
 
 
10
  messages: Message[];
11
  input: string;
12
  isLoading: boolean;
 
5
  }
6
 
7
  export interface ChatHandler {
8
+ collSelectedId: string;
9
+ collSelectedName: string;
10
+ handleCollIdSelect: (collection_id: string) => void;
11
+ handleCollNameSelect: (display_name: string) => void;
12
  messages: Message[];
13
  input: string;
14
  isLoading: boolean;
frontend/app/components/ui/chat/use-copy-to-clipboard.tsx CHANGED
@@ -23,7 +23,8 @@ export function useCopyToClipboard({
23
 
24
  const showToastMessage = () => {
25
  toast.success("Message copied to clipboard!", {
26
- position: "top-center",
 
27
  });
28
  };
29
 
 
23
 
24
  const showToastMessage = () => {
25
  toast.success("Message copied to clipboard!", {
26
+ position: "top-right",
27
+ closeOnClick: true,
28
  });
29
  };
30
 
frontend/app/components/ui/query/index.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import QuerySelection from "./query-selection";
2
+ import QueryDocumentUpload from "./query-document-upload";
3
+ import QueryMenu from "./query-menu";
4
+ import QueryCollectionManage from "./query-manage";
5
+
6
+ export { QueryMenu, QuerySelection, QueryDocumentUpload, QueryCollectionManage };
frontend/app/components/ui/query/query-document-upload.tsx ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useRef } from 'react';
4
+ import { toast } from 'react-toastify';
5
+ import Swal from 'sweetalert2';
6
+ import { AlertTriangle } from "lucide-react";
7
+ import { IconSpinner } from '@/app/components/ui/icons';
8
+ import { useSession } from "next-auth/react";
9
+
10
+ export default function QueryDocumentUpload() {
11
+ const [files, setFiles] = useState<File[]>([]);
12
+ const [displayName, setDisplayName] = useState('');
13
+ const [description, setDescription] = useState('');
14
+ const [displayNameError, setDisplayNameError] = useState(false);
15
+ const [descriptionError, setDescriptionError] = useState(false);
16
+ const [fileError, setFileError] = useState(false);
17
+ const [fileErrorMsg, setFileErrorMsg] = useState('');
18
+ const [isLoading, setisLoading] = useState(false);
19
+ const indexerApi = process.env.NEXT_PUBLIC_INDEXER_API;
20
+ const { data: session } = useSession();
21
+ const supabaseAccessToken = session?.supabaseAccessToken;
22
+
23
+ const MAX_FILES = 10; // Maximum number of files allowed
24
+ const MAX_TOTAL_SIZE = 15 * 1024 * 1024; // Maximum total size allowed (15 MB in bytes)
25
+ // The total size of all selected files should not exceed this value
26
+
27
+ const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
28
+ const selectedFiles = event.target.files;
29
+ if (selectedFiles) {
30
+ const fileList = Array.from(selectedFiles);
31
+
32
+ // Check if the total number of files exceeds the maximum allowed
33
+ if (fileList.length > MAX_FILES) {
34
+ // Show toast notification
35
+ toast.error(`You can only upload a maximum of ${MAX_FILES} files.`, {
36
+ position: "top-right",
37
+ autoClose: 5000,
38
+ hideProgressBar: false,
39
+ closeOnClick: true,
40
+ pauseOnHover: true,
41
+ });
42
+ setFileError(true);
43
+ setFileErrorMsg(`You can only upload a maximum of ${MAX_FILES} files.`);
44
+ return;
45
+ }
46
+
47
+ // Calculate the total size of selected files
48
+ const totalSize = fileList.reduce((acc, file) => acc + file.size, 0);
49
+
50
+ // Check if the total size exceeds the maximum allowed
51
+ if (totalSize > MAX_TOTAL_SIZE) {
52
+ // Show toast notification
53
+ toast.error(`Total size of selected files exceeds the maximum allowed (${MAX_TOTAL_SIZE} bytes).`, {
54
+ position: "top-right",
55
+ });
56
+ setFileError(true);
57
+ setFileErrorMsg(`Total size of selected files exceeds the maximum allowed (${MAX_TOTAL_SIZE} bytes).`);
58
+ return;
59
+ }
60
+
61
+ // Check if the file types are allowed
62
+ const allowedTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/plain'];
63
+ const invalidFiles = fileList.filter(file => !allowedTypes.includes(file.type));
64
+ if (invalidFiles.length) {
65
+ // Show toast notification
66
+ toast.error(`Invalid file type(s) selected!`, {
67
+ position: "top-right",
68
+ });
69
+ setFileError(true);
70
+ setFileErrorMsg(`Invalid file type(s) selected!`);
71
+ return;
72
+ }
73
+
74
+ // Update the state with the selected files
75
+ setFiles(fileList);
76
+ }
77
+ };
78
+
79
+ const handleDescriptionChange = (event: React.ChangeEvent<HTMLInputElement>) => {
80
+ setDescription(event.target.value);
81
+ setDescriptionError(false);
82
+ };
83
+
84
+ const handleDisplayNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
85
+ setDisplayName(event.target.value);
86
+ setDisplayNameError(false);
87
+ };
88
+
89
+ const handleSubmit = (event: React.FormEvent) => {
90
+ event.preventDefault();
91
+ // Perform validation and submit logic here
92
+ console.log("Display Name:", displayName);
93
+ console.log("Description:", description);
94
+ console.log("Files:", files);
95
+ // Ensure that the required fields are not empty
96
+ if (!displayName.trim()) {
97
+ setDisplayNameError(true);
98
+ console.log("Display Name is required!");
99
+ }
100
+ if (!description.trim()) {
101
+ setDescriptionError(true);
102
+ console.log("Description is required!");
103
+ }
104
+ if (!files.length) {
105
+ setFileError(true);
106
+ setFileErrorMsg("Please select a file to upload!");
107
+ console.log("Please select a file to upload!");
108
+ }
109
+ if (!displayName.trim() || !description.trim() || !files.length) {
110
+ // Show toast notification
111
+ toast.error("Please fill in all required fields!", {
112
+ position: "top-right",
113
+ closeOnClick: true,
114
+ });
115
+ }
116
+ else {
117
+ setisLoading(true);
118
+ // Show confirmation dialog
119
+ Swal.fire({
120
+ title: 'Are you sure?',
121
+ text: "You are about to upload and index your documents. Ensure that there are no sensitive/secret documents! Do you want to proceed?",
122
+ icon: 'warning',
123
+ showCancelButton: true,
124
+ confirmButtonColor: '#4caf50',
125
+ cancelButtonColor: '#b91c1c',
126
+ confirmButtonText: 'Yes',
127
+ cancelButtonText: 'No',
128
+ }).then((result) => {
129
+ if (result.isConfirmed) {
130
+ // Perform the upload and indexing logic
131
+ console.log("Uploading and indexing documents...");
132
+ // Make a POST request to the API with the form data to save to the database
133
+ fetch('/api/user/collections', {
134
+ method: 'POST',
135
+ headers: {
136
+ 'Content-Type': 'application/json',
137
+ },
138
+ body: JSON.stringify({
139
+ display_name: displayName,
140
+ description: description,
141
+ }),
142
+ })
143
+ .then(async response => {
144
+ if (response.ok) {
145
+ // Get the response data
146
+ const data = await response.json();
147
+ console.log('Insert New Collection Results:', data);
148
+ // Show success dialog
149
+ Swal.fire({
150
+ title: 'Success!',
151
+ text: 'Documents uploaded successfully! The documents will be indexed shortly. Do not leave this page until the indexing is completed!',
152
+ icon: 'success',
153
+ confirmButtonColor: '#4caf50',
154
+ });
155
+ // Show toast loading notification
156
+ const toastId = toast.loading('Uploading and Indexing Documents...');
157
+ // Create a new FormData object
158
+ const formData = new FormData();
159
+ // Append the collection_id to the FormData object
160
+ formData.append('collection_id', data.collectionId);
161
+ // Append each file to the FormData object
162
+ files.forEach((file, index) => {
163
+ formData.append('files', file);
164
+ });
165
+ // Make a POST request to the Backend Indexer API with the files data to upload and index
166
+ fetch(`${indexerApi}`, {
167
+ method: 'POST',
168
+ headers: {
169
+ // Add the access token to the request headers
170
+ 'Authorization': `Bearer ${supabaseAccessToken}`,
171
+ },
172
+ body: formData,
173
+ })
174
+ .then(async response => {
175
+ if (response.ok) {
176
+ // Get the response data
177
+ const data = await response.json();
178
+ console.log('Indexer Results:', data);
179
+ setisLoading(false);
180
+ // Update toast notification
181
+ toast.update(toastId, {
182
+ render: 'Documents uploaded and indexed successfully! 🎉',
183
+ type: 'success',
184
+ className: 'rotateY animated',
185
+ autoClose: 5000,
186
+ closeButton: true,
187
+ isLoading: false
188
+ });
189
+ // Reset the form fields
190
+ setDisplayName('');
191
+ setDescription('');
192
+ setFiles([]);
193
+ } else {
194
+ const data = await response.json();
195
+ // Log to console
196
+ console.error('Error uploading and indexing documents:', data.error);
197
+ setisLoading(false);
198
+ // Update toast notification
199
+ toast.update(toastId, {
200
+ render: 'Failed to upload and index documents! 😢 (Check Console for details)',
201
+ type: 'error',
202
+ className: 'rotateY animated',
203
+ autoClose: 5000,
204
+ closeButton: true,
205
+ isLoading: false
206
+ });
207
+ }
208
+ })
209
+ .catch(error => {
210
+ console.error('Error uploading and indexing documents:', error);
211
+ setisLoading(false);
212
+ // Update toast notification
213
+ toast.update(toastId, {
214
+ render: 'Failed to upload and index documents! 😢 (Check Console for details)',
215
+ type: 'error',
216
+ className: 'rotateY animated',
217
+ autoClose: 5000,
218
+ closeButton: true,
219
+ isLoading: false
220
+ });
221
+ });
222
+ } else {
223
+ const data = await response.json();
224
+ // Log to console
225
+ console.error('Error uploading and indexing documents:', data.error);
226
+ // Show error dialog
227
+ Swal.fire({
228
+ title: 'Error!',
229
+ text: 'Failed to upload and index documents. Please try again later. (Check Console for more details)',
230
+ icon: 'error',
231
+ confirmButtonColor: '#4caf50',
232
+ });
233
+ setisLoading(false);
234
+ }
235
+ })
236
+ .catch(error => {
237
+ console.error('Error uploading and indexing documents:', error);
238
+ // Show error dialog
239
+ Swal.fire({
240
+ title: 'Error!',
241
+ text: 'Failed to upload and index documents. Please try again later. (Check Console for more details)',
242
+ icon: 'error',
243
+ confirmButtonColor: '#4caf50',
244
+ });
245
+ setisLoading(false);
246
+ });
247
+ }
248
+ else {
249
+ setisLoading(false);
250
+ }
251
+ });
252
+ }
253
+ };
254
+
255
+ return (
256
+ <div className="w-full rounded-xl bg-white dark:bg-zinc-700/30 dark:from-inherit p-4 shadow-xl">
257
+ <div className="rounded-lg pt-5 pr-10 pl-10 flex h-[50vh] flex-col divide-y overflow-y-auto">
258
+ <form onSubmit={handleSubmit} className="flex flex-col w-full justify-center gap-4">
259
+ <h2 className="text-lg text-center font-semibold mb-4">Upload & Index Your Own Document Set:</h2>
260
+ {/* Warning Banner */}
261
+ <div className="flex flex-col bg-red-100 border border-orange-400 text-orange-600 px-4 py-3 rounded text-center items-center" role="alert">
262
+ <AlertTriangle />
263
+ <div className="flex text-center items-center font-bold">
264
+ WARNING
265
+ </div>
266
+ <div className="flex">Smart Retrieval is still in the demo stage, avoid uploading sensitive/secret documents.</div>
267
+ </div>
268
+ <div className={`flex flex-col ${displayNameError ? 'has-error' : ''}`}>
269
+ <label htmlFor="displayName" title='Display Name' className='mb-2'>Display Name:</label>
270
+ <input
271
+ type="text"
272
+ id="displayName"
273
+ title='Display Name'
274
+ value={displayName}
275
+ onChange={handleDisplayNameChange}
276
+ className={`h-10 rounded-lg w-full border px-2 bg-gray-300 dark:bg-zinc-700/65 ${displayNameError ? 'border-red-500 ' : ''}`}
277
+ />
278
+ {displayNameError && <p className="text-red-500 text-sm pl-1 pt-1">Display Name is required!</p>}
279
+ </div>
280
+ <div className={`flex flex-col ${descriptionError ? 'has-error' : ''}`}>
281
+ <label htmlFor="collectionName" title='Description' className='mb-2'>Description:</label>
282
+ <input
283
+ type="text"
284
+ id="description"
285
+ title='Description'
286
+ value={description}
287
+ onChange={handleDescriptionChange}
288
+ className={`h-10 rounded-lg w-full border px-2 bg-gray-300 dark:bg-zinc-700/65 ${descriptionError ? 'border-red-500' : ''}`}
289
+ />
290
+ {descriptionError && <p className="text-red-500 text-sm pl-1 pt-1">Description is required!</p>}
291
+ </div>
292
+ <div className='flex flex-col'>
293
+ <label htmlFor="fileUpload" title='Select Files' className='mb-2'>Select Files:</label>
294
+ <input
295
+ type="file"
296
+ id="fileUpload"
297
+ title='Select Files'
298
+ multiple
299
+ accept=".pdf,.doc,.docx,.xls,xlsx,.txt"
300
+ onChange={handleFileChange}
301
+ className={`h-12 rounded-lg w-full bg-gray-300 dark:bg-zinc-700/65 border px-2 py-2 ${fileError ? 'border-red-500' : ''}`}
302
+ />
303
+ {fileError && <p className="text-red-500 text-sm pl-1 pt-1">{fileErrorMsg}</p>}
304
+ </div>
305
+ <div className="flex flex-col gap-4">
306
+ <button
307
+ disabled={isLoading}
308
+ type="submit"
309
+ title='Submit'
310
+ className="text-center items-center text-l disabled:bg-orange-400 bg-blue-500 text-white px-6 py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:scale-105 disabled:hover:scale-100">
311
+ {isLoading ? <IconSpinner className="animate-spin h-5 w-5 mx-auto" /> : "Submit"}
312
+ </button>
313
+ </div>
314
+ </form>
315
+ </div>
316
+ </div>
317
+ );
318
+ }
frontend/app/components/ui/query/query-manage.tsx ADDED
@@ -0,0 +1,575 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { List, Table, Trash, Eye, EyeOff, RefreshCw } from 'lucide-react';
5
+ import Swal from 'sweetalert2';
6
+ import { toast } from 'react-toastify';
7
+ import { IconSpinner } from '@/app/components/ui/icons';
8
+ import { useSession } from 'next-auth/react';
9
+
10
+ export default function QueryCollectionManage() {
11
+ const [userCollections, setUserCollections] = useState<any[]>([]);
12
+ const [tableView, setTableView] = useState<boolean>(false); // Track whether to show the table view
13
+ const [isRefreshed, setIsRefreshed] = useState<boolean>(true); // Track whether the data has been refreshed
14
+ const [isLoading, setIsLoading] = useState<boolean>(true); // Track whether the data is loading
15
+ const { data: session, status } = useSession();
16
+ const supabaseAccessToken = session?.supabaseAccessToken;
17
+
18
+ // Retrieve the user's collections and public collections requests data from the database
19
+ const getUserCollectionsandRequests = async () => {
20
+ setIsLoading(true);
21
+ // Fetch the user's public collection requests from the API
22
+ fetch('/api/user/collections-requests'
23
+ , {
24
+ method: 'GET',
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ 'Cache-Control': 'no-cache', // Disable caching
28
+ },
29
+ }
30
+ )
31
+ .then((response) => response.json())
32
+ .then((data) => {
33
+ const publicCollectionsRequests = data.userPubCollectionsReq;
34
+ // console.log('Public Collections Requests:', publicCollectionsRequests);
35
+ // Sort the collections by created date in descending order (oldest first)
36
+ publicCollectionsRequests.sort((a: any, b: any) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
37
+ // Extract the collection data from the public collections requests
38
+ const updatedCollections = publicCollectionsRequests.map((collection: any) => {
39
+ // Check if the collection has any public collection requests
40
+ if (collection.public_collections_requests.length === 0) {
41
+ // If not, return the collection data with no other details
42
+ return {
43
+ collection_id: collection.collection_id,
44
+ display_name: collection.display_name,
45
+ description: collection.description,
46
+ isPublic: collection.is_public,
47
+ // Convert the date to a readable format in the user's locale and timezone e.g. "2022-01-01T12:00:00" => "1 Jan, 2022, 12:00:00 PM"
48
+ created_at: new Date(collection.created_at).toLocaleDateString('en-GB', { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true }),
49
+ };
50
+ }
51
+ else {
52
+ // If the collection has public collection requests, return the collection data with the request status and dates
53
+ return {
54
+ collection_id: collection.collection_id,
55
+ display_name: collection.display_name,
56
+ description: collection.description,
57
+ created_at: new Date(collection.created_at).toLocaleDateString('en-GB', { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true }),
58
+ isPublic: collection.is_public,
59
+ requestType: collection.public_collections_requests[0].is_make_public ? 'Public' : 'Private',
60
+ requestStatus: collection.public_collections_requests[0].is_pending ? '⏳Pending' : collection.public_collections_requests[0].is_approved ? '✅Approved' : '❌Rejected',
61
+ requestDate: new Date(collection.public_collections_requests[0].created_at).toLocaleDateString('en-GB', { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true }),
62
+ updatedRequestDate: new Date(collection.public_collections_requests[0].updated_at).toLocaleDateString('en-GB', { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true }),
63
+ };
64
+ }
65
+ });
66
+ // Update the userCollections state with the fetched data
67
+ setUserCollections(updatedCollections);
68
+ setIsLoading(false);
69
+ })
70
+ .catch((error) => {
71
+ console.error("Error fetching user public collection requests:", error);
72
+ setIsLoading(false);
73
+ return false;
74
+ });
75
+ return true;
76
+ }
77
+
78
+
79
+ // Fetch the user's collections and public collections requests data from the database on component mount
80
+ useEffect(() => {
81
+ getUserCollectionsandRequests();
82
+ }, []);
83
+
84
+ // Function to toggle between list and table views
85
+ const toggleView = () => {
86
+ setTableView((prevValue) => !prevValue);
87
+ };
88
+
89
+ // Function to handle requesting to be public or private
90
+ const handleRequest = (collectionId: string, isPublic: boolean) => {
91
+ // Implement request logic here
92
+ console.log(`Requesting to set ${isPublic ? 'be Public' : 'be Private'} for collection with ID: ${collectionId}`);
93
+ // Display a confirmation dialog
94
+ Swal.fire({
95
+ title: 'Request Confirmation',
96
+ text: `Are you sure you want to request to ${isPublic ? 'make this collection Public (available to other users in "Chat/Search")' : 'make this collection Private (remove from "Chat/Search")'}?`,
97
+ icon: 'question',
98
+ showCancelButton: true,
99
+ confirmButtonText: 'Yes',
100
+ cancelButtonText: 'No',
101
+ confirmButtonColor: '#4caf50',
102
+ cancelButtonColor: '#b91c1c',
103
+ }).then((result) => {
104
+ if (result.isConfirmed) {
105
+ // Check if there is an existing request for the collection
106
+ const existingRequest = userCollections.find((collection) => collection.collection_id === collectionId)?.requestStatus;
107
+ const existingRequestType = userCollections.find((collection) => collection.collection_id === collectionId)?.requestType;
108
+ // If an existing request is found, Make a POST request to the API & display a confirmation dialog
109
+ if (existingRequest) {
110
+ // Display a confirmation dialog if there is an existing request
111
+ Swal.fire({
112
+ title: 'Existing Request Found',
113
+ text: `Existing Request with Status: ${existingRequest} for Type: ${existingRequestType}. Do you want to proceed with this new request?`,
114
+ icon: 'question',
115
+ showCancelButton: true,
116
+ confirmButtonText: 'Yes',
117
+ cancelButtonText: 'No',
118
+ confirmButtonColor: '#4caf50',
119
+ cancelButtonColor: '#b91c1c',
120
+ }).then((result) => {
121
+ if (result.isConfirmed) {
122
+ // If confirm, make a put request to the API & display a success/error message
123
+ fetch('/api/user/collections-requests',
124
+ {
125
+ method: 'PUT',
126
+ headers: {
127
+ 'Content-Type': 'application/json',
128
+ 'Cache-Control': 'no-cache', // Disable caching
129
+ },
130
+ body: JSON.stringify({ collection_id: collectionId, is_make_public: isPublic }),
131
+ }
132
+ )
133
+ .then(async response => {
134
+ if (response.ok) {
135
+ // Show success dialog
136
+ console.log('Request sent successfully:', response.statusText);
137
+ Swal.fire({
138
+ title: 'Request Sent',
139
+ text: `Your request to ${isPublic ? 'make this collection Public' : 'make this collection Private'} has been sent successfully.`,
140
+ icon: 'success',
141
+ confirmButtonText: 'OK',
142
+ confirmButtonColor: '#4caf50',
143
+ });
144
+ // Refresh the userCollections state after the request is sent
145
+ getUserCollectionsandRequests();
146
+ } else {
147
+ const data = await response.json();
148
+ // Log to console
149
+ console.error('Error sending request:', data.error);
150
+ // Show error dialog
151
+ Swal.fire({
152
+ title: 'Request Failed',
153
+ text: 'An error occurred while sending the request. Please try again later. (Check Console for more details)',
154
+ icon: 'error',
155
+ confirmButtonText: 'OK',
156
+ confirmButtonColor: '#4caf50',
157
+ });
158
+ }
159
+ })
160
+ .catch(error => {
161
+ console.error('Error sending request:', error);
162
+ // Show error dialog
163
+ Swal.fire({
164
+ title: 'Error!',
165
+ text: 'Failed to send request. Please try again later. (Check Console for more details)',
166
+ icon: 'error',
167
+ confirmButtonColor: '#4caf50',
168
+ });
169
+ });
170
+ }
171
+ });
172
+ }
173
+ // If there is no existing request, make a POST request to the API & display a success/error message
174
+ else {
175
+ fetch('/api/user/collections-requests',
176
+ {
177
+ method: 'POST',
178
+ headers: {
179
+ 'Content-Type': 'application/json',
180
+ 'Cache-Control': 'no-cache', // Disable caching
181
+ },
182
+ body: JSON.stringify({ collection_id: collectionId, is_make_public: isPublic }),
183
+ }
184
+ )
185
+ .then(async response => {
186
+ if (response.ok) {
187
+ // Show success dialog
188
+ console.log('Request sent successfully:', response.statusText);
189
+ Swal.fire({
190
+ title: 'Request Sent',
191
+ text: `Your request to ${isPublic ? 'make this collection Public' : 'make this collection Private'} has been sent successfully.`,
192
+ icon: 'success',
193
+ confirmButtonText: 'OK',
194
+ confirmButtonColor: '#4caf50',
195
+ });
196
+ // Refresh the userCollections state after the request is sent
197
+ getUserCollectionsandRequests();
198
+ } else {
199
+ const data = await response.json();
200
+ // Log to console
201
+ console.error('Error sending request:', data.error);
202
+ // Show error dialog
203
+ Swal.fire({
204
+ title: 'Request Failed',
205
+ text: 'An error occurred while sending the request. Please try again later. (Check Console for more details)',
206
+ icon: 'error',
207
+ confirmButtonText: 'OK',
208
+ confirmButtonColor: '#4caf50',
209
+ });
210
+ }
211
+ })
212
+ .catch(error => {
213
+ console.error('Error sending request:', error);
214
+ // Show error dialog
215
+ Swal.fire({
216
+ title: 'Error!',
217
+ text: 'Failed to send request. Please try again later. (Check Console for more details)',
218
+ icon: 'error',
219
+ confirmButtonColor: '#4caf50',
220
+ });
221
+ });
222
+ }
223
+ }
224
+ });
225
+ };
226
+
227
+ // Function to handle cancelling a request
228
+ const handleCancelRequest = (collectionId: string, isPublic: boolean) => {
229
+ // Implement cancel request logic here
230
+ console.log(`Cancelling request for collection with ID: ${collectionId}`);
231
+ // Display a confirmation dialog
232
+ Swal.fire({
233
+ title: 'Cancel Request Confirmation',
234
+ text: `Are you sure you want to cancel the request to ${isPublic ? 'make this collection Private' : 'make this collection Public'}?`,
235
+ icon: 'question',
236
+ showCancelButton: true,
237
+ confirmButtonText: 'Yes',
238
+ cancelButtonText: 'No',
239
+ confirmButtonColor: '#4caf50',
240
+ cancelButtonColor: '#b91c1c',
241
+ }).then((result) => {
242
+ if (result.isConfirmed) {
243
+ // Make a delete request to the API & display a success/error message
244
+ fetch('/api/user/collections-requests',
245
+ {
246
+ method: 'DELETE',
247
+ headers: {
248
+ 'Content-Type': 'application/json',
249
+ 'Cache-Control': 'no-cache', // Disable caching
250
+ },
251
+ body: JSON.stringify({ collection_id: collectionId }),
252
+ }
253
+ )
254
+ .then(async response => {
255
+ if (response.ok) {
256
+ // Show success dialog
257
+ console.log('Request cancelled successfully:', response.statusText);
258
+ Swal.fire({
259
+ title: 'Request Cancelled',
260
+ text: `Your request to ${isPublic ? 'make this collection Private' : 'make this collection Public'} has been cancelled successfully.`,
261
+ icon: 'success',
262
+ confirmButtonText: 'OK',
263
+ confirmButtonColor: '#4caf50',
264
+ });
265
+ // Refresh the userCollections state after the request is sent
266
+ getUserCollectionsandRequests();
267
+ } else {
268
+ const data = await response.json();
269
+ // Log to console
270
+ console.error('Error cancelling request:', data.error);
271
+ // Show error dialog
272
+ Swal.fire({
273
+ title: 'Request Cancellation Failed',
274
+ text: 'An error occurred while cancelling the request. Please try again later. (Check Console for more details)',
275
+ icon: 'error',
276
+ confirmButtonText: 'OK',
277
+ confirmButtonColor: '#4caf50',
278
+ });
279
+ }
280
+ })
281
+ .catch(error => {
282
+ console.error('Error cancelling request:', error);
283
+ // Show error dialog
284
+ Swal.fire({
285
+ title: 'Error!',
286
+ text: 'Failed to upload and index document set. Please try again later. (Check Console for more details)',
287
+ icon: 'error',
288
+ confirmButtonColor: '#4caf50',
289
+ });
290
+ });
291
+ }
292
+ });
293
+ };
294
+
295
+ // Function to handle deleting a collection
296
+ const handleDelete = (collectionId: string, isPublic: boolean) => {
297
+ // Implement delete logic here
298
+ console.log(`Deleting collection with ID: ${collectionId}`);
299
+ // Display a confirmation dialog
300
+ Swal.fire({
301
+ title: 'Delete Confirmation',
302
+ text: `Are you sure you want to delete this collection? This action cannot be undone!`,
303
+ icon: 'warning',
304
+ showCancelButton: true,
305
+ confirmButtonText: 'Yes',
306
+ cancelButtonText: 'No',
307
+ confirmButtonColor: '#4caf50',
308
+ cancelButtonColor: '#b91c1c',
309
+ }).then((result) => {
310
+ if (result.isConfirmed) {
311
+ // If the user confirms the delete, make a delete request to the API, display a success/error message
312
+ fetch('/api/user/collections',
313
+ {
314
+ method: 'DELETE',
315
+ headers: {
316
+ 'Content-Type': 'application/json',
317
+ 'Cache-Control': 'no-cache', // Disable caching
318
+ 'Authorization': `Bearer ${supabaseAccessToken}`, // Add the access token to the request headers
319
+ },
320
+ body: JSON.stringify({ collection_id: collectionId }),
321
+ }
322
+ )
323
+ .then(async response => {
324
+ if (response.ok) {
325
+ // If the delete request is successful, display a success message
326
+ console.log('Collection deleted successfully:', response.statusText);
327
+ Swal.fire({
328
+ title: 'Collection Deleted!',
329
+ text: 'Your collection has been deleted successfully.',
330
+ icon: 'success',
331
+ confirmButtonColor: '#4caf50',
332
+ });
333
+ // Refresh the userCollections state after the request is sent
334
+ getUserCollectionsandRequests();
335
+ } else {
336
+ const data = await response.json();
337
+ // Log to console
338
+ console.error('Error deleting collection:', data.error);
339
+ // Show error dialog
340
+ Swal.fire({
341
+ title: 'Error!',
342
+ text: 'Failed to delete collection. Please try again later. (Check Console for more details)',
343
+ icon: 'error',
344
+ confirmButtonColor: '#4caf50',
345
+ });
346
+ }
347
+ })
348
+ .catch(error => {
349
+ console.error('Error uploading and indexing document set:', error);
350
+ // Show error dialog
351
+ Swal.fire({
352
+ title: 'Error!',
353
+ text: 'Failed to delete collection. Please try again later. (Check Console for more details)',
354
+ icon: 'error',
355
+ confirmButtonColor: '#4caf50',
356
+ });
357
+ });
358
+ }
359
+ });
360
+ };
361
+
362
+ const handleRefresh = async () => {
363
+ setIsRefreshed(false);
364
+ const timer = setInterval(() => {
365
+ // Timer to simulate a spinner while refreshing data
366
+ setIsRefreshed(true);
367
+ }, 1000); // Adjust the delay time as needed (e.g., 5000 milliseconds = 5 seconds)
368
+ // Display a toast notification
369
+ if (await getUserCollectionsandRequests()) {
370
+ toast('Data refreshed successfully!', {
371
+ type: 'success',
372
+ position: 'top-right',
373
+ autoClose: 3000,
374
+ closeOnClick: true,
375
+ pauseOnHover: true,
376
+ });
377
+ }
378
+ else {
379
+ toast('Error refreshing data. Please try again later.', {
380
+ type: 'error',
381
+ position: 'top-right',
382
+ autoClose: 3000,
383
+ closeOnClick: true,
384
+ pauseOnHover: true,
385
+ });
386
+ }
387
+ return () => clearInterval(timer); // Cleanup the timer on complete
388
+ }
389
+
390
+ return (
391
+ <div className="w-full rounded-xl bg-white dark:bg-zinc-700/30 dark:from-inherit p-4 shadow-xl">
392
+ <div className="rounded-lg pt-5 pr-5 pl-5 flex h-[50vh] flex-col overflow-y-auto pb-4">
393
+ <h2 className="text-lg text-center font-semibold mb-4">Manage Your Collections:</h2>
394
+ <div className="flex justify-between mb-2 mx-2">
395
+ {/* Refresh Data button */}
396
+ <button onClick={handleRefresh}
397
+ className="flex items-center justify-center hover:text-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 rounded-full p-1 transition duration-300 ease-in-out transform hover:scale-110 hover:bg-blue-500/10 focus:bg-blue-500/10"
398
+ title='Refresh Data'
399
+ >
400
+ {isRefreshed ? <RefreshCw className='w-5 h-5' /> : <RefreshCw className='w-5 h-5 animate-spin' />}
401
+ </button>
402
+ {/* Button to toggle between list and table views */}
403
+ <button onClick={toggleView} className="flex items-center text-center text-sm text-gray-500 underline gap-2 hover:text-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 rounded-full p-1 transition duration-300 ease-in-out transform hover:scale-110 hover:bg-blue-500/10 focus:bg-blue-500/10">
404
+ {tableView ? <Table /> : <List />}
405
+ {tableView ? 'Table View' : 'List View'}
406
+ </button>
407
+ </div>
408
+ {/* Render list or table view based on the tableView state */}
409
+ {isLoading ? (
410
+ <IconSpinner className='w-10 h-10 mx-auto my-auto animate-spin' />
411
+ ) : userCollections.length === 0 ? (
412
+ <div className="mx-auto my-auto text-center text-lg text-gray-500 dark:text-gray-400">No collections found.</div>
413
+ ) : tableView ? (
414
+ <div className="relative overflow-x-auto rounded-lg">
415
+ <table className="w-full text-xl text-left rtl:text-right text-gray-500 dark:text-gray-400 p-4">
416
+ <thead className="text-sm text-center text-gray-700 uppercase bg-gray-400 dark:bg-gray-700 dark:text-gray-400">
417
+ <tr>
418
+ <th scope="col" className="px-6 py-3">Display Name</th>
419
+ <th scope="col" className="px-6 py-3">Description</th>
420
+ <th scope="col" className="px-6 py-3">Created</th>
421
+ <th scope="col" className="px-6 py-3">Current Visibility</th>
422
+ <th scope="col" className="px-6 py-3">Request Status</th>
423
+ <th scope="col" className="px-6 py-3">Requested</th>
424
+ <th scope="col" className="px-6 py-3">Request Updated</th>
425
+ <th scope="col" className="px-6 py-3">Actions</th>
426
+ </tr>
427
+ </thead>
428
+ <tbody>
429
+ {userCollections.map((collection, index) => (
430
+ <tr className="text-sm text-center item-center bg-gray-100 border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600" key={index}>
431
+ {/* Render table rows */}
432
+ <td className="px-6 py-3">{collection.display_name}</td>
433
+ <td className="px-6 py-3">{collection.description}</td>
434
+ <td className="px-6 py-3">{collection.created_at}</td>
435
+ <td className="px-6 py-3">{collection.isPublic ? <div className='flex items-center'><Eye className='w-4 h-4 mr-1' /> Public</div> : <div className='flex items-center'><EyeOff className='w-4 h-4 mr-1' /> Private</div>}</td>
436
+ <td className="px-6 py-3">
437
+ <div>
438
+ <span className={
439
+ `text-nowrap ${collection.requestStatus === '⏳Pending' ? 'text-orange-400 ' :
440
+ collection.requestStatus === '✅Approved' ? 'text-green-500' :
441
+ collection.requestStatus === '❌Rejected' ? 'text-red-500' : ''}`
442
+ }>
443
+ {collection.requestStatus}
444
+ </span>
445
+ </div>
446
+ </td>
447
+ <td className="px-6 py-3">{collection.requestDate}</td>
448
+ <td className="px-6 py-3">{collection.updatedRequestDate}</td>
449
+ <td className="px-6 py-3 w-full">
450
+ <div className='flex flex-col justify-between gap-2'>
451
+ {/* Conditional rendering button based on request status */}
452
+ {collection.requestStatus === '⏳Pending' ? (
453
+ <button onClick={() => handleCancelRequest(collection.collection_id, collection.isPublic)}
454
+ title='Cancel Request'
455
+ className="flex flex-grow justify-center text-center items-center text-xs lg:text-sm bg-orange-400 text-white px-1 py-1 lg:px-3 lg:py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:bg-orange-500/40"
456
+ >
457
+ Cancel Request
458
+ </button>
459
+ ) :
460
+ collection.isPublic ? (
461
+ <button onClick={() => handleRequest(collection.collection_id, false)}
462
+ disabled={collection.requestStatus === '⏳Pending'}
463
+ title='Set Private'
464
+ className="flex flex-grow justify-center text-center items-center text-sm disabled:bg-gray-500 bg-blue-500 text-white px-3 py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:bg-blue-500/40"
465
+ >
466
+ <EyeOff className='w-5 h-5 mr-1' />
467
+ Set Private
468
+ </button>
469
+ ) : (
470
+ <button onClick={() => handleRequest(collection.collection_id, true)}
471
+ disabled={collection.requestStatus === '⏳Pending'}
472
+ title='Set Public'
473
+ className="flex flex-grow justify-center text-center items-center text-sm disabled:bg-gray-500 bg-blue-500 text-white px-3 py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:bg-blue-500/40"
474
+ >
475
+ <Eye className='w-5 h-5 mr-1' />
476
+ Set Public
477
+ </button>
478
+ )}
479
+ {/* Conditional rendering of delete button based on current visibility of collection, only private collection can be deleted */}
480
+ {!collection.isPublic && (
481
+ <button onClick={() => handleDelete(collection.collection_id, true)}
482
+ title='Delete'
483
+ className="flex flex-grow justify-center text-center items-center text-sm disabled:bg-gray-500 bg-red-500 text-white px-3 py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:bg-red-500/40"
484
+ >
485
+ <Trash className='w-4 h-4 mr-1' />
486
+ Delete
487
+ </button>
488
+ )}
489
+ </div>
490
+ </td>
491
+ </tr>
492
+ ))}
493
+ </tbody>
494
+ </table>
495
+ </div>
496
+ ) : (
497
+ <ul className={`transition duration-500 ease-in-out transform ${!isLoading ? "" : "animate-pulse"}`}>
498
+ {userCollections.map((collection, index) => (
499
+ <li key={index} className="p-2 mb-2 border border-zinc-500/30 dark:border-white rounded-lg overflow-y-auto">
500
+ {/* Render list items */}
501
+ <div className="flex items-center justify-between">
502
+ <div className='justify-start'>
503
+ <div className='text-xs lg:text-base text-blue-500'>{collection.display_name}</div>
504
+ <div className="text-xs lg:text-sm text-gray-500">{collection.description}</div>
505
+ <div className="border-b border-zinc-500/30 dark:border-white my-2"></div> {/* Divider */}
506
+ <div className="text-xs lg:text-sm"><span className='font-bold'>Created: </span>{collection.created_at}</div>
507
+ <div className='flex items-center text-xs lg:text-sm'><span className='font-bold'>Current Visibility: </span>{collection.isPublic ? <span className='flex text-center items-center'><Eye className='w-4 h-4 mx-1' />Public</span> : <span className='flex text-center items-center'><EyeOff className='w-4 h-4 mx-1' />Private</span>}</div>
508
+
509
+ {/* Render request status and dates */}
510
+ {collection.requestStatus && (
511
+ <div className='flex flex-col items-start text-xs lg:text-sm'>
512
+ <div><span className='font-bold'>Request Type: </span>{collection.requestType}</div>
513
+ <div>
514
+ <span className='font-bold'>Request Status: </span>
515
+ <span className={
516
+ `${collection.requestStatus === '⏳Pending' ? 'text-orange-400' :
517
+ collection.requestStatus === '✅Approved' ? 'text-green-500' :
518
+ collection.requestStatus === '❌Rejected' ? 'text-red-500' : ''}`
519
+ }>
520
+ {collection.requestStatus}
521
+ </span>
522
+ </div>
523
+ <div><span className='font-bold'>Requested: </span>{collection.requestDate}</div>
524
+ <div><span className='font-bold'>Request Updated: </span>{collection.updatedRequestDate}</div>
525
+ </div>
526
+ )}
527
+ </div>
528
+ {/* Manage section */}
529
+ <div className="flex flex-col justify-between gap-2">
530
+ {/* Conditional rendering button based on request status */}
531
+ {collection.requestStatus === '⏳Pending' ? (
532
+ <button onClick={() => handleCancelRequest(collection.collection_id, collection.isPublic)}
533
+ title='Cancel Request'
534
+ className="flex flex-grow text-center items-center justify-center text-xs lg:text-sm bg-orange-400 text-white px-1 py-1 lg:px-3 lg:py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:bg-orange-500/40"
535
+ >
536
+ Cancel Request
537
+ </button>
538
+ ) :
539
+ collection.isPublic ? (
540
+ <button onClick={() => handleRequest(collection.collection_id, false)}
541
+ title='Set Private'
542
+ className="flex flex-grow text-center items-center justify-center text-xs lg:text-sm bg-blue-500 text-white px-1 py-1 lg:px-3 lg:py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:bg-blue-500/40"
543
+ >
544
+ <EyeOff className='w-4 h-4 mr-1' />
545
+ <span>Set Private</span>
546
+ </button>
547
+ ) : (
548
+ <button onClick={() => handleRequest(collection.collection_id, true)}
549
+ title='Set Public'
550
+ className="flex flex-grow text-center items-center justify-center text-xs lg:text-sm bg-blue-500 text-white px-1 py-1 lg:px-3 lg:py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:bg-blue-500/40"
551
+ >
552
+ <Eye className='w-4 h-4 mr-1' />
553
+ <span>Set Public</span>
554
+ </button>
555
+ )}
556
+ {/* Conditional rendering of delete button based on current visibility of collection, only private collection can be deleted */}
557
+ {!collection.isPublic && (
558
+ <button onClick={() => handleDelete(collection.collection_id, true)}
559
+ title='Delete'
560
+ className="flex flex-grow text-center items-center justify-center text-xs lg:text-sm bg-red-500 text-white px-2 py-2 lg:px-3 lg:py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:bg-red-500/40"
561
+ >
562
+ <Trash className='w-4 h-4 mr-1' />
563
+ <span>Delete</span>
564
+ </button>
565
+ )}
566
+ </div>
567
+ </div>
568
+ </li>
569
+ ))}
570
+ </ul>
571
+ )}
572
+ </div>
573
+ </div>
574
+ );
575
+ }
frontend/app/components/ui/query/query-menu.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { MessageCircle, Upload, FileCog } from 'lucide-react';
4
+ import { QueryMenuHandler } from '@/app/components/ui/query/query.interface';
5
+
6
+ export default function QueryMenu(
7
+ props: Pick<QueryMenuHandler, "showUpload" | "setShowUpload" | "showChat" | "setShowChat" | "showManage" | "setShowManage" | "setCollSelectedId">,
8
+ ) {
9
+ const handleShowChatTab = () => {
10
+ props.setShowChat(true);
11
+ props.setShowUpload(false);
12
+ props.setShowManage(false);
13
+ props.setCollSelectedId('');
14
+ }
15
+ const handleShowUploadTab = () => {
16
+ props.setShowUpload(true);
17
+ props.setShowChat(false);
18
+ props.setShowManage(false);
19
+ props.setCollSelectedId('');
20
+ }
21
+
22
+ const handleShowManageTab = () => {
23
+ props.setShowManage(true);
24
+ props.setShowUpload(false);
25
+ props.setShowChat(false);
26
+ props.setCollSelectedId('');
27
+ }
28
+
29
+ return (
30
+ <div className="w-full rounded-xl bg-white dark:bg-zinc-700/30 dark:from-inherit shadow-xl">
31
+ <div className="flex rounded-lg px-4 py-2 text-center items-center overflow-y-auto">
32
+ <button
33
+ className={`flex text-center items-center text-l ${props.showChat ? 'text-blue-500' : ''} bg-transparent px-4 py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:scale-105`}
34
+ onClick={() => handleShowChatTab()}
35
+ title='Chat'
36
+ >
37
+ <MessageCircle className="mr-1 h-5 w-5" />
38
+ Chat
39
+ </button>
40
+ <button
41
+ className={`flex text-center items-center text-l ${props.showUpload ? 'text-blue-500' : ''} bg-transparent px-4 py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:scale-105`}
42
+ onClick={() => handleShowUploadTab()}
43
+ title='Upload'
44
+ >
45
+ <Upload className="mr-1 h-5 w-5" />
46
+ Upload
47
+ </button>
48
+ <button
49
+ className={`flex text-center items-center text-l ${props.showManage ? 'text-blue-500' : ''} bg-transparent px-4 py-3 rounded-md font-bold transition duration-300 ease-in-out transform hover:scale-105`}
50
+ onClick={() => handleShowManageTab()}
51
+ title='Manage'
52
+ >
53
+ <FileCog className="mr-1 h-5 w-5" />
54
+ Manage
55
+ </button>
56
+ </div>
57
+ </div>
58
+ );
59
+ }
frontend/app/components/ui/query/query-selection.tsx ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { ChatHandler } from '@/app/components/ui/chat';
5
+ import { IconSpinner } from '@/app/components/ui/icons';
6
+
7
+ export default function QuerySelection(
8
+ props: Pick<ChatHandler, "collSelectedId" | "collSelectedName" | "handleCollIdSelect" | "handleCollNameSelect">,
9
+ ) {
10
+ const [userCollections, setuserCollections] = useState<any[]>([]);
11
+ const [isLoading, setisLoading] = useState(true); // Loading state
12
+
13
+ const handleCollectionSelect = (collectionId: string, displayName: string) => {
14
+ props.handleCollIdSelect(collectionId);
15
+ props.handleCollNameSelect(displayName);
16
+ };
17
+
18
+ const getUserCollections = async () => {
19
+ setisLoading(true); // Set loading state to true
20
+ // Fetch the public collection sets from the API
21
+ const response = await fetch('/api/user/collections', {
22
+ method: 'GET',
23
+ headers: {
24
+ 'Content-Type': 'application/json',
25
+ 'Cache-Control': 'no-cache', // Disable caching
26
+ },
27
+ });
28
+
29
+ if (!response.ok) {
30
+ console.error("Error fetching user collections:", response.statusText);
31
+ return;
32
+ }
33
+
34
+ const data = await response.json();
35
+ // Sort the collections by created date in descending order (oldest first)
36
+ const sortedUserCollections = data.userCollections.sort((a: any, b: any) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
37
+ setuserCollections(sortedUserCollections);
38
+ setisLoading(false); // Set loading state to false
39
+ }
40
+
41
+ // Retrieve the public collection sets from the database
42
+ useEffect(() => {
43
+ getUserCollections();
44
+ }, []);
45
+
46
+ // console.log('userCollections:', userCollections);
47
+
48
+ return (
49
+ <div className="w-full rounded-xl bg-white dark:bg-zinc-700/30 dark:from-inherit p-4 shadow-xl">
50
+ <div className="rounded-lg pt-5 pr-10 pl-10 flex h-[50vh] flex-col overflow-y-auto pb-4">
51
+ <h2 className="text-lg text-center font-semibold mb-4">Select Your Document Set to Chat with:</h2>
52
+ {isLoading ? (
53
+ <IconSpinner className='w-10 h-10 mx-auto my-auto animate-spin' />
54
+ ) : userCollections.length === 0 ? (<div className="mx-auto my-auto text-center text-lg text-gray-500 dark:text-gray-400">No collections found.</div>)
55
+ : userCollections.map((collection, index) => (
56
+ (<ul key={index}>
57
+ <li className="p-2 mb-2 border border-zinc-500/30 dark:border-white rounded-lg hover:bg-zinc-500/30 transition duration-300 ease-in-out transform cursor-pointer">
58
+ <button className="text-blue-500 w-full text-left" onClick={() => handleCollectionSelect(collection.collection_id, collection.display_name)}>
59
+ <div>{collection.display_name}</div>
60
+ <div className="text-sm text-gray-500">{collection.description}</div>
61
+ </button>
62
+ </li>
63
+ </ul>)
64
+ ))}
65
+ </div>
66
+ </div>
67
+ );
68
+ };
frontend/app/components/ui/query/query.interface.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ export interface QueryMenuHandler {
2
+ showUpload: boolean,
3
+ setShowUpload: (showUpload: boolean) => void,
4
+ showChat: boolean,
5
+ setShowChat: (showChat: boolean) => void,
6
+ showManage: boolean,
7
+ setShowManage: (showManage: boolean) => void,
8
+ setCollSelectedId: (collSelectedId: string) => void,
9
+ }