Spaces:
Build error
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 +1 -1
- Dockerfile +3 -2
- README.md +1 -2
- backend/README.md +18 -6
- backend/backend/app/utils/contants.py +2 -1
- backend/backend/app/utils/index.py +3 -11
- backend/backend/get_PSSCOC_docs.py +186 -0
- backend/backend/main.py +7 -0
- backend/poetry.lock +0 -0
- backend/pyproject.toml +20 -12
- frontend/app/components/chat-section.tsx +9 -1
- frontend/app/components/header.tsx +19 -14
- frontend/app/components/query-section.tsx +8 -0
- frontend/app/components/search-section.tsx +14 -1
- frontend/app/components/ui/autofill-prompt/autofill-prompt-dialog.tsx +95 -0
- frontend/app/components/ui/autofill-prompt/autofill-prompt.interface.tsx +14 -0
- frontend/app/components/ui/autofill-prompt/autofill-search-prompt-dialog.tsx +97 -0
- frontend/app/components/ui/chat/chat-input.tsx +6 -5
- frontend/app/components/{login-buttons.tsx → ui/login-buttons.tsx} +5 -10
- frontend/app/components/ui/main-container.tsx +1 -1
- frontend/app/components/ui/search/search-input.tsx +2 -9
- frontend/app/components/ui/search/search-results.tsx +101 -63
- frontend/app/components/ui/search/{search-types.tsx → search.interface.ts} +11 -1
- frontend/app/components/ui/search/useSearch.tsx +7 -3
- frontend/app/sign-in/page.tsx +6 -4
- frontend/public/sitemap.xml +24 -17
@@ -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 --
|
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: |
|
@@ -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 --
|
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
|
@@ -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 |
|
@@ -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 |
-
|
|
|
27 |
|
28 |
Then activate the conda environment:
|
29 |
|
@@ -33,24 +40,29 @@ conda activate SmartRetrieval
|
|
33 |
|
34 |
Second, setup the environment:
|
35 |
|
36 |
-
```
|
37 |
-
# Only
|
|
|
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 --
|
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 --
|
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
|
@@ -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 =
|
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 {}
|
@@ -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=
|
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 |
-
|
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,
|
@@ -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()
|
@@ -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")
|
The diff for this file is too large to render.
See raw diff
|
|
@@ -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.
|
14 |
-
pypdf = "^3.17.
|
15 |
python-dotenv = "^1.0.0"
|
16 |
-
llama-cpp-python = "^0.2.
|
17 |
-
transformers = "^4.
|
18 |
docx2txt = "^0.8"
|
|
|
19 |
|
20 |
-
|
|
|
21 |
[tool.poetry.group.dev.dependencies]
|
|
|
22 |
flake8 = "^7.0.0"
|
23 |
-
pytest = "^
|
24 |
|
25 |
# For CPU torch version: Windows and Linux
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
|
|
|
|
|
|
31 |
|
32 |
-
|
|
|
|
|
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'" },
|
@@ -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}
|
@@ -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 |
-
{
|
174 |
-
<
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
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)
|
204 |
-
|
205 |
-
|
|
|
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 |
}
|
@@ -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}
|
@@ -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 |
};
|
@@ -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 |
+
}
|
@@ -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 |
+
];
|
@@ -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 |
+
}
|
@@ -1,6 +1,6 @@
|
|
1 |
-
import { Button } from "
|
2 |
-
import { Input } from "
|
3 |
-
import { ChatHandler } from "
|
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 |
);
|
@@ -1,8 +1,7 @@
|
|
1 |
'use client'
|
2 |
|
3 |
-
import { useState
|
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'
|
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'
|
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" />
|
@@ -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 |
);
|
@@ -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 |
-
|
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,
|
@@ -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 |
-
|
9 |
-
|
10 |
-
|
11 |
-
}
|
12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
|
14 |
-
|
15 |
-
|
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 |
-
|
47 |
-
<
|
48 |
-
<
|
49 |
-
<
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
{
|
61 |
-
<
|
62 |
-
|
63 |
-
|
64 |
-
>
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
{expandedResult === result.id ? (
|
70 |
-
<
|
71 |
) : (
|
72 |
-
<
|
73 |
)}
|
74 |
-
</
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
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 |
);
|
@@ -1,4 +1,14 @@
|
|
1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
@@ -1,7 +1,7 @@
|
|
1 |
"use client";
|
2 |
|
3 |
import { useState, useEffect } from "react";
|
4 |
-
import { SearchResult } from "@/app/components/ui/search/search
|
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) {
|
@@ -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
|
28 |
-
|
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
|
@@ -1,20 +1,27 @@
|
|
1 |
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
<urlset
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
<
|
9 |
-
|
10 |
-
|
11 |
-
<
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
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>
|