khronoz commited on
Commit
7d9d30d
·
unverified ·
1 Parent(s): 7091242

Autofill Prompt (#16)

Browse files

* Minor Backend Changes

Added PSSCOC docs downloader script.
Redirect / to /docs for swagger docs.
Updated poetry dependencies.
Updated torch-cuda dependency as optional, defaulting to torch-cpu.

* Updated README.md

* Added Autofill Prompt Component

Added a base component to autofill the prompt message selected by the user.

* Update Dockerfile & workflow in line with updated poetry

* Moved login-buttons into ui components

* Updated transitions for autofill dialog

* Added autofill dialog to query page

* Usability improvements to search function

* Added a few todo

* Added loading spinner to status icon

* Update Search to use interface

* Added Autofill Prompt for Search

* Updated Package Ver

* Updated pyproject.toml, README, Dockerfile & test

* Updated poetry.lock

* Updated poetry lock and pyproject.toml

* Commented out cpu torch, to install cuda version only.

* Updated python test workflow to not install torch package

* Updated README

.github/workflows/python-tests.yml CHANGED
@@ -33,7 +33,7 @@ jobs:
33
  run: |
34
  # python -m pip install --upgrade pip setuptools wheel
35
  # python -m pip install poetry
36
- poetry install --without torch-cuda
37
  - name: Lint with flake8
38
  working-directory: ./backend
39
  run: |
 
33
  run: |
34
  # python -m pip install --upgrade pip setuptools wheel
35
  # python -m pip install poetry
36
+ poetry install --with dev
37
  - name: Lint with flake8
38
  working-directory: ./backend
39
  run: |
Dockerfile CHANGED
@@ -40,8 +40,9 @@ ENV CUDA_DOCKER_ARCH=all \
40
  # Set the uvicorn env
41
  ENVIRONMENT=prod \
42
  ##########################################################
43
- # Build llama-cpp-python with cuda support
44
  # CMAKE_ARGS="-DLLAMA_CUBLAS=on"
 
45
  # Build llama-cpp-python with openblas support on CPU
46
  CMAKE_ARGS="-DLLAMA_BLAS=ON -DLLAMA_BLAS_VENDOR=OpenBLAS"
47
  ##########################################################
@@ -62,7 +63,7 @@ COPY --chown=user ./backend/pyproject.toml ./backend/poetry.lock $HOME/app/
62
  COPY --chown=user ./backend $HOME/app
63
 
64
  # Install the dependencies
65
- RUN poetry install --without dev,torch-cpu && \
66
  rm -rf /tmp/poetry_cache
67
 
68
  # Change to the package directory
 
40
  # Set the uvicorn env
41
  ENVIRONMENT=prod \
42
  ##########################################################
43
+ # # Build llama-cpp-python with cuda support
44
  # CMAKE_ARGS="-DLLAMA_CUBLAS=on"
45
+ ##########################################################
46
  # Build llama-cpp-python with openblas support on CPU
47
  CMAKE_ARGS="-DLLAMA_BLAS=ON -DLLAMA_BLAS_VENDOR=OpenBLAS"
48
  ##########################################################
 
63
  COPY --chown=user ./backend $HOME/app
64
 
65
  # Install the dependencies
66
+ RUN poetry install --with torch-cuda && \
67
  rm -rf /tmp/poetry_cache
68
 
69
  # Change to the package directory
README.md CHANGED
@@ -50,8 +50,6 @@ pinned: false
50
  Smart Retrieval is a platform for efficient and streamlined information retrieval, especially in the realm of legal and compliance documents.
51
  With the power of Open-Source Large Language Models (LLM) and Retrieval Augmented Generation (RAG), it aims to enhance user experiences at JTC by addressing key challenges such as manual search inefficiencies and rigid file naming conventions, revolutionizing the way JTC employees access and comprehend crucial documents
52
 
53
- Project files bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
54
-
55
  ## 🏁 Getting Started <a name = "getting_started"></a>
56
 
57
  These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See [deployment](#deployment) for notes on how to deploy the project on a live system.
@@ -76,6 +74,7 @@ For more information, see the [DEPLOYMENT.md](./DEPLOYMENT.md).
76
  - [Python](https://python.org/) - Backend Server Environment
77
  - [FastAPI](https://fastapi.tiangolo.com/) - Backend API Web Framework
78
  - [LlamaIndex](https://www.llamaindex.ai/) - Data Framework for LLM
 
79
 
80
  ## 📑 Contributing <a name = "contributing"></a>
81
 
 
50
  Smart Retrieval is a platform for efficient and streamlined information retrieval, especially in the realm of legal and compliance documents.
51
  With the power of Open-Source Large Language Models (LLM) and Retrieval Augmented Generation (RAG), it aims to enhance user experiences at JTC by addressing key challenges such as manual search inefficiencies and rigid file naming conventions, revolutionizing the way JTC employees access and comprehend crucial documents
52
 
 
 
53
  ## 🏁 Getting Started <a name = "getting_started"></a>
54
 
55
  These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See [deployment](#deployment) for notes on how to deploy the project on a live system.
 
74
  - [Python](https://python.org/) - Backend Server Environment
75
  - [FastAPI](https://fastapi.tiangolo.com/) - Backend API Web Framework
76
  - [LlamaIndex](https://www.llamaindex.ai/) - Data Framework for LLM
77
+ - [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama) - LlamaIndex Application Bootstrap Tool
78
 
79
  ## 📑 Contributing <a name = "contributing"></a>
80
 
backend/README.md CHANGED
@@ -2,6 +2,8 @@
2
 
3
  The backend is built using Python & [FastAPI](https://fastapi.tiangolo.com/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
4
 
 
 
5
  ## Requirements
6
 
7
  1. Python >= 3.11
@@ -21,9 +23,14 @@ The backend is built using Python & [FastAPI](https://fastapi.tiangolo.com/) boo
21
 
22
  ## Getting Started
23
 
24
- First, ensure if you want to use the cuda version of pytorch, you have the correct version `cuda > 12.1` of cuda installed. You can check this by running `nvcc --version or nvidia-smi` in your terminal. If you do not have cuda installed, you can install it from [here](https://developer.nvidia.com/cuda-downloads).
 
 
 
 
25
 
26
- Ensure you have followed the steps in the requirements section above.
 
27
 
28
  Then activate the conda environment:
29
 
@@ -33,24 +40,29 @@ conda activate SmartRetrieval
33
 
34
  Second, setup the environment:
35
 
36
- ```bash
37
- # Only run one of the following commands:
 
38
  -----------------------------------------------
39
  # Install dependencies and torch (cpu version)
 
 
40
  # Windows: Set env for llama-cpp-python with openblas support on cpu
41
  $env:CMAKE_ARGS = "-DLLAMA_BLAS=ON -DLLAMA_BLAS_VENDOR=OpenBLAS"
42
  # Linux: Set env for llama-cpp-python with openblas support on cpu
43
  CMAKE_ARGS="-DLLAMA_BLAS=ON -DLLAMA_BLAS_VENDOR=OpenBLAS"
44
  # Then:
45
- poetry install --without torch-cuda
46
  -----------------------------------------------
47
  # Install dependencies and torch (cuda version)
 
 
48
  # Windows: Set env for llama-cpp-python with cuda support on gpu
49
  $env:CMAKE_ARGS = "-DLLAMA_CUBLAS=on"
50
  # Linux: Set env for llama-cpp-python with cuda support on gpu
51
  CMAKE_ARGS="-DLLAMA_CUBLAS=on"
52
  # Then:
53
- poetry install --without torch-cpu
54
  ```
55
 
56
  ```bash
 
2
 
3
  The backend is built using Python & [FastAPI](https://fastapi.tiangolo.com/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
4
 
5
+ To get started, you must first install the required dependencies in `Requirements` section below, then follow the `Getting Started` section.
6
+
7
  ## Requirements
8
 
9
  1. Python >= 3.11
 
23
 
24
  ## Getting Started
25
 
26
+ First, ensure if you want to use the cuda version of pytorch, you have the correct version `cuda > 12.1` of cuda installed. You can check this by running `nvcc --version or nvidia-smi` in your terminal, nvcc --version should correctly chow whether you have installed cuda properly or not. If you do not have cuda installed, you can install it from [here](https://developer.nvidia.com/cuda-downloads).
27
+
28
+ - You may need to add cuda to your path, which can be found online.
29
+
30
+ Ensure you have followed the steps in the `requirements` section above.
31
 
32
+ - If on windows, make sure you are running the commands in powershell.
33
+ - Add conda to your path, which can be found [here](https://stackoverflow.com/questions/64149680/how-can-i-activate-a-conda-environment-from-powershell)
34
 
35
  Then activate the conda environment:
36
 
 
40
 
41
  Second, setup the environment:
42
 
43
+ ```powershell
44
+ # Only choose one of the options below depending on if you have CUDA enabled GPU or not:
45
+ # If running on windows, make sure you are running the commands in powershell.
46
  -----------------------------------------------
47
  # Install dependencies and torch (cpu version)
48
+ # Go to the backend directory and edit the pyproject.toml file to uncomment the `torch-cpu` poetry section
49
+ -----------------------------------------------
50
  # Windows: Set env for llama-cpp-python with openblas support on cpu
51
  $env:CMAKE_ARGS = "-DLLAMA_BLAS=ON -DLLAMA_BLAS_VENDOR=OpenBLAS"
52
  # Linux: Set env for llama-cpp-python with openblas support on cpu
53
  CMAKE_ARGS="-DLLAMA_BLAS=ON -DLLAMA_BLAS_VENDOR=OpenBLAS"
54
  # Then:
55
+ poetry install --with torch-cpu
56
  -----------------------------------------------
57
  # Install dependencies and torch (cuda version)
58
+ # Installing torch with cuda support on a system without cuda support is also possible.
59
+ -----------------------------------------------
60
  # Windows: Set env for llama-cpp-python with cuda support on gpu
61
  $env:CMAKE_ARGS = "-DLLAMA_CUBLAS=on"
62
  # Linux: Set env for llama-cpp-python with cuda support on gpu
63
  CMAKE_ARGS="-DLLAMA_CUBLAS=on"
64
  # Then:
65
+ poetry install --with torch-cuda
66
  ```
67
 
68
  ```bash
backend/backend/app/utils/contants.py CHANGED
@@ -7,7 +7,7 @@ from torch.cuda import is_available as is_cuda_available
7
 
8
  # Model Constants
9
  MAX_NEW_TOKENS = 4096
10
- CONTEXT_SIZE = MAX_NEW_TOKENS
11
  DEVICE_TYPE = "cuda" if is_cuda_available() else "cpu"
12
 
13
  # Get the current directory
@@ -18,6 +18,7 @@ DATA_DIR = str(CUR_DIR / "data") # directory containing the documents to index
18
 
19
  # LLM Model Constants
20
  LLM_MODEL_URL = "https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGUF/resolve/main/llama-2-7b-chat.Q4_K_M.gguf"
 
21
  # Model Kwargs
22
  # set to at least 1 to use GPU, adjust according to your GPU memory, but must be able to fit the model
23
  MODEL_KWARGS = {"n_gpu_layers": 100} if DEVICE_TYPE == "cuda" else {}
 
7
 
8
  # Model Constants
9
  MAX_NEW_TOKENS = 4096
10
+ CONTEXT_SIZE = 3900 # llama2 has a context window of 4096 tokens, but we set it lower to allow for some wiggle room
11
  DEVICE_TYPE = "cuda" if is_cuda_available() else "cpu"
12
 
13
  # Get the current directory
 
18
 
19
  # LLM Model Constants
20
  LLM_MODEL_URL = "https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGUF/resolve/main/llama-2-7b-chat.Q4_K_M.gguf"
21
+ LLM_TEMPERATURE = 0.1
22
  # Model Kwargs
23
  # set to at least 1 to use GPU, adjust according to your GPU memory, but must be able to fit the model
24
  MODEL_KWARGS = {"n_gpu_layers": 100} if DEVICE_TYPE == "cuda" else {}
backend/backend/app/utils/index.py CHANGED
@@ -28,6 +28,7 @@ from backend.app.utils.contants import (
28
  EMBED_MODEL_NAME,
29
  EMBED_POOLING,
30
  LLM_MODEL_URL,
 
31
  MAX_NEW_TOKENS,
32
  MODEL_KWARGS,
33
  NUM_OUTPUT,
@@ -36,12 +37,11 @@ from backend.app.utils.contants import (
36
 
37
  llm = LlamaCPP(
38
  model_url=LLM_MODEL_URL,
39
- temperature=0.1,
40
  max_new_tokens=MAX_NEW_TOKENS,
41
- # llama2 has a context window of 4096 tokens, but we set it lower to allow for some wiggle room
42
  context_window=CONTEXT_SIZE,
43
  # kwargs to pass to __call__()
44
- # generate_kwargs={},
45
  # kwargs to pass to __init__()
46
  model_kwargs=MODEL_KWARGS,
47
  # transform inputs into Llama2 format
@@ -50,14 +50,6 @@ llm = LlamaCPP(
50
  verbose=True,
51
  )
52
 
53
- # define prompt helper
54
- # set maximum input size
55
- max_input_size = 4096
56
- # set number of output tokens
57
- num_output = 256
58
- # set maximum chunk overlap
59
- max_chunk_overlap = 0.2
60
-
61
  embed_model = HuggingFaceEmbedding(
62
  model_name=EMBED_MODEL_NAME,
63
  pooling=EMBED_POOLING,
 
28
  EMBED_MODEL_NAME,
29
  EMBED_POOLING,
30
  LLM_MODEL_URL,
31
+ LLM_TEMPERATURE,
32
  MAX_NEW_TOKENS,
33
  MODEL_KWARGS,
34
  NUM_OUTPUT,
 
37
 
38
  llm = LlamaCPP(
39
  model_url=LLM_MODEL_URL,
40
+ temperature=LLM_TEMPERATURE,
41
  max_new_tokens=MAX_NEW_TOKENS,
 
42
  context_window=CONTEXT_SIZE,
43
  # kwargs to pass to __call__()
44
+ generate_kwargs={},
45
  # kwargs to pass to __init__()
46
  model_kwargs=MODEL_KWARGS,
47
  # transform inputs into Llama2 format
 
50
  verbose=True,
51
  )
52
 
 
 
 
 
 
 
 
 
53
  embed_model = HuggingFaceEmbedding(
54
  model_name=EMBED_MODEL_NAME,
55
  pooling=EMBED_POOLING,
backend/backend/get_PSSCOC_docs.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+
4
+ import requests
5
+ from bs4 import BeautifulSoup
6
+ from doc2docx import convert as convert_doc2docx
7
+
8
+ """
9
+ A web scraping script to download the Public Sector Standard Conditions of Contract (PSSCOC) documents from the BCA website.
10
+ The script will create a folder for each category and download the documents into the respective category folder.
11
+ The script will also convert doc files to docx files.
12
+ The script will also get the About PSSCOC page info and saves to a json file.
13
+ """
14
+
15
+ # Website URL
16
+ base_url = "https://www1.bca.gov.sg"
17
+ docs_endpoint = "/procurement/post-tender-stage/public-sector-standard-conditions-of-contract-psscoc"
18
+
19
+ # Source Documents Folder
20
+ source_doc_dir = "data"
21
+ # PSSCOC Page Info Folder
22
+ psscoc_page_info_dir = "About PSSCOC"
23
+
24
+
25
+ # Get the PSSCOC documents
26
+ def get_psscoc_docs():
27
+ """
28
+ Downloads the PSSCOC documents.
29
+ """
30
+ # Send a GET request to the website URL
31
+ response = requests.get(base_url + docs_endpoint, timeout=10)
32
+ response.raise_for_status() # Check for HTTP errors
33
+
34
+ # Parse the HTML content of the response
35
+ soup = BeautifulSoup(response.content, "html.parser")
36
+
37
+ # Find the table element that contains the Conditions of Contract & Downloads link
38
+ table = soup.find("table")
39
+
40
+ # Loop through each row of the table element, skipping the first row
41
+ for row in table.find_all("tr")[1:]:
42
+ # Find the first cell of the row that contains the category name
43
+ first_cell = row.find("td", attrs={"scope": "row"})
44
+ category_name = first_cell.find("strong").text.strip()
45
+ print("Category:", category_name)
46
+
47
+ # Create a folder for each category if it doesn't exist
48
+ category_folder = os.path.join(source_doc_dir, category_name)
49
+ if not os.path.exists(category_folder):
50
+ os.makedirs(category_folder)
51
+
52
+ # Find the second cell of the row that contains the href links
53
+ second_cell = row.find("ol")
54
+ # Loop through each list item in the second cell
55
+ for li in second_cell.find_all("li"):
56
+ # Find the href link
57
+ href_link = li.find("a")["href"]
58
+ # if starts with /docs, then it is a relative link
59
+ if href_link.startswith("/docs"):
60
+ href_link = base_url + href_link
61
+
62
+ # Get the filename from the href link
63
+ filename = os.path.basename(href_link).split("?")[0]
64
+ print("Downloading:", filename)
65
+ # Send a GET request to the href link
66
+ response = requests.get(href_link, timeout=10)
67
+ # Write the response content to a file
68
+ with open(os.path.join(category_folder, filename), "wb") as f:
69
+ f.write(response.content)
70
+ print("Saved to:", os.path.join(category_folder, filename))
71
+ # convert doc to docx
72
+ if filename.endswith(".doc"):
73
+ print("Converting to docx...")
74
+ convert_doc2docx(os.path.join(category_folder, filename))
75
+ print(
76
+ "Converted to:",
77
+ os.path.join(category_folder, filename + "x"),
78
+ )
79
+ # remove the original doc file
80
+ os.remove(os.path.join(category_folder, filename))
81
+ # line break
82
+ print("-" * 100)
83
+
84
+
85
+ def get_psscoc_page_info():
86
+ """
87
+ Get the About PSSCOC page info and saves to a json file.
88
+ """
89
+ print("Getting PSSCOC Page Info...")
90
+ # Send a GET request to the website URL
91
+ response = requests.get(base_url + docs_endpoint, timeout=10)
92
+ response.raise_for_status() # Check for HTTP errors
93
+
94
+ # Parse the HTML content of the response
95
+ soup = BeautifulSoup(response.content, "html.parser")
96
+
97
+ # Extract the necessary HTML elements
98
+ mid_body = soup.find("div", attrs={"class": "mid"})
99
+
100
+ cleaned_results = {}
101
+
102
+ # Extract title from the mid_body
103
+ title = mid_body.find("div", attrs={"class": "title"}).text.strip()
104
+ cleaned_results["Title"] = title
105
+
106
+ # Extract the flow content from the mid_body
107
+ flow_content = mid_body.find("ul", attrs={"class": "rsmFlow rsmLevel rsmOneLevel"})
108
+ flow = [li.text.strip() for li in flow_content.find_all("li")]
109
+ cleaned_results["Stage of PSSCOC"] = " > ".join(flow[1:])
110
+
111
+ # Extract sfContentBlock from the mid_body
112
+ sf_content_block = mid_body.find("div", attrs={"class": "sfContentBlock"})
113
+
114
+ # Extract all the paragraphs from the sfContentBlock but not nested paragraphs
115
+ paragraphs = sf_content_block.find_all("p", recursive=False)
116
+ paragraphs_text = [p.text.strip() for p in paragraphs]
117
+ cleaned_results["About"] = paragraphs_text[0]
118
+ more_about = {}
119
+ more_about[paragraphs_text[1]] = (
120
+ paragraphs_text[2]
121
+ .replace("\n", " ")
122
+ .replace("\r", " ")
123
+ .replace("\u00a0", " ")
124
+ .strip()
125
+ )
126
+
127
+ # Extract ul from the sfContentBlock
128
+ ul_content = sf_content_block.find("ul")
129
+ ul_li = [li.text.strip() for li in ul_content.find_all("li")]
130
+ more_about[paragraphs_text[3]] = ul_li # title for 3rd paragraph
131
+
132
+ cleaned_results["More About"] = more_about
133
+
134
+ # Extract the table from the sfContentBlock
135
+ table = sf_content_block.find("table")
136
+ table_rows = table.find_all("tr")
137
+ header_row = [td.text.strip() for td in table_rows[0].find_all("td")]
138
+ header_row = ["Category Name", "Category File Names"]
139
+ table_data_list = []
140
+ for row in table_rows[1:]:
141
+ row_data = [td.text.strip() for td in row.find_all("td")]
142
+ row_data[-1] = row_data[-1].split("\n")
143
+ # remove empty string
144
+ row_data[-1] = [x.strip() for x in row_data[-1] if x]
145
+ table_data = dict(zip(header_row, row_data))
146
+ table_data_list.append(table_data)
147
+ cleaned_results["Categories of PSSCOC"] = table_data_list
148
+
149
+ # Extract all the divs from the sfContentBlock but not nested divs
150
+ # divs = sf_content_block.find_all("div", recursive=False)
151
+
152
+ # Save the json results content to a file
153
+ page_info_folder = os.path.join(source_doc_dir, psscoc_page_info_dir)
154
+ file_name = "About PSSCOC.json"
155
+ # Create a folder for page info if it doesn't exist
156
+ if not os.path.exists(page_info_folder):
157
+ os.makedirs(page_info_folder)
158
+ # Save the results content to a file
159
+ with open(os.path.join(page_info_folder, file_name), "wb") as f:
160
+ f.write(json.dumps(cleaned_results, indent=4).encode("utf-8"))
161
+ print(
162
+ "Saved to:",
163
+ os.path.join(page_info_folder, file_name),
164
+ )
165
+
166
+
167
+ # Main function
168
+ def main():
169
+ """
170
+ Main function.
171
+ """
172
+ try:
173
+ # Get the PSSCOC documents
174
+ get_psscoc_docs()
175
+ # Get the About PSSCOC page info
176
+ get_psscoc_page_info()
177
+ except requests.exceptions.RequestException as e:
178
+ print("Error: Failed to make a request to the website.")
179
+ print(e)
180
+ except Exception as e:
181
+ print("An unexpected error occurred:")
182
+ print(e)
183
+
184
+
185
+ if __name__ == "__main__":
186
+ main()
backend/backend/main.py CHANGED
@@ -4,6 +4,7 @@ import os
4
  from dotenv import load_dotenv
5
  from fastapi import FastAPI
6
  from fastapi.middleware.cors import CORSMiddleware
 
7
  from torch.cuda import is_available as is_cuda_available
8
 
9
  from backend.app.api.routers.chat import chat_router
@@ -55,3 +56,9 @@ app.include_router(healthcheck_router, prefix="/api/healthcheck")
55
 
56
  # Try to create the index first on startup
57
  create_index()
 
 
 
 
 
 
 
4
  from dotenv import load_dotenv
5
  from fastapi import FastAPI
6
  from fastapi.middleware.cors import CORSMiddleware
7
+ from fastapi.responses import RedirectResponse
8
  from torch.cuda import is_available as is_cuda_available
9
 
10
  from backend.app.api.routers.chat import chat_router
 
56
 
57
  # Try to create the index first on startup
58
  create_index()
59
+
60
+
61
+ # Redirect to the /docs endpoint
62
+ @app.get("/")
63
+ async def docs_redirect():
64
+ return RedirectResponse(url="/docs")
backend/poetry.lock CHANGED
The diff for this file is too large to render. See raw diff
 
backend/pyproject.toml CHANGED
@@ -10,26 +10,34 @@ packages = [{ include = "backend" }]
10
  python = "^3.11,<3.12"
11
  fastapi = "^0.104.1"
12
  uvicorn = { extras = ["standard"], version = "^0.23.2" }
13
- llama-index = "^0.9.4"
14
- pypdf = "^3.17.0"
15
  python-dotenv = "^1.0.0"
16
- llama-cpp-python = "^0.2.18"
17
- transformers = "^4.35.2"
18
  docx2txt = "^0.8"
 
19
 
20
- # Dev Dependencies here
 
21
  [tool.poetry.group.dev.dependencies]
 
22
  flake8 = "^7.0.0"
23
- pytest = "^7.4.4"
24
 
25
  # For CPU torch version: Windows and Linux
26
- [tool.poetry.group.torch-cpu.dependencies]
27
- torch = [
28
- { url = "https://download.pytorch.org/whl/cpu/torch-2.1.1%2Bcpu-cp311-cp311-win_amd64.whl", markers = "sys_platform == 'win32'" },
29
- { url = "https://download.pytorch.org/whl/cpu/torch-2.1.1%2Bcpu-cp311-cp311-linux_x86_64.whl", markers = "sys_platform == 'linux'" },
30
- ]
 
 
 
31
 
32
- ## For Cuda torch version: Windows and Linux
 
 
33
  [tool.poetry.group.torch-cuda.dependencies]
34
  torch = [
35
  { url = "https://download.pytorch.org/whl/cu121/torch-2.1.1%2Bcu121-cp311-cp311-win_amd64.whl", markers = "sys_platform == 'win32'" },
 
10
  python = "^3.11,<3.12"
11
  fastapi = "^0.104.1"
12
  uvicorn = { extras = ["standard"], version = "^0.23.2" }
13
+ llama-index = "^0.9.48"
14
+ pypdf = "^3.17.4"
15
  python-dotenv = "^1.0.0"
16
+ llama-cpp-python = "^0.2.52"
17
+ transformers = "^4.38.1"
18
  docx2txt = "^0.8"
19
+ doc2docx = "^0.2.4"
20
 
21
+ [tool.poetry.group.dev]
22
+ optional = true
23
  [tool.poetry.group.dev.dependencies]
24
+ # Dev Dependencies here
25
  flake8 = "^7.0.0"
26
+ pytest = "^8.0.2"
27
 
28
  # For CPU torch version: Windows and Linux
29
+ # NOTE: To uncomment out the following lines, should you need to use the CPU version of torch
30
+ # [tool.poetry.group.torch-cpu]
31
+ # optional = true
32
+ # [tool.poetry.group.torch-cpu.dependencies]
33
+ # torch = [
34
+ # { url = "https://download.pytorch.org/whl/cpu/torch-2.1.1%2Bcpu-cp311-cp311-win_amd64.whl", markers = "sys_platform == 'win32'" },
35
+ # { url = "https://download.pytorch.org/whl/cpu/torch-2.1.1%2Bcpu-cp311-cp311-linux_x86_64.whl", markers = "sys_platform == 'linux'" },
36
+ # ]
37
 
38
+ # For Cuda torch version: Windows and Linux
39
+ [tool.poetry.group.torch-cuda]
40
+ optional = true
41
  [tool.poetry.group.torch-cuda.dependencies]
42
  torch = [
43
  { url = "https://download.pytorch.org/whl/cu121/torch-2.1.1%2Bcu121-cp311-cp311-win_amd64.whl", markers = "sys_platform == 'win32'" },
frontend/app/components/chat-section.tsx CHANGED
@@ -2,6 +2,7 @@
2
 
3
  import { useChat } from "ai/react";
4
  import { ChatInput, ChatMessages } from "@/app/components/ui/chat";
 
5
 
6
  export default function ChatSection() {
7
  const {
@@ -15,13 +16,20 @@ export default function ChatSection() {
15
  } = useChat({ api: process.env.NEXT_PUBLIC_CHAT_API });
16
 
17
  return (
18
- <div className="space-y-4 max-w-5xl w-full">
19
  <ChatMessages
20
  messages={messages}
21
  isLoading={isLoading}
22
  reload={reload}
23
  stop={stop}
24
  />
 
 
 
 
 
 
 
25
  <ChatInput
26
  input={input}
27
  handleSubmit={handleSubmit}
 
2
 
3
  import { useChat } from "ai/react";
4
  import { ChatInput, ChatMessages } from "@/app/components/ui/chat";
5
+ import AutofillQuestion from "@/app/components/ui/autofill-prompt/autofill-prompt-dialog";
6
 
7
  export default function ChatSection() {
8
  const {
 
16
  } = useChat({ api: process.env.NEXT_PUBLIC_CHAT_API });
17
 
18
  return (
19
+ <div className="space-y-4 max-w-5xl w-full relative">
20
  <ChatMessages
21
  messages={messages}
22
  isLoading={isLoading}
23
  reload={reload}
24
  stop={stop}
25
  />
26
+ <AutofillQuestion
27
+ messages={messages}
28
+ isLoading={isLoading}
29
+ handleSubmit={handleSubmit}
30
+ handleInputChange={handleInputChange}
31
+ input={input}
32
+ />
33
  <ChatInput
34
  input={input}
35
  handleSubmit={handleSubmit}
frontend/app/components/header.tsx CHANGED
@@ -9,6 +9,7 @@ import useSWR from 'swr';
9
  import logo from '@/public/smart-retrieval-logo.webp';
10
  import { HeaderNavLink } from '@/app/components/ui/navlink';
11
  import { MobileMenu } from '@/app/components/ui/mobilemenu';
 
12
 
13
  const MobileMenuItems = [
14
  {
@@ -44,7 +45,7 @@ export default function Header() {
44
  const { theme, setTheme } = useTheme();
45
  // Use SWR for API status fetching
46
  const healthcheck_api = process.env.NEXT_PUBLIC_HEALTHCHECK_API;
47
- const { data, error: apiError } = useSWR(healthcheck_api, async (url) => {
48
  try {
49
  // Fetch the data
50
  const response = await fetch(url, {
@@ -170,15 +171,18 @@ export default function Header() {
170
  <span className='flex items-center mr-1'>API:</span>
171
  <HeaderNavLink href='/status'>
172
  <div className="flex items-center mr-2 text-xl transition duration-300 ease-in-out transform hover:scale-125">
173
- {apiError ? (
174
- <span role="img" aria-label="red circle">
175
- 🔴
176
- </span>
177
- ) : (
178
- <span role="img" aria-label="green circle">
179
- 🟢
180
- </span>
181
- )}
 
 
 
182
  </div>
183
  </HeaderNavLink>
184
  <span className="lg:text-lg font-nunito">|</span>
@@ -198,10 +202,11 @@ export default function Header() {
198
  )}
199
  </button>
200
  </div>
201
- </div>
202
  {/* Mobile menu component */}
203
- <MobileMenu isOpen={isMobileMenuOpen} onClose={() => setMobileMenuOpen(false)} logoSrc={logo} items={MobileMenuItems} />
204
- </nav>
205
- </div>
 
206
  );
207
  }
 
9
  import logo from '@/public/smart-retrieval-logo.webp';
10
  import { HeaderNavLink } from '@/app/components/ui/navlink';
11
  import { MobileMenu } from '@/app/components/ui/mobilemenu';
12
+ import { IconSpinner } from '@/app/components/ui/icons'
13
 
14
  const MobileMenuItems = [
15
  {
 
45
  const { theme, setTheme } = useTheme();
46
  // Use SWR for API status fetching
47
  const healthcheck_api = process.env.NEXT_PUBLIC_HEALTHCHECK_API;
48
+ const { data, error: apiError, isLoading } = useSWR(healthcheck_api, async (url) => {
49
  try {
50
  // Fetch the data
51
  const response = await fetch(url, {
 
171
  <span className='flex items-center mr-1'>API:</span>
172
  <HeaderNavLink href='/status'>
173
  <div className="flex items-center mr-2 text-xl transition duration-300 ease-in-out transform hover:scale-125">
174
+ {isLoading ? (
175
+ <IconSpinner className="mr-2 animate-spin" />
176
+ ) :
177
+ apiError ? (
178
+ <span role="img" aria-label="red circle">
179
+ 🔴
180
+ </span>
181
+ ) : (
182
+ <span role="img" aria-label="green circle">
183
+ 🟢
184
+ </span>
185
+ )}
186
  </div>
187
  </HeaderNavLink>
188
  <span className="lg:text-lg font-nunito">|</span>
 
202
  )}
203
  </button>
204
  </div>
205
+ </div >
206
  {/* Mobile menu component */}
207
+ < MobileMenu isOpen={isMobileMenuOpen} onClose={() => setMobileMenuOpen(false)
208
+ } logoSrc={logo} items={MobileMenuItems} />
209
+ </nav >
210
+ </div >
211
  );
212
  }
frontend/app/components/query-section.tsx CHANGED
@@ -2,6 +2,7 @@
2
 
3
  import { useChat } from "ai/react";
4
  import { ChatInput, ChatMessages } from "@/app/components/ui/chat";
 
5
 
6
  export default function QuerySection() {
7
  const {
@@ -22,6 +23,13 @@ export default function QuerySection() {
22
  reload={reload}
23
  stop={stop}
24
  />
 
 
 
 
 
 
 
25
  <ChatInput
26
  input={input}
27
  handleSubmit={handleSubmit}
 
2
 
3
  import { useChat } from "ai/react";
4
  import { ChatInput, ChatMessages } from "@/app/components/ui/chat";
5
+ import AutofillQuestion from "@/app/components/ui/autofill-prompt/autofill-prompt-dialog";
6
 
7
  export default function QuerySection() {
8
  const {
 
23
  reload={reload}
24
  stop={stop}
25
  />
26
+ <AutofillQuestion
27
+ messages={messages}
28
+ isLoading={isLoading}
29
+ handleSubmit={handleSubmit}
30
+ handleInputChange={handleInputChange}
31
+ input={input}
32
+ />
33
  <ChatInput
34
  input={input}
35
  handleSubmit={handleSubmit}
frontend/app/components/search-section.tsx CHANGED
@@ -5,17 +5,22 @@ import { useState, ChangeEvent, FormEvent } from "react";
5
  import useSearch from "@/app/components/ui/search/useSearch";
6
  import SearchResults from "@/app/components/ui/search/search-results";
7
  import SearchInput from "@/app/components/ui/search/search-input";
 
8
 
9
  const SearchSection: React.FC = () => {
10
  const [query, setQuery] = useState("");
11
  const { searchResults, isLoading, handleSearch } = useSearch();
 
 
12
 
13
  const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
14
  setQuery(e.target.value);
 
15
  };
16
 
17
  const handleSearchSubmit = (e: FormEvent) => {
18
  e.preventDefault();
 
19
  handleSearch(query);
20
  };
21
 
@@ -24,10 +29,18 @@ const SearchSection: React.FC = () => {
24
  <SearchInput
25
  query={query}
26
  isLoading={isLoading}
 
 
 
 
 
 
 
 
27
  onInputChange={handleInputChange}
28
  onSearchSubmit={handleSearchSubmit}
29
  />
30
- <SearchResults results={searchResults} isLoading={isLoading} />
31
  </div>
32
  );
33
  };
 
5
  import useSearch from "@/app/components/ui/search/useSearch";
6
  import SearchResults from "@/app/components/ui/search/search-results";
7
  import SearchInput from "@/app/components/ui/search/search-input";
8
+ import AutofillSearchQuery from "@/app/components/ui/autofill-prompt/autofill-search-prompt-dialog";
9
 
10
  const SearchSection: React.FC = () => {
11
  const [query, setQuery] = useState("");
12
  const { searchResults, isLoading, handleSearch } = useSearch();
13
+ const [searchButtonPressed, setSearchButtonPressed] = useState(false);
14
+
15
 
16
  const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
17
  setQuery(e.target.value);
18
+ setSearchButtonPressed(false);
19
  };
20
 
21
  const handleSearchSubmit = (e: FormEvent) => {
22
  e.preventDefault();
23
+ setSearchButtonPressed(true);
24
  handleSearch(query);
25
  };
26
 
 
29
  <SearchInput
30
  query={query}
31
  isLoading={isLoading}
32
+ results={searchResults}
33
+ onInputChange={handleInputChange}
34
+ onSearchSubmit={handleSearchSubmit}
35
+ />
36
+ <AutofillSearchQuery
37
+ query={query}
38
+ isLoading={isLoading}
39
+ results={searchResults}
40
  onInputChange={handleInputChange}
41
  onSearchSubmit={handleSearchSubmit}
42
  />
43
+ <SearchResults query={query} results={searchResults} isLoading={isLoading} searchButtonPressed={searchButtonPressed} />
44
  </div>
45
  );
46
  };
frontend/app/components/ui/autofill-prompt/autofill-prompt-dialog.tsx ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react";
2
+ import { QuestionsBankProp, questionsBank } 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
+ "messages" | "isLoading" | "handleSubmit" | "handleInputChange" | "input"
9
+ >,
10
+ ) {
11
+ // Keep track of whether to show the overlay
12
+ const [showOverlay, setShowOverlay] = useState(true);
13
+ // Randomly select a subset of questions
14
+ const [randomQuestions, setRandomQuestions] = useState<QuestionsBankProp[]>([]);
15
+ // Keep track of the current question index
16
+ const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
17
+
18
+ // Shuffle the array using Fisher-Yates algorithm
19
+ function shuffleArray(array: any[]) {
20
+ for (let i = array.length - 1; i > 0; i--) {
21
+ const j = Math.floor(Math.random() * (i + 1));
22
+ [array[i], array[j]] = [array[j], array[i]];
23
+ }
24
+ return array;
25
+ }
26
+
27
+ // TODO: To load the questionsbank from a database in the future
28
+
29
+ // Randomly select a subset of 3-4 questions
30
+ useEffect(() => {
31
+ // Shuffle the questionsBank array
32
+ const shuffledQuestions = shuffleArray(questionsBank);
33
+ // Get a random subset of 3-4 questions
34
+ const subsetSize = Math.floor(Math.random() * 2) + 3; // Randomly choose between 3 and 4
35
+ const selectedQuestions = shuffledQuestions.slice(0, subsetSize);
36
+ // Do a short delay before setting the state to show the animation
37
+ setTimeout(() => {
38
+ setRandomQuestions(selectedQuestions);
39
+ }, 300);
40
+ }, []);
41
+
42
+
43
+ // Hide overlay when there are messages
44
+ useEffect(() => {
45
+ if (props.messages.length > 0) {
46
+ setShowOverlay(false);
47
+ }
48
+ else {
49
+ setShowOverlay(true);
50
+ }
51
+ }, [props.messages, props.input]);
52
+
53
+ // Automatically advance to the next question after a delay
54
+ useEffect(() => {
55
+ const timer = setInterval(() => {
56
+ if (currentQuestionIndex < randomQuestions.length - 1) {
57
+ setCurrentQuestionIndex((prevIndex) => prevIndex + 1);
58
+ }
59
+ else {
60
+ clearInterval(timer); // Stop the timer when all questions have been displayed
61
+ }
62
+ }, 100); // Adjust the delay time as needed (e.g., 5000 milliseconds = 5 seconds)
63
+
64
+ return () => clearInterval(timer); // Cleanup the timer on component unmount
65
+ }, [currentQuestionIndex, randomQuestions]);
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
+ return (
73
+ <>
74
+ {showOverlay && (
75
+ <div className="fixed inset-0 flex items-center justify-center">
76
+ <div className="rounded-lg pt-5 pr-10 pl-10 flex h-[50vh] flex-col divide-y overflow-y-auto pb-4">
77
+ <h2 className="text-lg text-center font-semibold mb-4">How can I help you today?</h2>
78
+ {randomQuestions.map((question, index) => (
79
+ <ul>
80
+ <li key={index} className={`p-2 mb-2 border border-zinc-500/30 dark:border-white rounded-lg hover:bg-zinc-500/30 transition duration-300 ease-in-out transform cursor-pointer ${index <= currentQuestionIndex ? 'opacity-100 duration-500' : 'opacity-0'}`}>
81
+ <button
82
+ className="text-blue-500 w-full text-left"
83
+ onClick={() => handleAutofillQuestionClick(question.title)}
84
+ >
85
+ {question.title}
86
+ </button>
87
+ </li>
88
+ </ul>
89
+ ))}
90
+ </div>
91
+ </div>
92
+ )}
93
+ </>
94
+ );
95
+ }
frontend/app/components/ui/autofill-prompt/autofill-prompt.interface.tsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface QuestionsBankProp {
2
+ title: string;
3
+ }
4
+
5
+ export const questionsBank: QuestionsBankProp[] = [
6
+ { title: "Under PSSCOC, what are the differences between the role of the SO Rep and SO?" },
7
+ { title: "Who has the authority to appoint SO Rep assistants and what can the SO Rep assistants be authorized to do?" },
8
+ { title: "What are the general obligations of the contractor and consultant?" },
9
+ { title: "In which situations can the client claim for liquidated damages?" },
10
+ { title: "What are the requirements for the contractor to claim for extension of time?" },
11
+ { title: "What are the requirements for the contractor to claim for loss and expense?" },
12
+ { title: "Under PSSCOC, briefly describe the proper payment claim process prescribed." },
13
+ // Add more common questions as needed
14
+ ];
frontend/app/components/ui/autofill-prompt/autofill-search-prompt-dialog.tsx ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react";
2
+ import { QuestionsBankProp, questionsBank } 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
+ "query" | "isLoading" | "onSearchSubmit" | "onInputChange" | "results" | "searchButtonPressed"
9
+ >,
10
+ ) {
11
+ // Keep track of whether to show the overlay
12
+ const [showOverlay, setShowOverlay] = useState(true);
13
+ // Randomly select a subset of questions
14
+ const [randomQuestions, setRandomQuestions] = useState<QuestionsBankProp[]>([]);
15
+ // Keep track of the current question index
16
+ const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
17
+
18
+ // Shuffle the array using Fisher-Yates algorithm
19
+ function shuffleArray(array: any[]) {
20
+ for (let i = array.length - 1; i > 0; i--) {
21
+ const j = Math.floor(Math.random() * (i + 1));
22
+ [array[i], array[j]] = [array[j], array[i]];
23
+ }
24
+ return array;
25
+ }
26
+
27
+ // TODO: To load the questionsbank from a database in the future
28
+
29
+ // Randomly select a subset of 3-4 questions
30
+ useEffect(() => {
31
+ // Shuffle the questionsBank array
32
+ const shuffledQuestions = shuffleArray(questionsBank);
33
+ // Get a random subset of 3-4 questions
34
+ const subsetSize = Math.floor(Math.random() * 2) + 3; // Randomly choose between 3 and 4
35
+ const selectedQuestions = shuffledQuestions.slice(0, subsetSize);
36
+ // Do a short delay before setting the state to show the animation
37
+ setTimeout(() => {
38
+ setRandomQuestions(selectedQuestions);
39
+ }, 300);
40
+ }, []);
41
+
42
+
43
+ // Hide overlay when there are query
44
+ useEffect(() => {
45
+ if (props.query.length > 0) {
46
+ setShowOverlay(false);
47
+ }
48
+ else {
49
+ setShowOverlay(true);
50
+ }
51
+ }, [props.results, props.query]);
52
+
53
+ // Automatically advance to the next question after a delay
54
+ useEffect(() => {
55
+ const timer = setInterval(() => {
56
+ if (currentQuestionIndex < randomQuestions.length - 1) {
57
+ setCurrentQuestionIndex((prevIndex) => prevIndex + 1);
58
+ }
59
+ else {
60
+ clearInterval(timer); // Stop the timer when all questions have been displayed
61
+ }
62
+ }, 100); // Adjust the delay time as needed (e.g., 5000 milliseconds = 5 seconds)
63
+
64
+ return () => clearInterval(timer); // Cleanup the timer on component unmount
65
+ }, [currentQuestionIndex, randomQuestions]);
66
+
67
+ // Handle autofill questions click
68
+ const handleAutofillQuestionClick = (questionInput: string) => {
69
+ if (props.onInputChange) {
70
+ props.onInputChange({ target: { name: "message", value: questionInput } } as React.ChangeEvent<HTMLInputElement>);
71
+ }
72
+ };
73
+
74
+ return (
75
+ <>
76
+ {showOverlay && (
77
+ <div className="relative mx-auto">
78
+ <div className="rounded-lg pt-5 pr-10 pl-10 flex flex-col divide-y overflow-y-auto pb-4 bg-white dark:bg-zinc-700/30 shadow-xl">
79
+ <h2 className="text-lg text-center font-semibold mb-4">How can I help you today?</h2>
80
+ {randomQuestions.map((question, index) => (
81
+ <ul>
82
+ <li key={index} className={`p-2 mb-2 border border-zinc-500/30 dark:border-white rounded-lg hover:bg-zinc-500/30 transition duration-300 ease-in-out transform cursor-pointer ${index <= currentQuestionIndex ? 'opacity-100 duration-500' : 'opacity-0'}`}>
83
+ <button
84
+ className="text-blue-500 w-full text-left"
85
+ onClick={() => handleAutofillQuestionClick(question.title)}
86
+ >
87
+ {question.title}
88
+ </button>
89
+ </li>
90
+ </ul>
91
+ ))}
92
+ </div>
93
+ </div>
94
+ )}
95
+ </>
96
+ );
97
+ }
frontend/app/components/ui/chat/chat-input.tsx CHANGED
@@ -1,6 +1,6 @@
1
- import { Button } from "../button";
2
- import { Input } from "../input";
3
- import { ChatHandler } from "./chat.interface";
4
  import { Send } from "lucide-react";
5
 
6
  export default function ChatInput(
@@ -9,6 +9,7 @@ export default function ChatInput(
9
  "isLoading" | "handleSubmit" | "handleInputChange" | "input"
10
  >,
11
  ) {
 
12
  return (
13
  <form
14
  onSubmit={props.handleSubmit}
@@ -18,7 +19,7 @@ export default function ChatInput(
18
  autoFocus
19
  name="message"
20
  placeholder="Type a Message"
21
- className="flex-1 bg-white dark:bg-zinc-500/30"
22
  value={props.input}
23
  onChange={props.handleInputChange}
24
  />
@@ -27,7 +28,7 @@ export default function ChatInput(
27
  <Send className="h-5 w-5" />
28
  </Button>
29
  <Button type="submit" disabled={props.isLoading} className="md:hidden"> {/* Hide on larger screens */}
30
- <Send className="h-5 w-5"/>
31
  </Button>
32
  </form>
33
  );
 
1
+ import { Button } from "@/app/components/ui/button";
2
+ import { Input } from "@/app/components/ui/input";
3
+ import { ChatHandler } from "@/app/components/ui/chat/chat.interface";
4
  import { Send } from "lucide-react";
5
 
6
  export default function ChatInput(
 
9
  "isLoading" | "handleSubmit" | "handleInputChange" | "input"
10
  >,
11
  ) {
12
+
13
  return (
14
  <form
15
  onSubmit={props.handleSubmit}
 
19
  autoFocus
20
  name="message"
21
  placeholder="Type a Message"
22
+ className="flex-1 bg-white dark:bg-zinc-500/30 z-10"
23
  value={props.input}
24
  onChange={props.handleInputChange}
25
  />
 
28
  <Send className="h-5 w-5" />
29
  </Button>
30
  <Button type="submit" disabled={props.isLoading} className="md:hidden"> {/* Hide on larger screens */}
31
+ <Send className="h-5 w-5" />
32
  </Button>
33
  </form>
34
  );
frontend/app/components/{login-buttons.tsx → ui/login-buttons.tsx} RENAMED
@@ -1,8 +1,7 @@
1
  'use client'
2
 
3
- import { useState, useEffect } from 'react'
4
  import { signIn } from 'next-auth/react'
5
-
6
  import { cn } from '@/app/components/ui/lib/utils'
7
  import { Button, type ButtonProps } from '@/app/components/ui/button'
8
  import { IconGoogle, IconSGid, IconSpinner } from '@/app/components/ui/icons'
@@ -20,20 +19,16 @@ function GoogleLoginButton({
20
  }: LoginButtonProps) {
21
  const [isLoading, setIsLoading] = useState(false);
22
 
23
- useEffect(() => {
24
- setIsLoading(false);
25
- }, []);
26
-
27
  return (
28
  <Button
29
  variant="outline"
30
  onClick={() => {
31
  setIsLoading(true);
32
- signIn('google', { callbackUrl: `/chat` });
33
  }}
34
- disabled={isLoading}
35
  className={cn(className)}
36
  {...props}
 
37
  >
38
  {isLoading ? (
39
  <IconSpinner className="mr-2 animate-spin" />
@@ -58,11 +53,11 @@ function SGIDLoginButton({
58
  variant="outline"
59
  onClick={() => {
60
  setIsLoading(true);
61
- signIn('sgid', { callbackUrl: `/chat` });
62
  }}
63
- disabled={isLoading}
64
  className={cn(className)}
65
  {...props}
 
66
  >
67
  {isLoading ? (
68
  <IconSpinner className="mr-2 animate-spin" />
 
1
  'use client'
2
 
3
+ import { useState } from 'react'
4
  import { signIn } from 'next-auth/react'
 
5
  import { cn } from '@/app/components/ui/lib/utils'
6
  import { Button, type ButtonProps } from '@/app/components/ui/button'
7
  import { IconGoogle, IconSGid, IconSpinner } from '@/app/components/ui/icons'
 
19
  }: LoginButtonProps) {
20
  const [isLoading, setIsLoading] = useState(false);
21
 
 
 
 
 
22
  return (
23
  <Button
24
  variant="outline"
25
  onClick={() => {
26
  setIsLoading(true);
27
+ signIn('google');
28
  }}
 
29
  className={cn(className)}
30
  {...props}
31
+ disabled={isLoading}
32
  >
33
  {isLoading ? (
34
  <IconSpinner className="mr-2 animate-spin" />
 
53
  variant="outline"
54
  onClick={() => {
55
  setIsLoading(true);
56
+ signIn('sgid');
57
  }}
 
58
  className={cn(className)}
59
  {...props}
60
+ disabled={isLoading}
61
  >
62
  {isLoading ? (
63
  <IconSpinner className="mr-2 animate-spin" />
frontend/app/components/ui/main-container.tsx CHANGED
@@ -9,7 +9,7 @@ interface ContainerProps {
9
 
10
  const Main: React.FC<ContainerProps> = ({ children }) => {
11
  return (
12
- <main className={cn("flex min-h-screen flex-col items-center gap-10 background-gradient dark:background-gradient-dark pt-10 px-4")}>
13
  {children}
14
  </main>
15
  );
 
9
 
10
  const Main: React.FC<ContainerProps> = ({ children }) => {
11
  return (
12
+ <main className={cn("flex min-h-screen flex-col items-center gap-10 background-gradient dark:background-gradient-dark pt-10 px-4 transition duration-300 ease-in-out transform")}>
13
  {children}
14
  </main>
15
  );
frontend/app/components/ui/search/search-input.tsx CHANGED
@@ -1,18 +1,11 @@
1
  // SearchInput.tsx
2
 
3
- import { ChangeEvent, FormEvent } from "react";
4
  import { Button } from "@/app/components/ui/button";
5
  import { Input } from "@/app/components/ui/input";
6
  import { Search } from "lucide-react";
 
7
 
8
- interface SearchInputProps {
9
- query: string;
10
- isLoading: boolean;
11
- onInputChange: (e: ChangeEvent<HTMLInputElement>) => void;
12
- onSearchSubmit: (e: FormEvent) => void;
13
- }
14
-
15
- const SearchInput: React.FC<SearchInputProps> = ({
16
  query,
17
  isLoading,
18
  onInputChange,
 
1
  // SearchInput.tsx
2
 
 
3
  import { Button } from "@/app/components/ui/button";
4
  import { Input } from "@/app/components/ui/input";
5
  import { Search } from "lucide-react";
6
+ import { SearchHandler } from "@/app/components/ui/search/search.interface";
7
 
8
+ const SearchInput: React.FC<SearchHandler> = ({
 
 
 
 
 
 
 
9
  query,
10
  isLoading,
11
  onInputChange,
frontend/app/components/ui/search/search-results.tsx CHANGED
@@ -1,24 +1,71 @@
1
- import { SearchResult } from "@/app/components/ui/search/search-types"
2
  import { IconSpinner } from "@/app/components/ui/icons";
3
- import { Fragment, useState } from "react";
4
  import { ArrowDownFromLine, ArrowUpFromLine, Copy } from "lucide-react";
5
  import { ToastContainer, toast } from 'react-toastify';
6
  import 'react-toastify/dist/ReactToastify.css';
 
7
 
8
- interface SearchResultsProps {
9
- results: SearchResult[];
10
- isLoading: boolean;
11
- }
12
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
- const SearchResults: React.FC<SearchResultsProps> = ({ results, isLoading }) => {
15
- const sortedResults = results.slice().sort((a, b) => b.similarity_score - a.similarity_score);
16
- const [expandedResult, setExpandedResult] = useState<number | null>(null);
17
 
 
18
  const handleToggleExpand = (resultId: number) => {
19
  setExpandedResult((prevId) => (prevId === resultId ? null : resultId));
20
  };
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  const showToastMessage = () => {
23
  toast.success("Text copied to clipboard!", {
24
  position: "top-center",
@@ -35,64 +82,55 @@ const SearchResults: React.FC<SearchResultsProps> = ({ results, isLoading }) =>
35
  return (
36
  <div className="flex w-full items-center justify-between rounded-xl bg-white dark:bg-zinc-700/30 p-4 shadow-xl">
37
  <ToastContainer />
38
- {isLoading ? (
39
- <div className="flex items-center">
40
- <IconSpinner className="mr-2 animate-spin" />
41
- <p>Loading...</p>
42
- </div>
43
- ) : null}
44
- {!isLoading && sortedResults.length === 0 && <p>No results found.</p>}
45
  <div className="relative overflow-x-auto">
46
- {!isLoading && sortedResults.length > 0 && (
47
- <table className="w-full text-xl text-left rtl:text-right text-gray-500 dark:text-gray-400 p-4">
48
- <thead className="text-sm text-center text-gray-700 uppercase bg-gray-400 dark:bg-gray-700 dark:text-gray-400">
49
- <tr>
50
- <th scope="col" className="px-6 py-3">ID</th>
51
- <th scope="col" className="px-6 py-3">File Name</th>
52
- <th scope="col" className="px-6 py-3">Page</th>
53
- <th scope="col" className="px-6 py-3">Text</th>
54
- <th scope="col" className="px-6 py-3">Score</th>
55
- <th scope="col" className="px-6 py-3">Action</th>
56
- <th scope="col" className="px-6 py-3">Expand</th>
57
- </tr>
58
- </thead>
59
- <tbody>
60
- {sortedResults.map((result) => (
61
- <Fragment key={result.id}>
62
- <tr
63
- 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 cursor-pointer"
64
- >
65
- <th scope="row" className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white" onClick={() => handleToggleExpand(result.id)}>{result.id}</th>
66
- <td className="px-6 py-4" onClick={() => handleToggleExpand(result.id)}>{result.file_name}</td>
67
- <td className="px-6 py-4" onClick={() => handleToggleExpand(result.id)}>{result.page_no}</td>
68
- <td className="px-6 py-4" onClick={() => handleToggleExpand(result.id)}>
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  {expandedResult === result.id ? (
70
- <span>{result.text}</span>
71
  ) : (
72
- <span>{result.text.slice(0, 50)}...</span>
73
  )}
74
- </td>
75
- <td className="px-6 py-4" onClick={() => handleToggleExpand(result.id)}>{result.similarity_score.toFixed(2)}</td>
76
- <td className="px-6 py-4 hover:bg-green-300 transition duration-300 ease-in-out transform hover:scale-105" onClick={() => handleCopyText(result.text)}>
77
- <div role="img" aria-label="copy text icon" className="flex items-center justify-center">
78
- <Copy />
79
- </div>
80
- </td>
81
- <td className="px-6 py-4" onClick={() => handleToggleExpand(result.id)}>
82
- <div role="img" aria-label="expand icon" className="flex items-center justify-center">
83
- {expandedResult === result.id ? (
84
- <ArrowUpFromLine />
85
- ) : (
86
- <ArrowDownFromLine />
87
- )}
88
- </div>
89
- </td>
90
- </tr>
91
- </Fragment>
92
- ))}
93
- </tbody>
94
- </table>
95
- )}
96
  </div>
97
  </div>
98
  );
 
 
1
  import { IconSpinner } from "@/app/components/ui/icons";
2
+ import { Fragment, useEffect, useState } from "react";
3
  import { ArrowDownFromLine, ArrowUpFromLine, Copy } from "lucide-react";
4
  import { ToastContainer, toast } from 'react-toastify';
5
  import 'react-toastify/dist/ReactToastify.css';
6
+ import { SearchHandler, SearchResult } from "@/app/components/ui/search/search.interface";
7
 
8
+ const SearchResults: React.FC<SearchHandler> = ({ query, results, isLoading, searchButtonPressed }) => {
9
+ const [sortedResults, setSortedResults] = useState<SearchResult[]>([]);
10
+ const [expandedResult, setExpandedResult] = useState<number | null>(null);
 
11
 
12
+ // Sort results by similarity score whenever results or query change
13
+ useEffect(() => {
14
+ if (query.trim() === "" && !searchButtonPressed){
15
+ // Reset sortedResults when query is empty
16
+ setSortedResults([]);
17
+ } else if (query.trim() !== "" && searchButtonPressed) {
18
+ // Sort results by similarity score
19
+ const sorted = results.slice().sort((a, b) => b.similarity_score - a.similarity_score);
20
+ // Update sortedResults state
21
+ setSortedResults(sorted);
22
+ }
23
+ }, [query, results]);
24
 
25
+ // Log sortedResults outside of useEffect to ensure you're getting the updated state
26
+ // console.log("Sorted results:", sortedResults);
 
27
 
28
+ // Handle expand/collapse of search results
29
  const handleToggleExpand = (resultId: number) => {
30
  setExpandedResult((prevId) => (prevId === resultId ? null : resultId));
31
  };
32
 
33
+ // TODO: Add a collapse all/ expand all button
34
+
35
+ // TODO: Add a button to clear search results & not on query change to empty string
36
+
37
+ // Handle Reseting the expanded result when the search button is pressed
38
+ useEffect(() => {
39
+ if (searchButtonPressed) {
40
+ setExpandedResult(null);
41
+ }
42
+ }, [searchButtonPressed]);
43
+
44
+ // Handle when there are no search results and search button is not pressed
45
+ if (sortedResults.length === 0 && !searchButtonPressed) {
46
+ return (
47
+ null
48
+ );
49
+ }
50
+
51
+ if (isLoading) {
52
+ return (
53
+ <div className="flex w-full items-center justify-center rounded-xl bg-white dark:bg-zinc-700/30 p-4 shadow-xl">
54
+ <IconSpinner className="mr-2 animate-spin" />
55
+ <p>Loading...</p>
56
+ </div>
57
+ );
58
+ }
59
+
60
+ // Handle when there are no search results
61
+ if (sortedResults.length === 0 && query.trim() !== "" && searchButtonPressed) {
62
+ return (
63
+ <div className="flex w-full items-center justify-center rounded-xl bg-white dark:bg-zinc-700/30 p-4 shadow-xl">
64
+ <p>No results found.</p>
65
+ </div>
66
+ );
67
+ }
68
+
69
  const showToastMessage = () => {
70
  toast.success("Text copied to clipboard!", {
71
  position: "top-center",
 
82
  return (
83
  <div className="flex w-full items-center justify-between rounded-xl bg-white dark:bg-zinc-700/30 p-4 shadow-xl">
84
  <ToastContainer />
 
 
 
 
 
 
 
85
  <div className="relative overflow-x-auto">
86
+ <table className="w-full text-xl text-left rtl:text-right text-gray-500 dark:text-gray-400 p-4">
87
+ <thead className="text-sm text-center text-gray-700 uppercase bg-gray-400 dark:bg-gray-700 dark:text-gray-400">
88
+ <tr>
89
+ <th scope="col" className="px-6 py-3">ID</th>
90
+ <th scope="col" className="px-6 py-3">File Name</th>
91
+ <th scope="col" className="px-6 py-3">Page</th>
92
+ <th scope="col" className="px-6 py-3">Text</th>
93
+ <th scope="col" className="px-6 py-3">Score</th>
94
+ <th scope="col" className="px-6 py-3">Action</th>
95
+ <th scope="col" className="px-6 py-3">Expand</th>
96
+ </tr>
97
+ </thead>
98
+ <tbody>
99
+ {sortedResults.map((result) => (
100
+ <Fragment key={result.id}>
101
+ <tr
102
+ 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 cursor-pointer"
103
+ >
104
+ <th scope="row" className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white" onClick={() => handleToggleExpand(result.id)}>{result.id}</th>
105
+ <td className="px-6 py-4" onClick={() => handleToggleExpand(result.id)}>{result.file_name}</td>
106
+ <td className="px-6 py-4" onClick={() => handleToggleExpand(result.id)}>{result.page_no}</td>
107
+ <td className="px-6 py-4" onClick={() => handleToggleExpand(result.id)}>
108
+ {expandedResult === result.id ? (
109
+ <span>{result.text}</span>
110
+ ) : (
111
+ <span>{result.text.slice(0, 50)}...</span>
112
+ )}
113
+ </td>
114
+ <td className="px-6 py-4" onClick={() => handleToggleExpand(result.id)}>{result.similarity_score.toFixed(2)}</td>
115
+ <td className="px-6 py-4 hover:bg-green-300 transition duration-300 ease-in-out transform hover:scale-105" onClick={() => handleCopyText(result.text)}>
116
+ <div role="img" aria-label="copy text icon" className="flex items-center justify-center">
117
+ <Copy />
118
+ </div>
119
+ </td>
120
+ <td className="px-6 py-4" onClick={() => handleToggleExpand(result.id)}>
121
+ <div role="img" aria-label="expand icon" className="flex items-center justify-center">
122
  {expandedResult === result.id ? (
123
+ <ArrowUpFromLine />
124
  ) : (
125
+ <ArrowDownFromLine />
126
  )}
127
+ </div>
128
+ </td>
129
+ </tr>
130
+ </Fragment>
131
+ ))}
132
+ </tbody>
133
+ </table>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  </div>
135
  </div>
136
  );
frontend/app/components/ui/search/{search-types.tsx → search.interface.ts} RENAMED
@@ -1,4 +1,14 @@
1
- // SearchTypes.tsx
 
 
 
 
 
 
 
 
 
 
2
  export interface SearchResult {
3
  id: number;
4
  file_name: string;
 
1
+ import { ChangeEvent, FormEvent } from "react";
2
+
3
+ export interface SearchHandler {
4
+ query: string;
5
+ isLoading: boolean;
6
+ onInputChange?: (e: ChangeEvent<HTMLInputElement>) => void;
7
+ onSearchSubmit?: (e: FormEvent) => void;
8
+ results: SearchResult[];
9
+ searchButtonPressed?: boolean;
10
+ }
11
+
12
  export interface SearchResult {
13
  id: number;
14
  file_name: string;
frontend/app/components/ui/search/useSearch.tsx CHANGED
@@ -1,7 +1,7 @@
1
  "use client";
2
 
3
  import { useState, useEffect } from "react";
4
- import { SearchResult } from "@/app/components/ui/search/search-types";
5
 
6
  interface UseSearchResult {
7
  searchResults: SearchResult[];
@@ -14,27 +14,31 @@ const search_api = process.env.NEXT_PUBLIC_SEARCH_API;
14
  const useSearch = (): UseSearchResult => {
15
  const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
16
  const [isLoading, setIsLoading] = useState(false);
 
17
 
18
  const handleSearch = async (query: string): Promise<void> => {
 
19
  setIsLoading(true);
20
 
 
21
  if (!search_api) {
22
  console.error("Search API is not defined");
23
  setIsLoading(false);
24
  return;
25
  }
 
26
  // Perform search logic here
27
  try {
28
  console.log("Searching for:", query);
29
  // check if query is empty
30
- if (query === "") {
31
  setSearchResults([]);
32
  setIsLoading(false);
33
  return;
34
  }
35
  const response = await fetch(`${search_api}?query=${query}`, {
36
  signal: AbortSignal.timeout(120000), // Abort the request if it takes longer than 120 seconds
37
- });
38
  const data = await response.json();
39
  setSearchResults(data);
40
  } catch (error: any) {
 
1
  "use client";
2
 
3
  import { useState, useEffect } from "react";
4
+ import { SearchResult } from "@/app/components/ui/search/search.interface";
5
 
6
  interface UseSearchResult {
7
  searchResults: SearchResult[];
 
14
  const useSearch = (): UseSearchResult => {
15
  const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
16
  const [isLoading, setIsLoading] = useState(false);
17
+ const [isSearchButtonPressed, setIsSearchButtonPressed] = useState(false);
18
 
19
  const handleSearch = async (query: string): Promise<void> => {
20
+ setIsSearchButtonPressed(isSearchButtonPressed);
21
  setIsLoading(true);
22
 
23
+ // Check if search API is defined
24
  if (!search_api) {
25
  console.error("Search API is not defined");
26
  setIsLoading(false);
27
  return;
28
  }
29
+
30
  // Perform search logic here
31
  try {
32
  console.log("Searching for:", query);
33
  // check if query is empty
34
+ if (query.trim() === "") { // Trim whitespace from query and check if it's empty
35
  setSearchResults([]);
36
  setIsLoading(false);
37
  return;
38
  }
39
  const response = await fetch(`${search_api}?query=${query}`, {
40
  signal: AbortSignal.timeout(120000), // Abort the request if it takes longer than 120 seconds
41
+ });
42
  const data = await response.json();
43
  setSearchResults(data);
44
  } catch (error: any) {
frontend/app/sign-in/page.tsx CHANGED
@@ -1,8 +1,10 @@
1
  "use client";
2
 
3
- import { GoogleLoginButton, SGIDLoginButton } from '@/app/components/login-buttons';
 
4
 
5
  const SignInPage = () => {
 
6
  return (
7
  <div className="rounded-xl shadow-xl p-4 max-w-5xl w-full">
8
  <div className="max-w-2xl mx-auto text-center">
@@ -16,7 +18,7 @@ const SignInPage = () => {
16
  <GoogleLoginButton />
17
  <SGIDLoginButton />
18
  <p className="text-gray-200 text-sm">
19
- Note: SGID login is only available via SingPass.
20
  </p>
21
  <div className="flex items-center justify-center gap-4">
22
  <div className="w-full h-px bg-gray-300"></div>
@@ -24,8 +26,8 @@ const SignInPage = () => {
24
  <button
25
  className="text-white font-bold hover:underline mt-4 rounded-md shadow-lg py-2 bg-gray-500 hover:bg-gray-300"
26
  onClick={() => {
27
- // Redirect back to the home page
28
- window.location.href = '/';
29
  }}
30
  >
31
  Cancel
 
1
  "use client";
2
 
3
+ import { GoogleLoginButton, SGIDLoginButton } from '@/app/components/ui/login-buttons';
4
+ import { useRouter } from 'next/navigation';
5
 
6
  const SignInPage = () => {
7
+ const router = useRouter();
8
  return (
9
  <div className="rounded-xl shadow-xl p-4 max-w-5xl w-full">
10
  <div className="max-w-2xl mx-auto text-center">
 
18
  <GoogleLoginButton />
19
  <SGIDLoginButton />
20
  <p className="text-gray-200 text-sm">
21
+ Note: SGID login is only available via SingPass App.
22
  </p>
23
  <div className="flex items-center justify-center gap-4">
24
  <div className="w-full h-px bg-gray-300"></div>
 
26
  <button
27
  className="text-white font-bold hover:underline mt-4 rounded-md shadow-lg py-2 bg-gray-500 hover:bg-gray-300"
28
  onClick={() => {
29
+ // Redirect back to the last page
30
+ router.back();
31
  }}
32
  >
33
  Cancel
frontend/public/sitemap.xml CHANGED
@@ -1,20 +1,27 @@
1
  <?xml version="1.0" encoding="UTF-8"?>
2
  <urlset
3
- xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
4
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5
- xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
6
- http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
7
-
8
- <url>
9
- <loc>https://smart-retrieval-demo.vercel.app/</loc>
10
- <lastmod>2024-01-30T06:02:15+00:00</lastmod>
11
- <priority>1.00</priority>
12
- </url>
13
- <url>
14
- <loc>https://smart-retrieval-demo.vercel.app/about</loc>
15
- <lastmod>2024-01-30T06:02:15+00:00</lastmod>
16
- <priority>0.80</priority>
17
- </url>
18
-
19
-
 
 
 
 
 
 
 
20
  </urlset>
 
1
  <?xml version="1.0" encoding="UTF-8"?>
2
  <urlset
3
+ xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
4
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5
+ xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
6
+ <url>
7
+ <loc>https://smart-retrieval-demo.vercel.app/</loc>
8
+ <lastmod>2024-02-06T06:15:09+00:00</lastmod>
9
+ <priority>1.00</priority>
10
+ </url>
11
+ <url>
12
+ <loc>
13
+ https://smart-retrieval-demo.vercel.app/about</loc>
14
+ <lastmod>2024-02-06T06:15:09+00:00</lastmod>
15
+ <priority>0.80</priority>
16
+ </url>
17
+ <url>
18
+ <loc>https://smart-retrieval-demo.vercel.app/terms-of-service</loc>
19
+ <lastmod>2024-02-06T06:15:09+00:00</lastmod>
20
+ <priority>0.80</priority>
21
+ </url>
22
+ <url>
23
+ <loc>https://smart-retrieval-demo.vercel.app/privacy-policy</loc>
24
+ <lastmod>2024-02-06T06:15:09+00:00</lastmod>
25
+ <priority>0.80</priority>
26
+ </url>
27
  </urlset>