Spaces:
Build error
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
- .github/workflows/sync-to-hugging-face-hub.yml +1 -1
- README.md +1 -1
- backend/backend/app/api/routers/chat.py +5 -5
- backend/backend/app/api/routers/collections.py +107 -0
- backend/backend/app/api/routers/indexer.py +73 -0
- backend/backend/app/api/routers/query.py +6 -5
- backend/backend/app/api/routers/search.py +11 -7
- backend/backend/app/utils/auth.py +9 -8
- backend/backend/app/utils/contants.py +1 -1
- backend/backend/app/utils/index.py +72 -3
- backend/backend/main.py +4 -0
- backend/poetry.lock +0 -0
- backend/pyproject.toml +2 -0
- frontend/.eslintrc.json +3 -0
- frontend/app/admin/page.tsx +10 -0
- frontend/app/api/admin/collections-requests/approve/route.ts +43 -0
- frontend/app/api/admin/collections-requests/reject/route.ts +30 -0
- frontend/app/api/admin/collections-requests/route.ts +26 -0
- frontend/app/api/admin/collections/route.ts +26 -0
- frontend/app/api/admin/is-admin/route.ts +52 -0
- frontend/app/api/admin/users/demote/route.ts +28 -0
- frontend/app/api/admin/users/promote/route.ts +27 -0
- frontend/app/api/admin/users/route.ts +26 -0
- frontend/app/api/profile/route.ts +128 -7
- frontend/app/api/public/collections/route.ts +26 -0
- frontend/app/api/user/collections-requests/route.ts +133 -0
- frontend/app/api/user/collections/route.ts +183 -0
- frontend/app/components/admin-section.tsx +47 -0
- frontend/app/components/chat-section.tsx +13 -8
- frontend/app/components/header.tsx +65 -30
- frontend/app/components/profile-section.tsx +275 -0
- frontend/app/components/query-section.tsx +68 -29
- frontend/app/components/search-section.tsx +13 -8
- frontend/app/components/ui/admin/admin-collections-requests.tsx +277 -0
- frontend/app/components/ui/admin/admin-manage-collections.tsx +283 -0
- frontend/app/components/ui/admin/admin-manage-users.tsx +280 -0
- frontend/app/components/ui/admin/admin-menu.tsx +56 -0
- frontend/app/components/ui/admin/admin.interface.ts +8 -0
- frontend/app/components/ui/admin/index.ts +11 -0
- frontend/app/components/ui/autofill-prompt/autofill-prompt-dialog.tsx +30 -25
- frontend/app/components/ui/autofill-prompt/autofill-search-prompt-dialog.tsx +30 -26
- frontend/app/components/ui/chat/chat-selection.tsx +50 -36
- frontend/app/components/ui/chat/chat.interface.ts +4 -2
- frontend/app/components/ui/chat/use-copy-to-clipboard.tsx +2 -1
- frontend/app/components/ui/query/index.ts +6 -0
- frontend/app/components/ui/query/query-document-upload.tsx +318 -0
- frontend/app/components/ui/query/query-manage.tsx +575 -0
- frontend/app/components/ui/query/query-menu.tsx +59 -0
- frontend/app/components/ui/query/query-selection.tsx +68 -0
- frontend/app/components/ui/query/query.interface.ts +9 -0
@@ -21,4 +21,4 @@ jobs:
|
|
21 |
- name: Push to hub
|
22 |
env:
|
23 |
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
24 |
-
run: git push https://
|
|
|
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
|
@@ -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
|
@@ -34,7 +34,7 @@ class _Message(BaseModel):
|
|
34 |
|
35 |
class _ChatData(BaseModel):
|
36 |
messages: List[_Message]
|
37 |
-
|
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
|
76 |
-
|
77 |
-
logger.info(f"
|
78 |
# get the index for the selected document set
|
79 |
-
index = get_index(collection_name=
|
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(
|
@@ -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
|
@@ -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.")
|
@@ -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 |
-
|
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
|
42 |
-
|
43 |
-
logger.info(f"
|
44 |
# get the index for the selected document set
|
45 |
-
index = get_index(collection_name=
|
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(
|
@@ -1,7 +1,7 @@
|
|
1 |
import logging
|
2 |
import re
|
3 |
|
4 |
-
from fastapi import APIRouter, Depends, HTTPException,
|
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 |
-
|
26 |
):
|
27 |
# query = request.query_params.get("query")
|
28 |
logger = logging.getLogger("uvicorn")
|
29 |
-
logger.info(f"Document Set: {
|
30 |
# get the index for the selected document set
|
31 |
-
index = get_index(collection_name=
|
32 |
-
if query is None or
|
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"] =
|
|
|
|
|
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
|
@@ -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("
|
79 |
# print(response.data)
|
80 |
if len(response.data) == 0:
|
81 |
return False
|
82 |
else:
|
83 |
-
return
|
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
|
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 |
-
|
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 |
-
|
134 |
# Check if the user exists in the database
|
135 |
-
|
|
|
136 |
logger.info("Validated User's Auth Token successfully!")
|
137 |
-
return
|
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:
|
@@ -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 =
|
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
|
@@ -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 [{
|
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 [{
|
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
|
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)
|
@@ -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
|
The diff for this file is too large to render.
See raw diff
|
|
@@ -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
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"extends": "next/core-web-vitals"
|
3 |
+
}
|
@@ -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 |
+
}
|
@@ -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 |
+
}
|
@@ -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 |
+
}
|
@@ -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 |
+
}
|
@@ -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 |
+
}
|
@@ -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 |
+
}
|
@@ -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 |
+
}
|
@@ -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 |
+
}
|
@@ -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 |
+
}
|
@@ -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
|
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
|
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.
|
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.
|
41 |
}
|
42 |
|
43 |
-
|
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 |
+
}
|
@@ -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 |
+
}
|
@@ -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 |
+
}
|
@@ -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 |
+
}
|
@@ -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;
|
@@ -2,15 +2,16 @@
|
|
2 |
|
3 |
import { useChat } from "ai/react";
|
4 |
import { ChatInput, ChatMessages } from "@/app/components/ui/chat";
|
5 |
-
import ChatSelection from "
|
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 [
|
|
|
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 |
-
|
31 |
},
|
32 |
});
|
33 |
|
34 |
return (
|
35 |
<div className="space-y-4 max-w-5xl w-full relative">
|
36 |
-
{
|
37 |
(
|
38 |
<>
|
39 |
<ChatMessages
|
@@ -43,11 +44,13 @@ export default function ChatSection() {
|
|
43 |
stop={stop}
|
44 |
/>
|
45 |
<AutofillQuestion
|
46 |
-
|
|
|
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 |
-
|
64 |
-
|
|
|
|
|
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>
|
@@ -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-
|
224 |
|
225 |
{/* Conditionally render the user profile and logout buttons based on the user's authentication status */}
|
226 |
-
{
|
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 |
-
{
|
233 |
-
|
234 |
-
|
235 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
236 |
</div>
|
237 |
-
</
|
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 |
-
<
|
253 |
-
<div className="flex items-center
|
254 |
-
<
|
255 |
-
|
|
|
|
|
|
|
|
|
256 |
</div>
|
257 |
-
</
|
258 |
)}
|
259 |
</div>
|
260 |
</div >
|
261 |
|
262 |
{/* Mobile menu component */}
|
263 |
< MobileMenu isOpen={isMobileMenuOpen} onClose={() => setMobileMenuOpen(false)
|
264 |
-
} logoSrc={logo} items={
|
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 |
);
|
@@ -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;
|
@@ -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 [
|
|
|
|
|
|
|
|
|
13 |
const {
|
14 |
messages,
|
15 |
input,
|
@@ -19,46 +26,78 @@ export default function QuerySection() {
|
|
19 |
reload,
|
20 |
stop,
|
21 |
} = useChat({
|
22 |
-
api: process.env.
|
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 |
-
|
30 |
},
|
31 |
});
|
32 |
|
33 |
return (
|
34 |
-
<div className="space-y-4
|
35 |
-
{/*
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
input={input}
|
48 |
/>
|
49 |
-
<ChatInput
|
50 |
-
input={input}
|
51 |
-
handleSubmit={handleSubmit}
|
52 |
-
handleInputChange={handleInputChange}
|
53 |
-
isLoading={isLoading}
|
54 |
-
/> */}
|
55 |
|
56 |
-
{/*
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
}
|
@@ -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 [
|
|
|
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,
|
23 |
};
|
24 |
|
25 |
return (
|
26 |
<div className="space-y-4 max-w-5xl w-full">
|
27 |
-
{
|
28 |
<>
|
29 |
-
<h2 className="text-lg text-center font-semibold mb-4">Searching in {docSelected}</h2>
|
30 |
<SearchInput
|
31 |
-
|
|
|
32 |
query={query}
|
33 |
isLoading={isLoading}
|
34 |
results={searchResults}
|
@@ -36,12 +37,14 @@ const SearchSection: React.FC = () => {
|
|
36 |
onSearchSubmit={handleSearchSubmit}
|
37 |
/>
|
38 |
<AutofillSearchQuery
|
39 |
-
|
|
|
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 |
-
|
56 |
-
|
|
|
|
|
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>
|
@@ -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 |
+
}
|
@@ -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 |
+
}
|
@@ -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 |
+
}
|
@@ -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 |
+
}
|
@@ -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 |
+
}
|
@@ -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 |
+
};
|
@@ -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 |
-
"
|
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.
|
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.
|
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
|
86 |
-
<
|
87 |
-
|
88 |
-
<
|
89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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 |
)}
|
@@ -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 |
-
"
|
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.
|
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.
|
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
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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 |
)}
|
@@ -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, "
|
14 |
) {
|
15 |
-
const [
|
|
|
16 |
|
17 |
-
const
|
18 |
-
props.
|
|
|
19 |
};
|
20 |
|
21 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
useEffect(() => {
|
23 |
-
|
24 |
-
|
25 |
-
|
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
|
38 |
<h2 className="text-lg text-center font-semibold mb-4">Select Document Set to Chat with:</h2>
|
39 |
-
{
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
onClick={() =>
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
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 |
);
|
@@ -5,8 +5,10 @@ export interface Message {
|
|
5 |
}
|
6 |
|
7 |
export interface ChatHandler {
|
8 |
-
|
9 |
-
|
|
|
|
|
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;
|
@@ -23,7 +23,8 @@ export function useCopyToClipboard({
|
|
23 |
|
24 |
const showToastMessage = () => {
|
25 |
toast.success("Message copied to clipboard!", {
|
26 |
-
position: "top-
|
|
|
27 |
});
|
28 |
};
|
29 |
|
|
|
23 |
|
24 |
const showToastMessage = () => {
|
25 |
toast.success("Message copied to clipboard!", {
|
26 |
+
position: "top-right",
|
27 |
+
closeOnClick: true,
|
28 |
});
|
29 |
};
|
30 |
|
@@ -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 };
|
@@ -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 |
+
}
|
@@ -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 |
+
}
|
@@ -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 |
+
}
|
@@ -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 |
+
};
|
@@ -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 |
+
}
|