diff --git a/ProductivitySuite/README.md b/ProductivitySuite/README.md
index a5eb5735ec..dc3f233a9f 100644
--- a/ProductivitySuite/README.md
+++ b/ProductivitySuite/README.md
@@ -38,10 +38,7 @@ flowchart LR
direction LR
LLM_CG([LLM MicroService]):::blue
end
- subgraph FaqGen-MegaService["FaqGen MegaService "]
- direction LR
- LLM_F([LLM MicroService]):::blue
- end
+
subgraph UserInterface[" User Interface "]
direction LR
a([User Input Query]):::orchid
@@ -63,7 +60,7 @@ flowchart LR
LLM_gen_CG{{LLM Service }}
GW_CG([CodeGen GateWay ]):::orange
LLM_gen_F{{LLM Service }}
- GW_F([FaqGen GateWay ]):::orange
+
PR([Prompt Registry MicroService]):::blue
CH([Chat History MicroService]):::blue
MDB{{Mongo DB }}
@@ -118,11 +115,6 @@ flowchart LR
direction LR
LLM_CG <-.-> LLM_gen_CG
- %% Questions interaction
- direction LR
- UI --> GW_F
- GW_F <==> FaqGen-MegaService
-
%% Embedding service flow
direction LR
@@ -158,10 +150,6 @@ Engage in intelligent conversations with your documents using our advanced **Ret
Summarize lengthy documents or articles, enabling you to grasp key takeaways quickly. Save time and effort with our intelligent summarization feature!
-### ❓ FAQ Generation
-
-Effortlessly create comprehensive FAQs based on your documents. Ensure your users have access to the information they need with minimal effort!
-
### 💻 Code Generation
Boost your coding productivity by providing a description of the functionality you require. Our application generates corresponding code snippets, saving you valuable time and effort!
diff --git a/ProductivitySuite/assets/img/Login_page.png b/ProductivitySuite/assets/img/Login_page.png
index 0c325e5f2e..7d56b16728 100644
Binary files a/ProductivitySuite/assets/img/Login_page.png and b/ProductivitySuite/assets/img/Login_page.png differ
diff --git a/ProductivitySuite/assets/img/chat_qna_init.png b/ProductivitySuite/assets/img/chat_qna_init.png
index c13630594b..1179175fb6 100644
Binary files a/ProductivitySuite/assets/img/chat_qna_init.png and b/ProductivitySuite/assets/img/chat_qna_init.png differ
diff --git a/ProductivitySuite/assets/img/chatqna_with_conversation.png b/ProductivitySuite/assets/img/chatqna_with_conversation.png
index 1dad3a099e..0fcd4c7921 100644
Binary files a/ProductivitySuite/assets/img/chatqna_with_conversation.png and b/ProductivitySuite/assets/img/chatqna_with_conversation.png differ
diff --git a/ProductivitySuite/assets/img/codegen.png b/ProductivitySuite/assets/img/codegen.png
index a4d38f6f42..70620a448b 100644
Binary files a/ProductivitySuite/assets/img/codegen.png and b/ProductivitySuite/assets/img/codegen.png differ
diff --git a/ProductivitySuite/assets/img/data_source.png b/ProductivitySuite/assets/img/data_source.png
index ae45e12238..6afe20afd4 100644
Binary files a/ProductivitySuite/assets/img/data_source.png and b/ProductivitySuite/assets/img/data_source.png differ
diff --git a/ProductivitySuite/assets/img/doc_summary.png b/ProductivitySuite/assets/img/doc_summary.png
new file mode 100644
index 0000000000..f279606f38
Binary files /dev/null and b/ProductivitySuite/assets/img/doc_summary.png differ
diff --git a/ProductivitySuite/docker_compose/intel/cpu/xeon/README.md b/ProductivitySuite/docker_compose/intel/cpu/xeon/README.md
index 8191b7cc70..af63141330 100644
--- a/ProductivitySuite/docker_compose/intel/cpu/xeon/README.md
+++ b/ProductivitySuite/docker_compose/intel/cpu/xeon/README.md
@@ -108,68 +108,46 @@ Since the `compose.yaml` will consume some environment variables, you need to se
export host_ip="External_Public_IP"
```
-**Export the value of your Huggingface API token to the `your_hf_api_token` environment variable**
+**Export the value of your Huggingface API token to the `HUGGINGFACEHUB_API_TOKEN` environment variable**
> Change the Your_Huggingface_API_Token below with tyour actual Huggingface API Token value
```
-export your_hf_api_token="Your_Huggingface_API_Token"
+export HUGGINGFACEHUB_API_TOKEN="Your_Huggingface_API_Token"
```
**Append the value of the public IP address to the no_proxy list**
```
-export your_no_proxy=${your_no_proxy},"External_Public_IP"
+export no_proxy=${no_proxy},"External_Public_IP"
```
```bash
-export MONGO_HOST=${host_ip}
-export MONGO_PORT=27017
-export DB_NAME="test"
-export COLLECTION_NAME="Conversations"
+export DB_NAME="opea"
export EMBEDDING_MODEL_ID="BAAI/bge-base-en-v1.5"
export RERANK_MODEL_ID="BAAI/bge-reranker-base"
export LLM_MODEL_ID="Intel/neural-chat-7b-v3-3"
export LLM_MODEL_ID_CODEGEN="meta-llama/CodeLlama-7b-hf"
-export TEI_EMBEDDING_ENDPOINT="http://${host_ip}:6006"
-export TEI_RERANKING_ENDPOINT="http://${host_ip}:8808"
-export TGI_LLM_ENDPOINT="http://${host_ip}:9009"
-export REDIS_URL="redis://${host_ip}:6379"
export INDEX_NAME="rag-redis"
-export HUGGINGFACEHUB_API_TOKEN=${your_hf_api_token}
-export MEGA_SERVICE_HOST_IP=${host_ip}
-export EMBEDDING_SERVICE_HOST_IP=${host_ip}
-export RETRIEVER_SERVICE_HOST_IP=${host_ip}
-export RERANK_SERVICE_HOST_IP=${host_ip}
-export LLM_SERVICE_HOST_IP=${host_ip}
-export LLM_SERVICE_HOST_IP_DOCSUM=${host_ip}
-export LLM_SERVICE_HOST_IP_FAQGEN=${host_ip}
-export LLM_SERVICE_HOST_IP_CODEGEN=${host_ip}
-export LLM_SERVICE_HOST_IP_CHATQNA=${host_ip}
-export TGI_LLM_ENDPOINT_CHATQNA="http://${host_ip}:9009"
-export TGI_LLM_ENDPOINT_CODEGEN="http://${host_ip}:8028"
-export TGI_LLM_ENDPOINT_FAQGEN="http://${host_ip}:9009"
-export TGI_LLM_ENDPOINT_DOCSUM="http://${host_ip}:9009"
+export HUGGINGFACEHUB_API_TOKEN=${HUGGINGFACEHUB_API_TOKEN}
export BACKEND_SERVICE_ENDPOINT_CHATQNA="http://${host_ip}:8888/v1/chatqna"
-export DATAPREP_DELETE_FILE_ENDPOINT="http://${host_ip}:5000/v1/dataprep/delete"
+export DATAPREP_DELETE_FILE_ENDPOINT="http://${host_ip}:6007/v1/dataprep/delete"
export BACKEND_SERVICE_ENDPOINT_CODEGEN="http://${host_ip}:7778/v1/codegen"
export BACKEND_SERVICE_ENDPOINT_DOCSUM="http://${host_ip}:8890/v1/docsum"
-export DATAPREP_SERVICE_ENDPOINT="http://${host_ip}:5000/v1/dataprep/ingest"
-export DATAPREP_GET_FILE_ENDPOINT="http://${host_ip}:5000/v1/dataprep/get"
+export DATAPREP_SERVICE_ENDPOINT="http://${host_ip}:6007/v1/dataprep/ingest"
+export DATAPREP_GET_FILE_ENDPOINT="http://${host_ip}:6007/v1/dataprep/get"
export CHAT_HISTORY_CREATE_ENDPOINT="http://${host_ip}:6012/v1/chathistory/create"
export CHAT_HISTORY_CREATE_ENDPOINT="http://${host_ip}:6012/v1/chathistory/create"
export CHAT_HISTORY_DELETE_ENDPOINT="http://${host_ip}:6012/v1/chathistory/delete"
export CHAT_HISTORY_GET_ENDPOINT="http://${host_ip}:6012/v1/chathistory/get"
export PROMPT_SERVICE_GET_ENDPOINT="http://${host_ip}:6018/v1/prompt/get"
export PROMPT_SERVICE_CREATE_ENDPOINT="http://${host_ip}:6018/v1/prompt/create"
+export PROMPT_SERVICE_DELETE_ENDPOINT="http://${host_ip}:6018/v1/prompt/delete"
export KEYCLOAK_SERVICE_ENDPOINT="http://${host_ip}:8080"
-export LLM_SERVICE_HOST_PORT_FAQGEN=9002
-export LLM_SERVICE_HOST_PORT_CODEGEN=9001
-export LLM_SERVICE_HOST_PORT_DOCSUM=9003
-export PROMPT_COLLECTION_NAME="prompt"
-export RERANK_SERVER_PORT=8808
-export EMBEDDING_SERVER_PORT=6006
-export LLM_SERVER_PORT=9009
+export DocSum_COMPONENT_NAME="OpeaDocSumTgi"
+
+#Set no proxy
+export no_proxy="$no_proxy,tgi_service_codegen,llm_codegen,tei-embedding-service,tei-reranking-service,chatqna-xeon-backend-server,retriever,tgi-service,redis-vector-db,whisper,llm-docsum-tgi,docsum-xeon-backend-server,mongo,codegen"
```
Note: Please replace with `host_ip` with you external IP address, do not use localhost.
@@ -203,16 +181,7 @@ Please refer to **[keycloak_setup_guide](keycloak_setup_guide.md)** for more det
-H 'Content-Type: application/json'
```
-2. Embedding Microservice
-
- ```bash
- curl http://${host_ip}:6000/v1/embeddings\
- -X POST \
- -d '{"text":"hello"}' \
- -H 'Content-Type: application/json'
- ```
-
-3. Retriever Microservice
+2. Retriever Microservice
To consume the retriever microservice, you need to generate a mock embedding vector by Python script. The length of embedding vector
is determined by the embedding model.
@@ -222,13 +191,13 @@ Please refer to **[keycloak_setup_guide](keycloak_setup_guide.md)** for more det
```bash
export your_embedding=$(python3 -c "import random; embedding = [random.uniform(-1, 1) for _ in range(768)]; print(embedding)")
- curl http://${host_ip}:7000/v1/retrieval \
+ curl http://${host_ip}:7001/v1/retrieval \
-X POST \
-d "{\"text\":\"test\",\"embedding\":${your_embedding}}" \
-H 'Content-Type: application/json'
```
-4. TEI Reranking Service
+3. TEI Reranking Service
```bash
curl http://${host_ip}:8808/rerank \
@@ -237,16 +206,7 @@ Please refer to **[keycloak_setup_guide](keycloak_setup_guide.md)** for more det
-H 'Content-Type: application/json'
```
-5. Reranking Microservice
-
- ```bash
- curl http://${host_ip}:8000/v1/reranking\
- -X POST \
- -d '{"initial_query":"What is Deep Learning?", "retrieved_docs": [{"text":"Deep Learning is not..."}, {"text":"Deep learning is..."}]}' \
- -H 'Content-Type: application/json'
- ```
-
-6. LLM backend Service (ChatQnA, DocSum, FAQGen)
+4. LLM backend Service (ChatQnA, DocSum)
```bash
curl http://${host_ip}:9009/generate \
@@ -255,7 +215,7 @@ Please refer to **[keycloak_setup_guide](keycloak_setup_guide.md)** for more det
-H 'Content-Type: application/json'
```
-7. LLM backend Service (CodeGen)
+5. LLM backend Service (CodeGen)
```bash
curl http://${host_ip}:8028/generate \
@@ -264,59 +224,42 @@ Please refer to **[keycloak_setup_guide](keycloak_setup_guide.md)** for more det
-H 'Content-Type: application/json'
```
-8. ChatQnA LLM Microservice
+6. CodeGen LLM Microservice
```bash
- curl http://${host_ip}:9000/v1/chat/completions\
+ curl http://${host_ip}:9001/v1/chat/completions\
-X POST \
- -d '{"query":"What is Deep Learning?","max_tokens":17,"top_k":10,"top_p":0.95,"typical_p":0.95,"temperature":0.01,"repetition_penalty":1.03,"stream":true}' \
+ -d '{"query":"def print_hello_world():"}' \
-H 'Content-Type: application/json'
```
-9. CodeGen LLM Microservice
+7. DocSum LLM Microservice
```bash
- curl http://${host_ip}:9001/v1/chat/completions\
+ curl http://${host_ip}:9003/v1/docsum\
-X POST \
- -d '{"query":"def print_hello_world():"}' \
+ -d '{"messages":"Text Embeddings Inference (TEI) is a toolkit for deploying and serving open source text embeddings and sequence classification models. TEI enables high-performance extraction for the most popular models, including FlagEmbedding, Ember, GTE and E5", "type": "text"}' \
-H 'Content-Type: application/json'
```
-10. DocSum LLM Microservice
-
- ```bash
- curl http://${host_ip}:9003/v1/docsum\
- -X POST \
- -d '{"query":"Text Embeddings Inference (TEI) is a toolkit for deploying and serving open source text embeddings and sequence classification models. TEI enables high-performance extraction for the most popular models, including FlagEmbedding, Ember, GTE and E5"}' \
- -H 'Content-Type: application/json'
- ```
-
-11. FAQGen LLM Microservice
+8. ChatQnA MegaService
- ```bash
- curl http://${host_ip}:9002/v1/faqgen\
- -X POST \
- -d '{"query":"Text Embeddings Inference (TEI) is a toolkit for deploying and serving open source text embeddings and sequence classification models. TEI enables high-performance extraction for the most popular models, including FlagEmbedding, Ember, GTE and E5"}' \
- -H 'Content-Type: application/json'
- ```
-
-12. ChatQnA MegaService
-
- ```bash
- curl http://${host_ip}:8888/v1/chatqna -H "Content-Type: application/json" -d '{
- "messages": "What is the revenue of Nike in 2023?"
- }'
- ```
+ ```bash
+ curl http://${host_ip}:8888/v1/chatqna -H "Content-Type: application/json" -d '{
+ "messages": "What is the revenue of Nike in 2023?"
+ }'
+ ```
-13. DocSum MegaService
+9. DocSum MegaService
- ```bash
- curl http://${host_ip}:8890/v1/docsum -H "Content-Type: application/json" -d '{
- "messages": "Text Embeddings Inference (TEI) is a toolkit for deploying and serving open source text embeddings and sequence classification models. TEI enables high-performance extraction for the most popular models, including FlagEmbedding, Ember, GTE and E5."
- }'
- ```
+ ```bash
+ curl http://${host_ip}:8890/v1/docsum -H "Content-Type: application/json" -d '{
+ "messages": "Text Embeddings Inference (TEI) is a toolkit for deploying and serving open source text embeddings and sequence classification models. TEI enables high-performance extraction for the most popular models, including FlagEmbedding, Ember, GTE and E5.",
+ "type": "text"
+ }'
+ ```
-14. CodeGen MegaService
+10. CodeGen MegaService
```bash
curl http://${host_ip}:7778/v1/codegen -H "Content-Type: application/json" -d '{
@@ -324,7 +267,7 @@ Please refer to **[keycloak_setup_guide](keycloak_setup_guide.md)** for more det
}'
```
-15. Dataprep Microservice
+11. Dataprep Microservice
If you want to update the default knowledge base, you can use the following commands:
@@ -374,13 +317,13 @@ Please refer to **[keycloak_setup_guide](keycloak_setup_guide.md)** for more det
-H "Content-Type: application/json"
```
-16. Prompt Registry Microservice
+12. Prompt Registry Microservice
If you want to update the default Prompts in the application for your user, you can use the following commands:
```bash
curl -X 'POST' \
- http://{host_ip}:6018/v1/prompt/create \
+ "http://${host_ip}:6018/v1/prompt/create" \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
@@ -392,14 +335,14 @@ Please refer to **[keycloak_setup_guide](keycloak_setup_guide.md)** for more det
```bash
curl -X 'POST' \
- http://{host_ip}:6018/v1/prompt/get \
+ "http://${host_ip}:6018/v1/prompt/get" \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"user": "test"}'
curl -X 'POST' \
- http://{host_ip}:6018/v1/prompt/get \
+ "http://${host_ip}:6018/v1/prompt/get" \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
@@ -410,14 +353,14 @@ Please refer to **[keycloak_setup_guide](keycloak_setup_guide.md)** for more det
```bash
curl -X 'POST' \
- http://{host_ip}:6018/v1/prompt/delete \
+ "http://${host_ip}:6018/v1/prompt/delete" \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"user": "test", "prompt_id":"{prompt_id to be deleted}"}'
```
-17. Chat History Microservice
+13. Chat History Microservice
To validate the chatHistory Microservice, you can use the following commands.
@@ -527,15 +470,4 @@ Here're some of the project's features:
#### Screenshots
-
-
-
-### ❓ FAQ Generator
-
-- **Generate FAQs from Text via Pasting**: Paste the text to into the text box, then click 'Generate FAQ' to produce a condensed FAQ of the content, which will be displayed in the 'FAQ' box below.
-
-- **Generate FAQs from Text via txt file Upload**: Upload the file in the Upload bar, then click 'Generate FAQ' to produce a condensed FAQ of the content, which will be displayed in the 'FAQ' box below.
-
-#### Screenshots
-
-
+
diff --git a/ProductivitySuite/docker_compose/intel/cpu/xeon/compose.yaml b/ProductivitySuite/docker_compose/intel/cpu/xeon/compose.yaml
index 2a4351d5da..00a16c1670 100644
--- a/ProductivitySuite/docker_compose/intel/cpu/xeon/compose.yaml
+++ b/ProductivitySuite/docker_compose/intel/cpu/xeon/compose.yaml
@@ -25,10 +25,10 @@ services:
no_proxy: ${no_proxy}
http_proxy: ${http_proxy}
https_proxy: ${https_proxy}
- REDIS_URL: ${REDIS_URL}
+ REDIS_URL: redis://redis-vector-db:6379
REDIS_HOST: redis-vector-db
INDEX_NAME: ${INDEX_NAME}
- TEI_EMBEDDING_ENDPOINT: ${TEI_EMBEDDING_ENDPOINT}
+ TEI_EMBEDDING_ENDPOINT: http://tei-embedding-service:80
HUGGINGFACEHUB_API_TOKEN: ${HUGGINGFACEHUB_API_TOKEN}
DATAPREP_TYPE: ${DATAPREP_TYPE}
LOGFLAG: ${LOGFLAG}
@@ -57,39 +57,22 @@ services:
interval: 10s
timeout: 10s
retries: 60
- embedding:
- image: ${REGISTRY:-opea}/embedding:${TAG:-latest}
- container_name: embedding-server
- depends_on:
- tei-embedding-service:
- condition: service_healthy
- ports:
- - "6000:6000"
- ipc: host
- environment:
- no_proxy: ${no_proxy}
- http_proxy: ${http_proxy}
- https_proxy: ${https_proxy}
- TEI_EMBEDDING_ENDPOINT: ${TEI_EMBEDDING_ENDPOINT}
- HF_TOKEN: ${HUGGINGFACEHUB_API_TOKEN}
- LOGFLAG: ${LOGFLAG}
- restart: unless-stopped
retriever:
image: ${REGISTRY:-opea}/retriever:${TAG:-latest}
container_name: retriever-redis-server
depends_on:
- redis-vector-db
ports:
- - "7000:7000"
+ - "7001:7000"
ipc: host
environment:
no_proxy: ${no_proxy}
http_proxy: ${http_proxy}
https_proxy: ${https_proxy}
- REDIS_URL: ${REDIS_URL}
+ REDIS_URL: redis://redis-vector-db:6379
REDIS_HOST: redis-vector-db
INDEX_NAME: ${INDEX_NAME}
- TEI_EMBEDDING_ENDPOINT: ${TEI_EMBEDDING_ENDPOINT}
+ TEI_EMBEDDING_ENDPOINT: http://tei-embedding-service:80
HUGGINGFACEHUB_API_TOKEN: ${HUGGINGFACEHUB_API_TOKEN}
LOGFLAG: ${LOGFLAG}
RETRIEVER_COMPONENT_NAME: "OPEA_RETRIEVER_REDIS"
@@ -116,27 +99,7 @@ services:
interval: 10s
timeout: 10s
retries: 60
- reranking:
- image: ${REGISTRY:-opea}/reranking:${TAG:-latest}
- container_name: reranking-tei-xeon-server
- depends_on:
- tei-reranking-service:
- condition: service_healthy
- ports:
- - "8000:8000"
- ipc: host
- environment:
- no_proxy: ${no_proxy}
- http_proxy: ${http_proxy}
- https_proxy: ${https_proxy}
- RERANK_TYPE: ${RERANK_TYPE}
- TEI_RERANKING_ENDPOINT: ${TEI_RERANKING_ENDPOINT}
- HUGGINGFACEHUB_API_TOKEN: ${HUGGINGFACEHUB_API_TOKEN}
- HF_HUB_DISABLE_PROGRESS_BARS: 1
- HF_HUB_ENABLE_HF_TRANSFER: 0
- LOGFLAG: ${LOGFLAG}
- restart: unless-stopped
- tgi_service:
+ tgi-service:
image: ghcr.io/huggingface/text-generation-inference:2.4.0-intel-cpu
container_name: tgi-service
ports:
@@ -158,26 +121,6 @@ services:
timeout: 10s
retries: 100
command: --model-id ${LLM_MODEL_ID} --cuda-graphs 0
- llm:
- image: ${REGISTRY:-opea}/llm-textgen:${TAG:-latest}
- container_name: llm-textgen-server
- depends_on:
- tgi_service:
- condition: service_healthy
- ports:
- - "9000:9000"
- ipc: host
- environment:
- no_proxy: ${no_proxy}
- http_proxy: ${http_proxy}
- https_proxy: ${https_proxy}
- LLM_ENDPOINT: ${TGI_LLM_ENDPOINT_CHATQNA}
- LLM_MODEL_ID: ${LLM_MODEL_ID}
- HUGGINGFACEHUB_API_TOKEN: ${HUGGINGFACEHUB_API_TOKEN}
- HF_HUB_DISABLE_PROGRESS_BARS: 1
- HF_HUB_ENABLE_HF_TRANSFER: 0
- LOGFLAG: ${LOGFLAG}
- restart: unless-stopped
chatqna-xeon-backend-server:
image: ${REGISTRY:-opea}/chatqna:${TAG:-latest}
container_name: chatqna-xeon-backend-server
@@ -192,27 +135,21 @@ services:
condition: service_started
tei-reranking-service:
condition: service_healthy
- tgi_service:
+ tgi-service:
condition: service_healthy
- embedding:
- condition: service_started
- reranking:
- condition: service_started
- llm:
- condition: service_started
ports:
- "8888:8888"
environment:
no_proxy: ${no_proxy}
https_proxy: ${https_proxy}
http_proxy: ${http_proxy}
- MEGA_SERVICE_HOST_IP: ${MEGA_SERVICE_HOST_IP}
- EMBEDDING_SERVER_HOST_IP: ${EMBEDDING_SERVICE_HOST_IP}
+ MEGA_SERVICE_HOST_IP: chatqna-xeon-backend-server
+ EMBEDDING_SERVER_HOST_IP: tei-embedding-service
EMBEDDING_SERVER_PORT: ${EMBEDDING_SERVER_PORT:-80}
- RETRIEVER_SERVICE_HOST_IP: ${RETRIEVER_SERVICE_HOST_IP}
- RERANK_SERVER_HOST_IP: ${RERANK_SERVICE_HOST_IP}
+ RETRIEVER_SERVICE_HOST_IP: retriever
+ RERANK_SERVER_HOST_IP: tei-reranking-service
RERANK_SERVER_PORT: ${RERANK_SERVER_PORT:-80}
- LLM_SERVER_HOST_IP: ${LLM_SERVICE_HOST_IP_CHATQNA}
+ LLM_SERVER_HOST_IP: tgi-service
LLM_SERVER_PORT: ${LLM_SERVER_PORT:-80}
LLM_MODEL: ${LLM_MODEL_ID}
LOGFLAG: ${LOGFLAG}
@@ -251,8 +188,8 @@ services:
no_proxy: ${no_proxy}
http_proxy: ${http_proxy}
https_proxy: ${https_proxy}
- LLM_ENDPOINT: ${TGI_LLM_ENDPOINT_CODEGEN}
- LLM_MODEL_ID: ${LLM_MODEL_ID}
+ LLM_ENDPOINT: http://tgi_service_codegen:80
+ LLM_MODEL_ID: ${LLM_MODEL_ID_CODEGEN}
HUGGINGFACEHUB_API_TOKEN: ${HUGGINGFACEHUB_API_TOKEN}
LOGFLAG: ${LOGFLAG}
restart: unless-stopped
@@ -260,7 +197,7 @@ services:
image: ${REGISTRY:-opea}/codegen:${TAG:-latest}
container_name: codegen-xeon-backend-server
depends_on:
- - llm
+ - llm_codegen
ports:
- "7778:7778"
environment:
@@ -268,33 +205,11 @@ services:
https_proxy: ${https_proxy}
http_proxy: ${http_proxy}
MEGA_SERVICE_HOST_IP: ${MEGA_SERVICE_HOST_IP}
- LLM_SERVICE_HOST_IP: ${LLM_SERVICE_HOST_IP_CODEGEN}
- LLM_SERVICE_PORT: ${LLM_SERVICE_HOST_PORT_CODEGEN}
+ LLM_SERVICE_HOST_IP: llm_codegen
+ LLM_SERVICE_PORT: ${LLM_SERVICE_HOST_PORT_CODEGEN:-9000}
LOGFLAG: ${LOGFLAG}
ipc: host
restart: always
- llm_faqgen:
- image: ${REGISTRY:-opea}/llm-faqgen:${TAG:-latest}
- container_name: llm-faqgen-server
- depends_on:
- tgi_service:
- condition: service_healthy
- ports:
- - "9002:9000"
- ipc: host
- environment:
- no_proxy: ${no_proxy}
- http_proxy: ${http_proxy}
- https_proxy: ${https_proxy}
- LLM_ENDPOINT: ${TGI_LLM_ENDPOINT_FAQGEN}
- HUGGINGFACEHUB_API_TOKEN: ${HUGGINGFACEHUB_API_TOKEN}
- LANGCHAIN_API_KEY: ${LANGCHAIN_API_KEY}
- LANGCHAIN_TRACING_V2: ${LANGCHAIN_TRACING_V2}
- LANGCHAIN_PROJECT: "opea-llm-service"
- LLM_MODEL_ID: ${LLM_MODEL_ID}
- FAQGen_COMPONENT_NAME: ${FAQGen_COMPONENT_NAME}
- LOGFLAG: ${LOGFLAG:-False}
- restart: unless-stopped
mongo:
image: mongo:7.0.11
container_name: mongodb
@@ -316,9 +231,9 @@ services:
http_proxy: ${http_proxy}
no_proxy: ${no_proxy}
https_proxy: ${https_proxy}
- MONGO_HOST: ${MONGO_HOST}
- MONGO_PORT: ${MONGO_PORT}
- COLLECTION_NAME: ${COLLECTION_NAME}
+ MONGO_HOST: ${MONGO_HOST:-mongo}
+ MONGO_PORT: ${MONGO_PORT:-27017}
+ COLLECTION_NAME: ${COLLECTION_NAME:-Conversations}
LOGFLAG: ${LOGFLAG}
restart: unless-stopped
@@ -332,9 +247,9 @@ services:
http_proxy: ${http_proxy}
https_proxy: ${https_proxy}
no_proxy: ${no_proxy}
- MONGO_HOST: ${MONGO_HOST}
- MONGO_PORT: ${MONGO_PORT}
- COLLECTION_NAME: ${PROMPT_COLLECTION_NAME}
+ MONGO_HOST: ${MONGO_HOST:-mongo}
+ MONGO_PORT: ${MONGO_PORT:-27017}
+ COLLECTION_NAME: ${PROMPT_COLLECTION_NAME:-prompt}
LOGFLAG: ${LOGFLAG}
restart: unless-stopped
keycloak:
@@ -367,10 +282,64 @@ services:
- APP_CHAT_HISTORY_GET_ENDPOINT=${CHAT_HISTORY_GET_ENDPOINT}
- APP_PROMPT_SERVICE_GET_ENDPOINT=${PROMPT_SERVICE_GET_ENDPOINT}
- APP_PROMPT_SERVICE_CREATE_ENDPOINT=${PROMPT_SERVICE_CREATE_ENDPOINT}
+ - APP_PROMPT_SERVICE_DELETE_ENDPOINT=${PROMPT_SERVICE_DELETE_ENDPOINT}
- APP_KEYCLOAK_SERVICE_ENDPOINT=${KEYCLOAK_SERVICE_ENDPOINT}
- LOGFLAG=${LOGFLAG}
ipc: host
restart: always
+
+ llm-docsum-tgi:
+ image: ${REGISTRY:-opea}/llm-docsum:${TAG:-latest}
+ container_name: docsum-xeon-llm-server
+ depends_on:
+ tgi-service:
+ condition: service_healthy
+ ports:
+ - ${LLM_PORT_DOCSUM:-9003}:9000
+ ipc: host
+ environment:
+ no_proxy: ${no_proxy}
+ http_proxy: ${http_proxy}
+ https_proxy: ${https_proxy}
+ LLM_ENDPOINT: http://tgi-service:80
+ LLM_MODEL_ID: ${LLM_MODEL_ID}
+ HUGGINGFACEHUB_API_TOKEN: ${HUGGINGFACEHUB_API_TOKEN}
+ MAX_INPUT_TOKENS: ${MAX_INPUT_TOKENS:-1024}
+ MAX_TOTAL_TOKENS: ${MAX_TOTAL_TOKENS:-2048}
+ DocSum_COMPONENT_NAME: ${DocSum_COMPONENT_NAME}
+ LOGFLAG: ${LOGFLAG:-False}
+ restart: unless-stopped
+ docsum-xeon-backend-server:
+ image: ${REGISTRY:-opea}/docsum:${TAG:-latest}
+ container_name: docsum-xeon-backend-server
+ depends_on:
+ - tgi-service
+ - llm-docsum-tgi
+ ports:
+ - "${BACKEND_SERVICE_PORT_DOCSUM:-8890}:8888"
+ environment:
+ - no_proxy=${no_proxy}
+ - https_proxy=${https_proxy}
+ - http_proxy=${http_proxy}
+ - MEGA_SERVICE_HOST_IP=${MEGA_SERVICE_HOST_IP}
+ - LLM_SERVICE_HOST_IP=llm-docsum-tgi
+ - LLM_SERVICE_PORT=9000
+ - ASR_SERVICE_HOST_IP=whisper
+ - OTEL_SDK_DISABLED=true
+ - OTEL_TRACES_EXPORTER=none
+ ipc: host
+ restart: always
+ whisper:
+ image: ${REGISTRY:-opea}/whisper:${TAG:-latest}
+ container_name: whisper-server
+ ports:
+ - "7066:7066"
+ ipc: host
+ environment:
+ no_proxy: ${no_proxy}
+ http_proxy: ${http_proxy}
+ https_proxy: ${https_proxy}
+ restart: unless-stopped
networks:
default:
driver: bridge
diff --git a/ProductivitySuite/docker_compose/intel/cpu/xeon/set_env.sh b/ProductivitySuite/docker_compose/intel/cpu/xeon/set_env.sh
old mode 100644
new mode 100755
index 0d0b8d6059..04d53e3639
--- a/ProductivitySuite/docker_compose/intel/cpu/xeon/set_env.sh
+++ b/ProductivitySuite/docker_compose/intel/cpu/xeon/set_env.sh
@@ -4,47 +4,28 @@ pushd "../../../../../" > /dev/null
source .set_env.sh
popd > /dev/null
-export MONGO_HOST=${host_ip}
-export MONGO_PORT=27017
export DB_NAME="opea"
-export COLLECTION_NAME="Conversations"
export EMBEDDING_MODEL_ID="BAAI/bge-base-en-v1.5"
export RERANK_MODEL_ID="BAAI/bge-reranker-base"
export LLM_MODEL_ID="Intel/neural-chat-7b-v3-3"
-export LLM_MODEL_ID_CODEGEN="meta-llama/CodeLlama-7b-hf"
-export TEI_EMBEDDING_ENDPOINT="http://${host_ip}:6006"
-export TEI_RERANKING_ENDPOINT="http://${host_ip}:8808"
-export TGI_LLM_ENDPOINT="http://${host_ip}:9009"
-export REDIS_URL="redis://${host_ip}:6379"
+export LLM_MODEL_ID_CODEGEN="Intel/neural-chat-7b-v3-3"
export INDEX_NAME="rag-redis"
export HUGGINGFACEHUB_API_TOKEN=${HUGGINGFACEHUB_API_TOKEN}
-export MEGA_SERVICE_HOST_IP=${host_ip}
-export EMBEDDING_SERVICE_HOST_IP=${host_ip}
-export RETRIEVER_SERVICE_HOST_IP=${host_ip}
-export RERANK_SERVICE_HOST_IP=${host_ip}
-export LLM_SERVICE_HOST_IP=${host_ip}
-export LLM_SERVICE_HOST_IP_DOCSUM=${host_ip}
-export LLM_SERVICE_HOST_IP_FAQGEN=${host_ip}
-export LLM_SERVICE_HOST_IP_CODEGEN=${host_ip}
-export LLM_SERVICE_HOST_IP_CHATQNA=${host_ip}
-export TGI_LLM_ENDPOINT_CHATQNA="http://${host_ip}:9009"
-export TGI_LLM_ENDPOINT_CODEGEN="http://${host_ip}:8028"
-export TGI_LLM_ENDPOINT_FAQGEN="http://${host_ip}:9009"
-export TGI_LLM_ENDPOINT_DOCSUM="http://${host_ip}:9009"
export BACKEND_SERVICE_ENDPOINT_CHATQNA="http://${host_ip}:8888/v1/chatqna"
-export DATAPREP_DELETE_FILE_ENDPOINT="http://${host_ip}:5000/v1/dataprep/delete"
+export DATAPREP_DELETE_FILE_ENDPOINT="http://${host_ip}:6007/v1/dataprep/delete"
export BACKEND_SERVICE_ENDPOINT_CODEGEN="http://${host_ip}:7778/v1/codegen"
export BACKEND_SERVICE_ENDPOINT_DOCSUM="http://${host_ip}:8890/v1/docsum"
-export DATAPREP_SERVICE_ENDPOINT="http://${host_ip}:5000/v1/dataprep/ingest"
-export DATAPREP_GET_FILE_ENDPOINT="http://${host_ip}:5000/v1/dataprep/get"
+export DATAPREP_SERVICE_ENDPOINT="http://${host_ip}:6007/v1/dataprep/ingest"
+export DATAPREP_GET_FILE_ENDPOINT="http://${host_ip}:6007/v1/dataprep/get"
export CHAT_HISTORY_CREATE_ENDPOINT="http://${host_ip}:6012/v1/chathistory/create"
export CHAT_HISTORY_CREATE_ENDPOINT="http://${host_ip}:6012/v1/chathistory/create"
export CHAT_HISTORY_DELETE_ENDPOINT="http://${host_ip}:6012/v1/chathistory/delete"
export CHAT_HISTORY_GET_ENDPOINT="http://${host_ip}:6012/v1/chathistory/get"
export PROMPT_SERVICE_GET_ENDPOINT="http://${host_ip}:6018/v1/prompt/get"
export PROMPT_SERVICE_CREATE_ENDPOINT="http://${host_ip}:6018/v1/prompt/create"
+export PROMPT_SERVICE_DELETE_ENDPOINT="http://${host_ip}:6018/v1/prompt/delete"
export KEYCLOAK_SERVICE_ENDPOINT="http://${host_ip}:8080"
-export LLM_SERVICE_HOST_PORT_FAQGEN=9002
-export LLM_SERVICE_HOST_PORT_CODEGEN=9001
-export LLM_SERVICE_HOST_PORT_DOCSUM=9003
-export PROMPT_COLLECTION_NAME="prompt"
+export DocSum_COMPONENT_NAME="OpeaDocSumTgi"
+
+#Set no proxy
+export no_proxy="$no_proxy,tgi_service_codegen,llm_codegen,tei-embedding-service,tei-reranking-service,chatqna-xeon-backend-server,retriever,tgi-service,redis-vector-db,whisper,llm-docsum-tgi,docsum-xeon-backend-server,mongo,codegen"
diff --git a/ProductivitySuite/docker_image_build/build.yaml b/ProductivitySuite/docker_image_build/build.yaml
index 34a3296c12..0401d6cfd0 100644
--- a/ProductivitySuite/docker_image_build/build.yaml
+++ b/ProductivitySuite/docker_image_build/build.yaml
@@ -77,3 +77,27 @@ services:
context: GenAIComps
dockerfile: comps/llms/src/faq-generation/Dockerfile
image: ${REGISTRY:-opea}/llm-faqgen:${TAG:-latest}
+ docsum:
+ build:
+ args:
+ http_proxy: ${http_proxy}
+ https_proxy: ${https_proxy}
+ no_proxy: ${no_proxy}
+ context: ../../DocSum/
+ dockerfile: ./Dockerfile
+ image: ${REGISTRY:-opea}/docsum:${TAG:-latest}
+ llm-docsum:
+ build:
+ context: GenAIComps
+ dockerfile: comps/llms/src/doc-summarization/Dockerfile
+ extends: docsum
+ image: ${REGISTRY:-opea}/llm-docsum:${TAG:-latest}
+ whisper:
+ build:
+ args:
+ http_proxy: ${http_proxy}
+ https_proxy: ${https_proxy}
+ context: GenAIComps
+ dockerfile: comps/third_parties/whisper/src/Dockerfile
+ extends: docsum
+ image: ${REGISTRY:-opea}/whisper:${TAG:-latest}
diff --git a/ProductivitySuite/tests/test_compose_on_xeon.sh b/ProductivitySuite/tests/test_compose_on_xeon.sh
index 7a5ba4be4f..34d9a96691 100755
--- a/ProductivitySuite/tests/test_compose_on_xeon.sh
+++ b/ProductivitySuite/tests/test_compose_on_xeon.sh
@@ -30,58 +30,31 @@ function build_docker_images() {
function start_services() {
cd $WORKPATH/docker_compose/intel/cpu/xeon/
+ export DB_NAME="opea"
export EMBEDDING_MODEL_ID="BAAI/bge-base-en-v1.5"
- export RERANK_TYPE="tei"
export RERANK_MODEL_ID="BAAI/bge-reranker-base"
export LLM_MODEL_ID="Intel/neural-chat-7b-v3-3"
export LLM_MODEL_ID_CODEGEN="Intel/neural-chat-7b-v3-3"
- export DATAPREP_TYPE="redis"
- export RETRIEVER_TYPE="redis"
- export TEI_EMBEDDING_ENDPOINT="http://${ip_address}:6006"
- export TEI_RERANKING_ENDPOINT="http://${ip_address}:8808"
- export TGI_LLM_ENDPOINT="http://${ip_address}:9009"
- export REDIS_URL="redis://${ip_address}:6379"
- export REDIS_HOST=${ip_address}
export INDEX_NAME="rag-redis"
export HUGGINGFACEHUB_API_TOKEN=${HUGGINGFACEHUB_API_TOKEN}
- export MEGA_SERVICE_HOST_IP=${ip_address}
- export EMBEDDING_SERVICE_HOST_IP=${ip_address}
- export RETRIEVER_SERVICE_HOST_IP=${ip_address}
- export RERANK_SERVICE_HOST_IP=${ip_address}
- export LLM_SERVICE_HOST_IP=${ip_address}
- export LLM_SERVICE_HOST_IP_DOCSUM=${ip_address}
- export LLM_SERVICE_HOST_IP_FAQGEN=${ip_address}
- export LLM_SERVICE_HOST_IP_CODEGEN=${ip_address}
- export LLM_SERVICE_HOST_IP_CHATQNA=${ip_address}
- export TGI_LLM_ENDPOINT_CHATQNA="http://${ip_address}:9009"
- export TGI_LLM_ENDPOINT_CODEGEN="http://${ip_address}:8028"
- export TGI_LLM_ENDPOINT_FAQGEN="http://${ip_address}:9009"
- export TGI_LLM_ENDPOINT_DOCSUM="http://${ip_address}:9009"
export BACKEND_SERVICE_ENDPOINT_CHATQNA="http://${ip_address}:8888/v1/chatqna"
- export DATAPREP_DELETE_FILE_ENDPOINT="http://${ip_address}:5000/v1/dataprep/delete"
+ export DATAPREP_DELETE_FILE_ENDPOINT="http://${ip_address}:6007/v1/dataprep/delete"
export BACKEND_SERVICE_ENDPOINT_CODEGEN="http://${ip_address}:7778/v1/codegen"
- export DATAPREP_SERVICE_ENDPOINT="http://${ip_address}:5000/v1/dataprep/ingest"
- export DATAPREP_GET_FILE_ENDPOINT="http://${ip_address}:5000/v1/dataprep/get"
+ export BACKEND_SERVICE_ENDPOINT_DOCSUM="http://${ip_address}:8890/v1/docsum"
+ export DATAPREP_SERVICE_ENDPOINT="http://${ip_address}:6007/v1/dataprep/ingest"
+ export DATAPREP_GET_FILE_ENDPOINT="http://${ip_address}:6007/v1/dataprep/get"
export CHAT_HISTORY_CREATE_ENDPOINT="http://${ip_address}:6012/v1/chathistory/create"
export CHAT_HISTORY_CREATE_ENDPOINT="http://${ip_address}:6012/v1/chathistory/create"
export CHAT_HISTORY_DELETE_ENDPOINT="http://${ip_address}:6012/v1/chathistory/delete"
export CHAT_HISTORY_GET_ENDPOINT="http://${ip_address}:6012/v1/chathistory/get"
export PROMPT_SERVICE_GET_ENDPOINT="http://${ip_address}:6018/v1/prompt/get"
export PROMPT_SERVICE_CREATE_ENDPOINT="http://${ip_address}:6018/v1/prompt/create"
+ export PROMPT_SERVICE_DELETE_ENDPOINT="http://${ip_address}:6018/v1/prompt/delete"
export KEYCLOAK_SERVICE_ENDPOINT="http://${ip_address}:8080"
- export MONGO_HOST=${ip_address}
- export MONGO_PORT=27017
- export DB_NAME="opea"
- export COLLECTION_NAME="Conversations"
- export LLM_SERVICE_HOST_PORT_FAQGEN=9002
- export LLM_SERVICE_HOST_PORT_CODEGEN=9001
- export RERANK_SERVER_PORT=8808
- export EMBEDDING_SERVER_PORT=6006
- export LLM_SERVER_PORT=9009
- export PROMPT_COLLECTION_NAME="prompt"
+ export DocSum_COMPONENT_NAME="OpeaDocSumTgi"
export host_ip=${ip_address}
- export FAQGen_COMPONENT_NAME="OpeaFaqGenTgi"
export LOGFLAG=True
+ export no_proxy="$no_proxy,tgi_service_codegen,llm_codegen,tei-embedding-service,tei-reranking-service,chatqna-xeon-backend-server,retriever,tgi-service,redis-vector-db,whisper,llm-docsum-tgi,docsum-xeon-backend-server,mongo,codegen"
# Start Docker Containers
docker compose up -d > ${LOG_PATH}/start_services_with_compose.log
@@ -141,34 +114,6 @@ function validate_service() {
sleep 1s
}
-function validate_faqgen() {
- local URL="$1"
- local EXPECTED_RESULT="$2"
- local SERVICE_NAME="$3"
- local DOCKER_NAME="$4"
- local INPUT_DATA="$5"
-
- local HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST -d "$INPUT_DATA" -H 'Content-Type: application/json' "$URL")
- if [ "$HTTP_STATUS" -eq 200 ]; then
- echo "[ $SERVICE_NAME ] HTTP status is 200. Checking content..."
-
- local CONTENT=$(curl -s -X POST -d "$INPUT_DATA" -H 'Content-Type: application/json' "$URL" | tee ${LOG_PATH}/${SERVICE_NAME}.log)
-
- if echo "$CONTENT" | grep -q "$EXPECTED_RESULT"; then
- echo "[ $SERVICE_NAME ] Content is as expected."
- else
- echo "[ $SERVICE_NAME ] Content does not match the expected result: $CONTENT"
- docker logs ${DOCKER_NAME} >> ${LOG_PATH}/${SERVICE_NAME}.log
- exit 1
- fi
- else
- echo "[ $SERVICE_NAME ] HTTP status is not 200. Received status was $HTTP_STATUS"
- docker logs ${DOCKER_NAME} >> ${LOG_PATH}/${SERVICE_NAME}.log
- exit 1
- fi
- sleep 1s
-}
-
function validate_microservices() {
# Check if the microservices are running correctly.
@@ -180,14 +125,6 @@ function validate_microservices() {
"tei-embedding-server" \
'{"inputs":"What is Deep Learning?"}'
- # embedding microservice
- validate_service \
- "${ip_address}:6000/v1/embeddings" \
- '"embedding":[' \
- "embedding-microservice" \
- "embedding-server" \
- '{"input":"What is Deep Learning?"}'
-
sleep 1m # retrieval can't curl as expected, try to wait for more time
# test /v1/dataprep/delete
@@ -222,7 +159,7 @@ function validate_microservices() {
# retrieval microservice
test_embedding=$(python3 -c "import random; embedding = [random.uniform(-1, 1) for _ in range(768)]; print(embedding)")
validate_service \
- "${ip_address}:7000/v1/retrieval" \
+ "${ip_address}:7001/v1/retrieval" \
"retrieved_docs" \
"retrieval-microservice" \
"retriever-redis-server" \
@@ -236,14 +173,6 @@ function validate_microservices() {
"tei-reranking-server" \
'{"query":"What is Deep Learning?", "texts": ["Deep Learning is not...", "Deep learning is..."]}'
- # rerank microservice
- validate_service \
- "${ip_address}:8000/v1/reranking" \
- "Deep learning is..." \
- "rerank-microservice" \
- "reranking-tei-xeon-server" \
- '{"initial_query":"What is Deep Learning?", "retrieved_docs": [{"text":"Deep Learning is not..."}, {"text":"Deep learning is..."}]}'
-
# tgi for llm service
validate_service \
"${ip_address}:9009/generate" \
@@ -252,21 +181,6 @@ function validate_microservices() {
"tgi-service" \
'{"inputs":"What is Deep Learning?","parameters":{"max_new_tokens":17, "do_sample": true}}'
- # ChatQnA llm microservice
- validate_service \
- "${ip_address}:9000/v1/chat/completions" \
- "data: " \
- "llm-microservice" \
- "llm-textgen-server" \
- '{"query":"What is Deep Learning?"}'
-
- # FAQGen llm microservice
- validate_faqgen \
- "${ip_address}:9002/v1/faqgen" \
- "text" \
- "llm_faqgen" \
- "llm-faqgen-server" \
- '{"messages":"Text Embeddings Inference (TEI) is a toolkit for deploying and serving open source text embeddings and sequence classification models. TEI enables high-performance extraction for the most popular models, including FlagEmbedding, Ember, GTE and E5."}'
# CodeGen llm microservice
validate_service \
diff --git a/ProductivitySuite/ui/react/.env.production b/ProductivitySuite/ui/react/.env.production
index f881c388ce..a063cc5142 100644
--- a/ProductivitySuite/ui/react/.env.production
+++ b/ProductivitySuite/ui/react/.env.production
@@ -1,7 +1,6 @@
VITE_BACKEND_SERVICE_ENDPOINT_CHATQNA=APP_BACKEND_SERVICE_ENDPOINT_CHATQNA
VITE_BACKEND_SERVICE_ENDPOINT_CODEGEN=APP_BACKEND_SERVICE_ENDPOINT_CODEGEN
VITE_BACKEND_SERVICE_ENDPOINT_DOCSUM=APP_BACKEND_SERVICE_ENDPOINT_DOCSUM
-VITE_BACKEND_SERVICE_ENDPOINT_FAQGEN=APP_BACKEND_SERVICE_ENDPOINT_FAQGEN
VITE_KEYCLOAK_SERVICE_ENDPOINT=APP_KEYCLOAK_SERVICE_ENDPOINT
@@ -14,3 +13,4 @@ VITE_CHAT_HISTORY_GET_ENDPOINT=APP_CHAT_HISTORY_GET_ENDPOINT
VITE_CHAT_HISTORY_DELETE_ENDPOINT=APP_CHAT_HISTORY_DELETE_ENDPOINT
VITE_PROMPT_SERVICE_GET_ENDPOINT=APP_PROMPT_SERVICE_GET_ENDPOINT
VITE_PROMPT_SERVICE_CREATE_ENDPOINT=APP_PROMPT_SERVICE_CREATE_ENDPOINT
+VITE_PROMPT_SERVICE_DELETE_ENDPOINT=APP_PROMPT_SERVICE_DELETE_ENDPOINT
diff --git a/ProductivitySuite/ui/react/README.md b/ProductivitySuite/ui/react/README.md
index c7b6d0b931..469a4c70fe 100644
--- a/ProductivitySuite/ui/react/README.md
+++ b/ProductivitySuite/ui/react/README.md
@@ -41,24 +41,13 @@ Here're some of the project's features:
### DOC SUMMARY
-- Summarizing Uploaded Files: Upload files from their local device, then click 'Generate Summary' to summarize the content of the uploaded file. The summary will be displayed on the 'Summary' box.
-- Summarizing Text via Pasting: Paste the text to be summarized into the text box, then click 'Generate Summary' to produce a condensed summary of the content, which will be displayed in the 'Summary' box on the right.
+- Summarizing Uploaded Files: Upload files from their local device, then click up arrow button to summarize the content of the uploaded file.
+- Summarizing Text via Pasting: Paste the text to be summarized into the text box, then click up arrow button to produce a condensed summary of the content, which will be displayed in the new page
- Scroll to Bottom: The summarized content will automatically scroll to the bottom.
#### Screen Shot
-
-
-
-### FAQ Generator
-
-- Generate FAQs from Text via Pasting: Paste the text to into the text box, then click 'Generate FAQ' to produce a condensed FAQ of the content, which will be displayed in the 'FAQ' box below.
-
-- Generate FAQs from Text via txt file Upload: Upload the file in the Upload bar, then click 'Generate FAQ' to produce a condensed FAQ of the content, which will be displayed in the 'FAQ' box below.
-
-#### Screen Shot
-
-
+
## 🛠️ Get it Running
@@ -68,22 +57,43 @@ Here're some of the project's features:
3. create a .env file and add the following variables and values.
```
- VITE_BACKEND_SERVICE_ENDPOINT_CHATQNA=''
- VITE_BACKEND_SERVICE_ENDPOINT_CODEGEN=''
- VITE_BACKEND_SERVICE_ENDPOINT_DOCSUM=''
- VITE_BACKEND_SERVICE_ENDPOINT_FAQGEN=''
- VITE_KEYCLOAK_SERVICE_ENDPOINT=''
- VITE_DATAPREP_SERVICE_ENDPOINT=''
- VITE_DATAPREP_GET_FILE_ENDPOINT=''
- VITE_DATAPREP_DELETE_FILE_ENDPOINT=''
- VITE_CHAT_HISTORY_CREATE_ENDPOINT=''
- VITE_CHAT_HISTORY_GET_ENDPOINT=''
- VITE_CHAT_HISTORY_DELETE_ENDPOINT=''
- VITE_PROMPT_SERVICE_GET_ENDPOINT=''
- VITE_PROMPT_SERVICE_CREATE_ENDPOINT=''
+ VITE_BACKEND_SERVICE_ENDPOINT_CHATQNA=""
+ VITE_DATAPREP_DELETE_FILE_ENDPOINT=""
+ VITE_BACKEND_SERVICE_ENDPOINT_CODEGEN=""
+ VITE_BACKEND_SERVICE_ENDPOINT_DOCSUM=""
+ VITE_DATAPREP_SERVICE_ENDPOINT=""
+ VITE_DATAPREP_GET_FILE_ENDPOINT=""
+ VITE_CHAT_HISTORY_CREATE_ENDPOINT=""
+ VITE_CHAT_HISTORY_DELETE_ENDPOINT=""
+ VITE_CHAT_HISTORY_GET_ENDPOINT=""
+ VITE_PROMPT_SERVICE_GET_ENDPOINT=""
+ VITE_PROMPT_SERVICE_CREATE_ENDPOINT=""
+ VITE_PROMPT_SERVICE_DELETE_ENDPOINT=""
+ VITE_KEYCLOAK_SERVICE_ENDPOINT=""
+ VITE_PROMPT_COLLECTION_NAME="prompt"
+ ```
+4. There is models_config.json file under public folder. It is in the below format. The types key in the below json array shows that the model is used for following types.
+
```
-4. Execute `npm install` to install the corresponding dependencies.
+ [
+
+ {
+ "model_name": "Intel/neural-chat-7b-v3-3",
+ "displayName": "Intel Neural Chat",
+ "minToken": 100,
+ "maxToken": 2000,
+ "types": [
+ "chat",
+ "summary",
+ "code"
+ ]
+ }
+
+ ]
+ ```
+
+5. Execute `npm install` to install the corresponding dependencies.
-5. Execute `npm run dev`
+6. Execute `npm run dev`
-6. open http://localhost:5174 in browser to the see the UI
+7. open http://localhost:5174 in browser to the see the UI
diff --git a/ProductivitySuite/ui/react/index.html b/ProductivitySuite/ui/react/index.html
index fbe87e0fd5..b048d29d77 100644
--- a/ProductivitySuite/ui/react/index.html
+++ b/ProductivitySuite/ui/react/index.html
@@ -1,18 +1,29 @@
-
-
-
- Conversations UI
+
+
+
+
+
+
+
+
+
+
+ ProductivitySuite UI
+ You need to enable JavaScript to run this app.
-
+
diff --git a/ProductivitySuite/ui/react/package.json b/ProductivitySuite/ui/react/package.json
index 57e384bfad..7b2fc4f007 100644
--- a/ProductivitySuite/ui/react/package.json
+++ b/ProductivitySuite/ui/react/package.json
@@ -1,56 +1,92 @@
{
- "name": "ui",
+ "name": "ProductivitySuite",
+ "version": "0.0.1",
+ "description": "ProductivitySuite UI - OPEA",
+ "homepage": ".",
"private": true,
- "version": "0.0.0",
"type": "module",
+ "engines": {
+ "node": "20.x"
+ },
"scripts": {
- "dev": "vite",
- "build": "tsc && vite build",
- "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
- "preview": "vite preview",
+ "dev": "vite --port 5173",
+ "build": "vite build",
+ "preview": "vite preview --port 5173",
+ "prettier:write": "prettier --write .",
"test": "vitest run"
},
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
"dependencies": {
- "@mantine/core": "^7.11.1",
- "@mantine/dropzone": "^7.11.1",
- "@mantine/hooks": "^7.11.1",
- "@mantine/notifications": "^7.10.2",
"@microsoft/fetch-event-source": "^2.0.1",
+ "@mui/icons-material": "^6.4.1",
+ "@mui/material": "^6.4.1",
+ "@mui/styled-engine-sc": "^6.4.0",
"@react-keycloak/web": "^3.4.0",
- "@reduxjs/toolkit": "^2.2.5",
- "@tabler/icons-react": "3.27.1",
- "axios": "^1.7.2",
+ "@reduxjs/toolkit": "^2.5.0",
+ "axios": "^1.7.9",
"keycloak-js": "^25.0.2",
- "luxon": "^3.4.4",
+ "notistack": "^3.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-markdown": "^9.0.1",
- "react-redux": "^9.1.2",
- "react-router-dom": "^6.25.1",
- "react-syntax-highlighter": "^15.5.0",
+ "react-markdown": "^8.0.7",
+ "react-redux": "^9.2.0",
+ "react-router-dom": "^7.1.1",
+ "react-syntax-highlighter": "^15.6.1",
+ "remark-breaks": "^4.0.0",
"remark-frontmatter": "^5.0.0",
- "remark-gfm": "^4.0.0"
+ "remark-gfm": "^3.0.1",
+ "styled-components": "^6.1.14"
},
"devDependencies": {
- "@testing-library/react": "^16.0.0",
- "@types/luxon": "^3.4.2",
- "@types/node": "^20.12.12",
- "@types/react": "^18.2.66",
- "@types/react-dom": "^18.2.22",
+ "@auth/express": "^0.8.4",
+ "@babel/plugin-transform-private-property-in-object": "^7.25.9",
+ "@electron-toolkit/preload": "^3.0.1",
+ "@mui/types": "^7.2.21",
+ "@rollup/plugin-terser": "^0.4.4",
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^14.4.3",
+ "@types/electron-devtools-installer": "^2.2.2",
+ "@types/jest": "^29.4.0",
+ "@types/node": "^18.13.0",
+ "@types/react": "^19.0.2",
+ "@types/react-dom": "^19.0.2",
"@types/react-syntax-highlighter": "^15.5.13",
- "@typescript-eslint/eslint-plugin": "^7.2.0",
- "@typescript-eslint/parser": "^7.2.0",
- "@vitejs/plugin-react": "^4.2.1",
- "eslint": "^8.57.0",
- "eslint-plugin-react-hooks": "^4.6.0",
- "eslint-plugin-react-refresh": "^0.4.6",
- "jsdom": "^24.1.0",
- "postcss": "^8.4.38",
- "postcss-preset-mantine": "^1.15.0",
- "postcss-simple-vars": "^7.0.1",
- "sass": "1.64.2",
- "typescript": "^5.2.2",
- "vite": "^5.2.13",
- "vitest": "^1.6.0"
+ "@vitejs/plugin-react": "^4.3.4",
+ "concurrently": "^7.6.0",
+ "cors": "^2.8.5",
+ "cross-env": "^7.0.3",
+ "dotenv": "^16.4.7",
+ "express": "^4.21.2",
+ "nodemon": "^3.1.9",
+ "prettier": "^3.5.3",
+ "rollup-plugin-visualizer": "^5.14.0",
+ "sass": "^1.83.1",
+ "typescript": "^5.7.3",
+ "vite": "^5.3.1",
+ "vite-plugin-compression": "^0.5.1",
+ "vite-plugin-mkcert": "^1.17.6",
+ "vite-plugin-sass-dts": "^1.3.30",
+ "vite-plugin-svgr": "^4.3.0",
+ "vitest": "^3.1.2",
+ "wait-on": "^7.0.1",
+ "webpack-bundle-analyzer": "^4.10.2"
}
}
diff --git a/ProductivitySuite/ui/react/postcss.config.cjs b/ProductivitySuite/ui/react/postcss.config.cjs
deleted file mode 100644
index e817f567be..0000000000
--- a/ProductivitySuite/ui/react/postcss.config.cjs
+++ /dev/null
@@ -1,14 +0,0 @@
-module.exports = {
- plugins: {
- "postcss-preset-mantine": {},
- "postcss-simple-vars": {
- variables: {
- "mantine-breakpoint-xs": "36em",
- "mantine-breakpoint-sm": "48em",
- "mantine-breakpoint-md": "62em",
- "mantine-breakpoint-lg": "75em",
- "mantine-breakpoint-xl": "88em",
- },
- },
- },
-};
diff --git a/ProductivitySuite/ui/react/public/favicon.ico b/ProductivitySuite/ui/react/public/favicon.ico
new file mode 100644
index 0000000000..c2c86b859e
Binary files /dev/null and b/ProductivitySuite/ui/react/public/favicon.ico differ
diff --git a/ProductivitySuite/ui/react/public/logo192.png b/ProductivitySuite/ui/react/public/logo192.png
new file mode 100644
index 0000000000..fa313abf53
Binary files /dev/null and b/ProductivitySuite/ui/react/public/logo192.png differ
diff --git a/ProductivitySuite/ui/react/public/logo512.png b/ProductivitySuite/ui/react/public/logo512.png
new file mode 100644
index 0000000000..bd5d4b5e23
Binary files /dev/null and b/ProductivitySuite/ui/react/public/logo512.png differ
diff --git a/ProductivitySuite/ui/react/public/manifest.json b/ProductivitySuite/ui/react/public/manifest.json
new file mode 100644
index 0000000000..a3295c9a2a
--- /dev/null
+++ b/ProductivitySuite/ui/react/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "ProductivitySuite",
+ "name": "ProductivitySuite UI",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/ProductivitySuite/ui/react/public/model_configs.json b/ProductivitySuite/ui/react/public/model_configs.json
new file mode 100644
index 0000000000..cea98dc7d3
--- /dev/null
+++ b/ProductivitySuite/ui/react/public/model_configs.json
@@ -0,0 +1,9 @@
+[
+ {
+ "model_name": "Intel/neural-chat-7b-v3-3",
+ "displayName": "Intel Neural Chat",
+ "minToken": 100,
+ "maxToken": 2000,
+ "types": ["chat", "summary", "code"]
+ }
+]
diff --git a/ProductivitySuite/ui/react/public/robots.txt b/ProductivitySuite/ui/react/public/robots.txt
new file mode 100644
index 0000000000..01b0f9a107
--- /dev/null
+++ b/ProductivitySuite/ui/react/public/robots.txt
@@ -0,0 +1,2 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
diff --git a/ProductivitySuite/ui/react/src/App.scss b/ProductivitySuite/ui/react/src/App.scss
index 187764a179..1317587986 100644
--- a/ProductivitySuite/ui/react/src/App.scss
+++ b/ProductivitySuite/ui/react/src/App.scss
@@ -1,42 +1 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-@import "./styles/styles";
-
-.root {
- @include flex(row, nowrap, flex-start, flex-start);
-}
-
-.layout-wrapper {
- @include absolutes;
-
- display: grid;
-
- width: 100%;
- height: 100%;
-
- grid-template-columns: 80px auto;
- grid-template-rows: 1fr;
-}
-
-/* ===== Scrollbar CSS ===== */
-/* Firefox */
-* {
- scrollbar-width: thin;
- scrollbar-color: #d6d6d6 #ffffff;
-}
-
-/* Chrome, Edge, and Safari */
-*::-webkit-scrollbar {
- width: 8px;
-}
-
-*::-webkit-scrollbar-track {
- background: #ffffff;
-}
-
-*::-webkit-scrollbar-thumb {
- background-color: #d6d6d6;
- border-radius: 16px;
- border: 4px double #dedede;
-}
+// Post javascript styles
diff --git a/ProductivitySuite/ui/react/src/App.tsx b/ProductivitySuite/ui/react/src/App.tsx
index c12ee1d8fa..970cbc58c6 100644
--- a/ProductivitySuite/ui/react/src/App.tsx
+++ b/ProductivitySuite/ui/react/src/App.tsx
@@ -1,68 +1,195 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-import "./App.scss"
-import {MantineProvider } from "@mantine/core"
-import '@mantine/notifications/styles.css';
-import { SideNavbar, SidebarNavList } from "./components/sidebar/sidebar"
-import { IconMessages, IconFileTextAi, IconCode, IconFileInfo, IconDatabaseCog } from "@tabler/icons-react"
-import Conversation from "./components/Conversation/Conversation"
-import { Notifications } from '@mantine/notifications';
-import { BrowserRouter, Route, Routes } from "react-router-dom";
-import CodeGen from "./components/CodeGen/CodeGen";
-import DocSum from "./components/DocSum/DocSum";
-import FaqGen from "./components/FaqGen/FaqGen";
+import "./App.scss";
+
+import React, { Suspense, useEffect } from "react";
+import { BrowserRouter, Routes, Route } from "react-router-dom";
+import ProtectedRoute from "@layouts/ProtectedRoute/ProtectedRoute";
+
import { useKeycloak } from "@react-keycloak/web";
-import DataSource from "./components/Conversation/DataSource";
-import { useAppDispatch } from "./redux/store";
-import { setUser } from "./redux/User/userSlice";
-import { useEffect } from "react";
-
-const title = "Chat QnA"
-const navList: SidebarNavList = [
- { icon: IconMessages, label: "Chat Qna", path: "/", children: },
- { icon: IconCode, label: "CodeGen", path: "/codegen", children: },
- { icon: IconFileTextAi, label: "DocSum", path: "/docsum", children: },
- { icon: IconFileInfo, label: "FaqGen", path: "/faqgen", children: },
- { icon: IconDatabaseCog, label: "Data Management", path: "/data-management", children: }
-]
-
-function App() {
+import { setUser, userSelector } from "@redux/User/userSlice";
+
+import MainLayout from "@layouts/Main/MainLayout";
+import MinimalLayout from "@layouts/Minimal/MinimalLayout";
+import Notification from "@components/Notification/Notification";
+import { Box, styled, Typography } from "@mui/material";
+import { AtomAnimation, AtomIcon } from "@icons/Atom";
+
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import {
+ conversationSelector,
+ getAllConversations,
+ /*getStoredPromptSettings,*/ getSupportedModels,
+ getSupportedUseCases,
+} from "@redux/Conversation/ConversationSlice";
+import { getPrompts } from "@redux/Prompt/PromptSlice";
+
+import Home from "@pages/Home/Home";
+import ChatView from "@pages/Chat/ChatView";
+
+const HistoryView = React.lazy(() => import("@pages/History/HistoryView"));
+const DataSourceManagement = React.lazy(
+ () => import("@pages/DataSource/DataSourceManagement"),
+);
+
+const LoadingBox = styled(Box)({
+ display: "flex",
+ flexDirection: "column",
+ justifyContent: "center",
+ alignItems: "center",
+ height: "100vh",
+ width: "100vw",
+});
+
+const App = () => {
const { keycloak } = useKeycloak();
- const dispatch = useAppDispatch()
- useEffect(()=>{
- dispatch(setUser(keycloak?.idTokenParsed?.preferred_username))
- },[keycloak.idTokenParsed])
-
- return (
- <>
-
- {!keycloak.authenticated ? (
- "redirecting to sso ..."
- ) : (
-
-
-
-
-
-
-
- {navList.map(tab => {
- return ( )
- })}
-
-
-
-
-
-
-
- )}
-
-
- >
- )
-
-}
-
-export default App
+ const dispatch = useAppDispatch();
+
+ const { name } = useAppSelector(userSelector);
+ const { useCase } = useAppSelector(conversationSelector);
+
+ useEffect(() => {
+ //TODO: get role from keyCloack scope, defaulting to Admin
+ dispatch(
+ setUser({
+ name: keycloak?.idTokenParsed?.preferred_username,
+ isAuthenticated: true,
+ role: "Admin",
+ }),
+ );
+ }, [keycloak.idTokenParsed]);
+
+ const initSettings = () => {
+ if (keycloak.authenticated) {
+ dispatch(getSupportedUseCases());
+ dispatch(getSupportedModels());
+ dispatch(getPrompts());
+ }
+ };
+
+ useEffect(() => {
+ if (keycloak.authenticated) initSettings();
+ }, [keycloak.authenticated]);
+
+ //TODO: on potential useCase change get different conversation data
+ useEffect(() => {
+ if (keycloak.authenticated && useCase) {
+ dispatch(getAllConversations({ user: name, useCase: useCase }));
+ // dispatch(getSharedConversations({ usecase: selectedUseCase.use_case }));
+ }
+ }, [useCase, name]);
+
+ return !keycloak.authenticated ? (
+
+
+ redirecting to sso ...
+
+ ) : (
+
+
+
+
+ }
+ >
+
+ {/* Routes wrapped in MainLayout */}
+ }>
+
+ }
+ />
+
+
+ }>
+
+ }
+ />
+
+
+ }>
+ (
+
+ )}
+ />
+ }
+ />
+ (
+
+ )}
+ />
+ }
+ />
+
+
+ }>
+
+ }
+ />
+
+ }
+ />
+
+ }
+ />
+
+ }
+ />
+
+
+ {/* Routes not wrapped in MainLayout */}
+ }>
+ {/* } /> */}
+
+
+
+
+
+ );
+};
+
+export default App;
diff --git a/ProductivitySuite/ui/react/src/assets/icons/moon.svg b/ProductivitySuite/ui/react/src/assets/icons/moon.svg
new file mode 100644
index 0000000000..a9f36a8321
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/assets/icons/moon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ProductivitySuite/ui/react/src/assets/icons/sun.svg b/ProductivitySuite/ui/react/src/assets/icons/sun.svg
new file mode 100644
index 0000000000..510dad63bf
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/assets/icons/sun.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/ProductivitySuite/ui/react/src/assets/opea-icon-black.svg b/ProductivitySuite/ui/react/src/assets/opea-icon-black.svg
deleted file mode 100644
index 5c96dc7622..0000000000
--- a/ProductivitySuite/ui/react/src/assets/opea-icon-black.svg
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/ProductivitySuite/ui/react/src/assets/opea-icon-color.svg b/ProductivitySuite/ui/react/src/assets/opea-icon-color.svg
deleted file mode 100644
index 790151171e..0000000000
--- a/ProductivitySuite/ui/react/src/assets/opea-icon-color.svg
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/ProductivitySuite/ui/react/src/assets/react.svg b/ProductivitySuite/ui/react/src/assets/react.svg
deleted file mode 100644
index 8e0e0f15c0..0000000000
--- a/ProductivitySuite/ui/react/src/assets/react.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/ProductivitySuite/ui/react/src/common/client.ts b/ProductivitySuite/ui/react/src/common/client.ts
deleted file mode 100644
index 7512f73e33..0000000000
--- a/ProductivitySuite/ui/react/src/common/client.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-import axios from "axios";
-
-//add iterceptors to add any request headers
-
-export default axios;
diff --git a/ProductivitySuite/ui/react/src/components/Chat_Assistant/ChatAssistant.module.scss b/ProductivitySuite/ui/react/src/components/Chat_Assistant/ChatAssistant.module.scss
new file mode 100644
index 0000000000..ac8428e853
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Chat_Assistant/ChatAssistant.module.scss
@@ -0,0 +1,68 @@
+.chatReply {
+ display: flex;
+ flex-direction: row;
+
+ .icon {
+ padding-right: 1rem;
+
+ svg {
+ width: 24px;
+ height: 24px;
+ }
+ }
+}
+
+.ellipsis {
+ position: relative;
+
+ span {
+ position: relative;
+ animation: dance 1.5s infinite ease-in-out;
+ }
+
+ span:nth-child(1) {
+ margin-left: 2px;
+ animation-delay: 0s;
+ }
+
+ span:nth-child(2) {
+ animation-delay: 0.3s;
+ }
+
+ span:nth-child(3) {
+ animation-delay: 0.6s;
+ }
+}
+
+@keyframes dance {
+ 0%,
+ 100% {
+ bottom: 0;
+ opacity: 1;
+ }
+ 20% {
+ bottom: 5px;
+ opacity: 0.7;
+ }
+ 40% {
+ bottom: 0;
+ opacity: 1;
+ }
+}
+
+.textedit {
+ width: 100%;
+ min-height: 50px;
+ padding: 1rem;
+}
+
+.chatPrompt {
+ width: 100%;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ word-break: break-word;
+
+ p:first-of-type {
+ margin-top: 0;
+ }
+}
diff --git a/ProductivitySuite/ui/react/src/components/Chat_Assistant/ChatAssistant.tsx b/ProductivitySuite/ui/react/src/components/Chat_Assistant/ChatAssistant.tsx
new file mode 100644
index 0000000000..9194a5ad1d
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Chat_Assistant/ChatAssistant.tsx
@@ -0,0 +1,227 @@
+import React, { useEffect, useRef, useState } from "react";
+
+import styles from "./ChatAssistant.module.scss";
+import {
+ Button,
+ Typography,
+ IconButton,
+ Box,
+ styled,
+ Tooltip,
+} from "@mui/material";
+import { AtomIcon } from "@icons/Atom";
+import ThumbUpIcon from "@mui/icons-material/ThumbUp";
+import ThumbUpOutlinedIcon from "@mui/icons-material/ThumbUpOutlined";
+import ThumbDownIcon from "@mui/icons-material/ThumbDown";
+import ThumbDownOutlinedIcon from "@mui/icons-material/ThumbDownOutlined";
+import ContentCopyIcon from "@mui/icons-material/ContentCopy";
+import EditNoteIcon from "@mui/icons-material/EditNote";
+import ChatSettingsModal from "@components/Chat_SettingsModal/ChatSettingsModal";
+
+import {
+ NotificationSeverity,
+ notify,
+} from "@components/Notification/Notification";
+import { ChatMessageProps, Message } from "@redux/Conversation/Conversation";
+import ChatMarkdown from "@components/Chat_Markdown/ChatMarkdown";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import {
+ conversationSelector,
+ saveConversationtoDatabase,
+ setSelectedConversationHistory,
+} from "@redux/Conversation/ConversationSlice";
+import WaitingIcon from "@icons/Waiting";
+
+const CancelStyle = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.actionButtons.delete,
+}));
+
+const SaveStyle = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.actionButtons.solid,
+}));
+
+const ChatAssistant: React.FC = ({
+ message,
+ pending = false,
+}) => {
+ const dispatch = useAppDispatch();
+ const {
+ onGoingResult,
+ selectedConversationHistory,
+ selectedConversationId,
+ type,
+ } = useAppSelector(conversationSelector);
+
+ const [currentMessage, setCurrentMessage] = useState(message);
+ const [editResponse, setEditResponse] = useState(false);
+ const responseRef = useRef(currentMessage.content);
+ const [disabledSave, setDisabledSave] = useState(false);
+ const [inputHeight, setInputHeight] = useState(0);
+ const heightCheck = useRef(null);
+ const isClipboardAvailable = navigator.clipboard && window.isSecureContext;
+
+ useEffect(() => {
+ setCurrentMessage(message);
+ }, [message]);
+
+ const assistantMessage = currentMessage.content ?? "";
+
+ // const [feedback, setFeedback] = useState(
+ // currentMessage.feedback?.is_thumbs_up === true ? true : currentMessage.feedback?.is_thumbs_up === false ? false : null
+ // );
+
+ // const submitFeedback = (thumbsUp: boolean) => {
+ // setFeedback(thumbsUp);
+ // notify('Feedback Submitted', NotificationSeverity.SUCCESS);
+ // // MessageService.submitFeedback({ id: currentMessage.message_id, feedback: {is_thumbs_up: thumbsUp}, useCase: selectedUseCase.use_case });
+ // };
+
+ const copyText = (text: string) => {
+ navigator.clipboard.writeText(text);
+ notify("Copied to clipboard", NotificationSeverity.SUCCESS);
+ };
+
+ const modifyResponse = () => {
+ if (heightCheck.current) {
+ let updateHeight = heightCheck.current.offsetHeight;
+ setInputHeight(updateHeight);
+ setEditResponse(true);
+ }
+ };
+
+ const updateResponse = (response: string) => {
+ responseRef.current = response;
+ setDisabledSave(response === "");
+ };
+
+ const saveResponse = () => {
+ const convoClone: Message[] = selectedConversationHistory.map(
+ (messageItem) => {
+ if (messageItem.time === currentMessage.time) {
+ return {
+ ...messageItem,
+ content: responseRef.current,
+ };
+ }
+ return messageItem;
+ },
+ );
+
+ dispatch(setSelectedConversationHistory(convoClone));
+ dispatch(
+ saveConversationtoDatabase({
+ conversation: { id: selectedConversationId },
+ }),
+ );
+
+ setInputHeight(0);
+ setEditResponse(false);
+ setDisabledSave(false);
+ };
+
+ const cancelResponse = () => {
+ setEditResponse(false);
+ };
+
+ const displayCurrentMessage = () => {
+ if (currentMessage.content) {
+ if (editResponse) {
+ return (
+
+
+
+
+ Save
+
+ Cancel
+
+ );
+ } else {
+ return (
+
+
+
+ );
+ }
+ } else {
+ return (
+
+ Generating response
+
+ .
+ .
+ .
+
+
+ );
+ }
+ };
+
+ const displayMessageActions = () => {
+ if (onGoingResult) return;
+
+ return (
+
+ {/*TODO: feedback support */}
+ {/* submitFeedback(true)}>
+ {feedback === null || feedback === false ? (
+
+ ) : (
+
+ )}
+
+
+ submitFeedback(false)}>
+ {feedback === null || feedback === true ? (
+
+ ) : (
+
+ )}
+ */}
+
+
+
+ {isClipboardAvailable && (
+
+ copyText(assistantMessage)}>
+
+
+
+ )}
+
+ {type === "chat" && (
+
+
+
+
+
+ )}
+
+ );
+ };
+
+ return (
+
+
+
+
+ {displayCurrentMessage()}
+
+ {!pending && displayMessageActions()}
+
+
+ );
+};
+
+export default ChatAssistant;
diff --git a/ProductivitySuite/ui/react/src/components/Chat_Markdown/ChatMarkdown.tsx b/ProductivitySuite/ui/react/src/components/Chat_Markdown/ChatMarkdown.tsx
new file mode 100644
index 0000000000..2d8c0658d9
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Chat_Markdown/ChatMarkdown.tsx
@@ -0,0 +1,90 @@
+import React, { lazy, Suspense, useEffect } from "react";
+import markdownStyles from "./markdown.module.scss";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+import remarkFrontmatter from "remark-frontmatter";
+import remarkBreaks from "remark-breaks";
+
+const CodeRender = lazy(() => import("./CodeRender/CodeRender"));
+
+type MarkdownProps = {
+ content: string;
+};
+
+const ChatMarkdown = ({ content }: MarkdownProps) => {
+ useEffect(() => {
+ // preload in background
+ import("./CodeRender/CodeRender");
+ }, []);
+
+ return (
+ {
+ // check for nested block elements attempting to inject into a p tag
+ const hasBlockElement = React.Children.toArray(children).some(
+ (child) =>
+ React.isValidElement(child) &&
+ typeof child.type === "string" &&
+ ["div", "h1", "h2", "h3", "ul", "ol", "table"].includes(
+ child.type,
+ ),
+ );
+
+ // If block-level elements are found, avoid wrapping in
+ return hasBlockElement ? (
+ <>{children}>
+ ) : (
+
+ {children}
+
+ );
+ },
+ a: ({ children, ...props }) => {
+ return (
+ //@ts-ignore
+
+ {children}
+
+ );
+ },
+ table: ({ children, ...props }) => {
+ return (
+
+ );
+ },
+ code({ inline, className, children }) {
+ const lang = /language-(\w+)/.exec(className || "");
+ return (
+ Loading Code Block...}>
+ {/*@ts-ignore*/}
+
+
+ );
+ },
+ }}
+ />
+ );
+};
+
+export default ChatMarkdown;
diff --git a/ProductivitySuite/ui/react/src/components/Chat_Markdown/CodeRender/CodeRender.tsx b/ProductivitySuite/ui/react/src/components/Chat_Markdown/CodeRender/CodeRender.tsx
new file mode 100644
index 0000000000..3fb833c90f
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Chat_Markdown/CodeRender/CodeRender.tsx
@@ -0,0 +1,78 @@
+import styles from "./codeRender.module.scss";
+import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
+import {
+ atomOneDark,
+ atomOneLight,
+} from "react-syntax-highlighter/dist/esm/styles/hljs";
+import ContentCopyIcon from "@mui/icons-material/ContentCopy";
+import { IconButton, styled, Tooltip, useTheme } from "@mui/material";
+import {
+ NotificationSeverity,
+ notify,
+} from "@components/Notification/Notification";
+
+const TitleBox = styled("div")(({ theme }) => ({
+ background: theme.customStyles.code?.primary,
+ color: theme.customStyles.code?.title,
+}));
+
+const StyledCode = styled(SyntaxHighlighter)(({ theme }) => ({
+ background: theme.customStyles.code?.secondary + " !important",
+}));
+
+type CodeRenderProps = {
+ cleanCode: React.ReactNode;
+ language: string;
+ inline: boolean;
+};
+const CodeRender = ({ cleanCode, language, inline }: CodeRenderProps) => {
+ const theme = useTheme();
+
+ const isClipboardAvailable = navigator.clipboard && window.isSecureContext;
+
+ cleanCode = String(cleanCode)
+ .replace(/\n$/, "")
+ .replace(/^\s*[\r\n]/gm, ""); //right trim and remove empty lines from the input
+
+ const copyText = (text: string) => {
+ navigator.clipboard.writeText(text);
+ notify("Copied to clipboard", NotificationSeverity.SUCCESS);
+ };
+
+ try {
+ return inline ? (
+
+ {cleanCode}
+
+ ) : (
+
+
+
+ {language || "language not detected"}
+
+
+ {isClipboardAvailable && (
+
+ copyText(cleanCode.toString())}>
+
+
+
+ )}
+
+
+
+
+ );
+ } catch (err) {
+ return {cleanCode} ;
+ }
+};
+
+export default CodeRender;
diff --git a/ProductivitySuite/ui/react/src/components/Chat_Markdown/CodeRender/codeRender.module.scss b/ProductivitySuite/ui/react/src/components/Chat_Markdown/CodeRender/codeRender.module.scss
new file mode 100644
index 0000000000..596004846e
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Chat_Markdown/CodeRender/codeRender.module.scss
@@ -0,0 +1,36 @@
+.code {
+ margin: 7px 0px;
+
+ .codeHead {
+ padding: 0px 10px !important;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ align-items: center;
+ justify-content: space-between;
+
+ .codeTitle {
+ }
+
+ .codeActionGroup {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ align-items: center;
+ justify-content: flex-start;
+ }
+ }
+
+ .codeHighlighterDiv {
+ margin: 0px !important;
+ white-space: pre-wrap !important;
+
+ code {
+ white-space: pre-wrap !important;
+ }
+ }
+}
+
+.inlineCode {
+ background: #fff;
+}
diff --git a/ProductivitySuite/ui/react/src/components/Chat_Markdown/markdown.module.scss b/ProductivitySuite/ui/react/src/components/Chat_Markdown/markdown.module.scss
new file mode 100644
index 0000000000..e86902eed3
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Chat_Markdown/markdown.module.scss
@@ -0,0 +1,29 @@
+.tableDiv {
+ &:first-of-type {
+ padding-top: 0px !important;
+ }
+
+ table,
+ th,
+ td {
+ border: 1px solid black;
+ border-collapse: collapse;
+ padding: 5px;
+ }
+}
+
+.md {
+ li {
+ margin-left: 35px; /* Adjust the value based on your preference */
+ }
+}
+
+.markdownWrapper {
+ > p:first-of-type {
+ margin-top: 0.25rem;
+ }
+
+ > p:last-of-type {
+ margin-bottom: 0.25rem;
+ }
+}
diff --git a/ProductivitySuite/ui/react/src/components/Chat_SettingsModal/ChatSettingsModal.module.scss b/ProductivitySuite/ui/react/src/components/Chat_SettingsModal/ChatSettingsModal.module.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/ProductivitySuite/ui/react/src/components/Chat_SettingsModal/ChatSettingsModal.tsx b/ProductivitySuite/ui/react/src/components/Chat_SettingsModal/ChatSettingsModal.tsx
new file mode 100644
index 0000000000..732e5a2123
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Chat_SettingsModal/ChatSettingsModal.tsx
@@ -0,0 +1,43 @@
+import * as React from "react";
+import {
+ Box,
+ Typography,
+ Modal,
+ IconButton,
+ styled,
+ Tooltip,
+} from "@mui/material";
+import SettingsApplicationsOutlinedIcon from "@mui/icons-material/SettingsApplicationsOutlined";
+import PromptSettings from "@components/PromptSettings/PromptSettings";
+import { Close } from "@mui/icons-material";
+import ModalBox from "@root/shared/ModalBox/ModalBox";
+
+const ChatSettingsModal = () => {
+ const [open, setOpen] = React.useState(false);
+ const handleOpen = () => setOpen(true);
+ const handleClose = () => setOpen(false);
+
+ return (
+
+
+
+
+
+
+
+
+
+ Response Settings
+ setOpen(false)}>
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ChatSettingsModal;
diff --git a/ProductivitySuite/ui/react/src/components/Chat_Sources/ChatSources.module.scss b/ProductivitySuite/ui/react/src/components/Chat_Sources/ChatSources.module.scss
new file mode 100644
index 0000000000..1a6a0d76e6
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Chat_Sources/ChatSources.module.scss
@@ -0,0 +1,47 @@
+.sourceWrapper {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+ width: var(--content-width);
+ margin: 0 auto var(--vertical-spacer);
+ max-width: 100%;
+}
+
+.iconWrap {
+ border: none;
+ border-radius: 6px;
+ margin-right: 0.5rem;
+ width: 30px;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.sourceBox {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ margin-left: 1rem;
+ padding: 5px;
+ border-radius: 6px;
+ margin-bottom: 1rem;
+}
+
+.title {
+ margin: 0 0.5rem 0 0;
+ white-space: nowrap;
+ display: inline-block;
+ max-width: 150px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-weight: 400;
+}
+
+.chip {
+ border-radius: 8px;
+ padding: 3px;
+ font-size: 12px;
+}
diff --git a/ProductivitySuite/ui/react/src/components/Chat_Sources/ChatSources.tsx b/ProductivitySuite/ui/react/src/components/Chat_Sources/ChatSources.tsx
new file mode 100644
index 0000000000..2bf0858254
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Chat_Sources/ChatSources.tsx
@@ -0,0 +1,28 @@
+import { Box } from "@mui/material";
+import { conversationSelector } from "@redux/Conversation/ConversationSlice";
+import { useAppSelector } from "@redux/store";
+import styles from "./ChatSources.module.scss";
+import FileDispaly from "@components/File_Display/FileDisplay";
+
+const ChatSources: React.FC = () => {
+ const { sourceLinks, sourceFiles, sourceType } =
+ useAppSelector(conversationSelector);
+ const isWeb = sourceType === "web";
+ const sourceElements = isWeb ? sourceLinks : sourceFiles;
+
+ if (sourceLinks.length === 0 && sourceFiles.length === 0) return;
+
+ const renderElements = () => {
+ return sourceElements.map((element: any, elementIndex) => {
+ return (
+
+
+
+ );
+ });
+ };
+
+ return {renderElements()} ;
+};
+
+export default ChatSources;
diff --git a/ProductivitySuite/ui/react/src/components/Chat_User/ChatUser.module.scss b/ProductivitySuite/ui/react/src/components/Chat_User/ChatUser.module.scss
new file mode 100644
index 0000000000..3a5b5079ee
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Chat_User/ChatUser.module.scss
@@ -0,0 +1,27 @@
+.userWrapper {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 2rem;
+ position: relative;
+
+ .userPrompt {
+ max-width: 80%;
+ border-radius: var(--input-radius);
+ padding: 0.75rem 2rem 0.75rem 1rem;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ word-break: break-word;
+ }
+
+ .addIcon {
+ position: absolute;
+ right: -16px;
+ top: 3px;
+ opacity: 0;
+ transition: opacity 0.3s;
+ }
+
+ &:hover .addIcon {
+ opacity: 1;
+ }
+}
diff --git a/ProductivitySuite/ui/react/src/components/Chat_User/ChatUser.tsx b/ProductivitySuite/ui/react/src/components/Chat_User/ChatUser.tsx
new file mode 100644
index 0000000000..37971f87cb
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Chat_User/ChatUser.tsx
@@ -0,0 +1,44 @@
+import { IconButton, styled, Tooltip } from "@mui/material";
+import React from "react";
+import styles from "./ChatUser.module.scss";
+import AddCircle from "@mui/icons-material/AddCircle";
+import { useAppDispatch } from "@redux/store";
+import { addPrompt } from "@redux/Prompt/PromptSlice";
+import ChatMarkdown from "@components/Chat_Markdown/ChatMarkdown";
+
+interface ChatUserProps {
+ content: string;
+}
+
+const UserInput = styled("div")(({ theme }) => ({
+ background: theme.customStyles.user?.main,
+}));
+
+const AddIcon = styled(AddCircle)(({ theme }) => ({
+ path: {
+ fill: theme.customStyles.icon?.main,
+ },
+}));
+
+const ChatUser: React.FC = ({ content }) => {
+ const dispatch = useAppDispatch();
+
+ const sharePrompt = () => {
+ dispatch(addPrompt({ promptText: content }));
+ };
+
+ return (
+
+ );
+};
+
+export default ChatUser;
diff --git a/ProductivitySuite/ui/react/src/components/CodeGen/CodeGen.tsx b/ProductivitySuite/ui/react/src/components/CodeGen/CodeGen.tsx
deleted file mode 100644
index 29c96f61cb..0000000000
--- a/ProductivitySuite/ui/react/src/components/CodeGen/CodeGen.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-import { KeyboardEventHandler, SyntheticEvent, useEffect, useRef, useState } from 'react'
-import styleClasses from "./codeGen.module.scss"
-import { ActionIcon, Textarea, Title, rem } from '@mantine/core'
-import { IconArrowRight } from '@tabler/icons-react'
-import { ConversationMessage } from '../Message/conversationMessage'
-import { fetchEventSource } from '@microsoft/fetch-event-source'
-import { CODE_GEN_URL } from '../../config'
-
-
-
-const CodeGen = () => {
- const [prompt, setPrompt] = useState("")
- const [submittedPrompt, setSubmittedPrompt] = useState("")
- const [response,setResponse] = useState("");
- const promptInputRef = useRef(null)
- const scrollViewport = useRef(null)
-
- const toSend = "Enter"
-
- const handleSubmit = async () => {
- setResponse("")
- setSubmittedPrompt(prompt)
- const body = {
- messages:prompt
- }
- fetchEventSource(CODE_GEN_URL, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "Accept":"*/*"
- },
- body: JSON.stringify(body),
- openWhenHidden: true,
- async onopen(response) {
- if (response.ok) {
- return;
- } else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
- const e = await response.json();
- console.log(e);
- throw Error(e.error.message);
- } else {
- console.log("error", response);
- }
- },
- onmessage(msg) {
- if (msg?.data != "[DONE]") {
- try {
- const match = msg.data.match(/b'([^']*)'/);
- if (match && match[1] != "") {
- const extractedText = match[1].replace(/\\n/g, "\n");
- setResponse(prev=>prev+extractedText);
- }
- } catch (e) {
- console.log("something wrong in msg", e);
- throw e;
- }
- }
- },
- onerror(err) {
- console.log("error", err);
- setResponse("")
- throw err;
- },
- onclose() {
- setPrompt("")
- },
- });
-
- }
-
- const scrollToBottom = () => {
- scrollViewport.current!.scrollTo({ top: scrollViewport.current!.scrollHeight })
- }
-
- useEffect(() => {
- scrollToBottom()
- }, [response])
-
- const handleKeyDown: KeyboardEventHandler = (event) => {
- if (!event.shiftKey && event.key === toSend) {
- handleSubmit()
- setTimeout(() => {
- setPrompt("")
- }, 1)
- }
- }
-
- const handleChange = (event: SyntheticEvent) => {
- event.preventDefault()
- setPrompt((event.target as HTMLTextAreaElement).value)
- }
- return (
-
-
-
-
-
CodeGen
-
-
-
- {!submittedPrompt && !response &&
- (<>
-
Start by asking a question
- >)
- }
- {submittedPrompt && (
-
- )}
- {response && (
-
- )}
-
-
-
-
-
-
-
- )
-}
-export default CodeGen;
diff --git a/ProductivitySuite/ui/react/src/components/CodeGen/codeGen.module.scss b/ProductivitySuite/ui/react/src/components/CodeGen/codeGen.module.scss
deleted file mode 100644
index acee80b066..0000000000
--- a/ProductivitySuite/ui/react/src/components/CodeGen/codeGen.module.scss
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-@import "../../styles/styles";
-
-.spacer {
- flex: 1 1 auto;
-}
-
-.codeGenWrapper {
- @include flex(row, nowrap, flex-start, flex-start);
- flex: 1 1 auto;
- height: 100%;
- & > * {
- height: 100%;
- }
- .codeGenContent {
- flex: 1 1 auto;
- position: relative;
- .codeGenContentMessages {
- @include absolutes;
- // @include flex(column, nowrap, flex-start, flex-start);
-
- display: grid;
- grid-template-areas:
- "header"
- "messages"
- "inputs";
-
- grid-template-columns: auto;
- grid-template-rows: 60px auto 100px;
-
- .codeGenTitle {
- grid-area: header;
- @include flex(row, nowrap, center, flex-start);
- height: 60px;
- padding: 8px 24px;
- border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
- }
-
- .historyContainer {
- grid-area: messages;
- overflow: auto;
- width: 100%;
- padding: 16px 32px;
- & > * {
- width: 100%;
- }
- }
-
- .codeGenActions {
- // padding: --var()
- grid-area: inputs;
- padding: 18px;
- border-top: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
- }
- }
- }
-}
diff --git a/ProductivitySuite/ui/react/src/components/Conversation/Conversation.tsx b/ProductivitySuite/ui/react/src/components/Conversation/Conversation.tsx
deleted file mode 100644
index e772248f39..0000000000
--- a/ProductivitySuite/ui/react/src/components/Conversation/Conversation.tsx
+++ /dev/null
@@ -1,197 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-import { KeyboardEventHandler, SyntheticEvent, useEffect, useRef, useState } from 'react'
-import styleClasses from "./conversation.module.scss"
-import { ActionIcon, Group, Textarea, Title, Tooltip, rem } from '@mantine/core'
-import { IconArrowDown, IconArrowRight, IconArrowUp, IconMessagePlus } from '@tabler/icons-react'
-import { conversationSelector, doConversation, getAllConversations, newConversation, setSystemPrompt } from '../../redux/Conversation/ConversationSlice'
-import { ConversationMessage } from '../Message/conversationMessage'
-import { useAppDispatch, useAppSelector } from '../../redux/store'
-import { Message, MessageRole } from '../../redux/Conversation/Conversation'
-import { getCurrentTimeStamp } from '../../common/util'
-import { ConversationSideBar } from './ConversationSideBar'
-import { getPrompts } from '../../redux/Prompt/PromptSlice'
-import { userSelector } from '../../redux/User/userSlice'
-import PromptTemplate from './PromptTemplate'
-
-type ConversationProps = {
- title: string
-}
-
-const Conversation = ({ title }: ConversationProps) => {
- const [prompt, setPrompt] = useState("")
- const [updateSystemPrompt, setUpdateSystemPrompt] = useState(false)
-
- const dispatch = useAppDispatch();
- const promptInputRef = useRef(null)
-
- const { conversations, onGoingResult, selectedConversationId, selectedConversationHistory, temperature, token, model, systemPrompt } = useAppSelector(conversationSelector)
- const { name } = useAppSelector(userSelector)
- const selectedConversation = conversations.find(x => x.id === selectedConversationId)
-
- const scrollViewport = useRef(null)
-
- const toSend = "Enter"
-
- const systemPromptObject: Message = {
- role: MessageRole.System,
- content: systemPrompt,
- };
-
-
- const handleSubmit = () => {
-
- const userPrompt: Message = {
- role: MessageRole.User,
- content: prompt,
- time: getCurrentTimeStamp().toString()
- };
- let messages: Message[] = [];
- // if (selectedConversation) {
- // messages = selectedConversation.Messages.map(message => {
- // return { role: message.role, content: message.content }
- // })
- // }
-
- messages = [systemPromptObject, ...(selectedConversationHistory)]
-
- doConversation({
- conversationId: selectedConversationId,
- userPrompt,
- messages,
- model,
- temperature,
- token
- })
- setPrompt("")
- }
-
- const scrollToBottom = () => {
- scrollViewport.current!.scrollTo({ top: scrollViewport.current!.scrollHeight })
- }
-
- useEffect(() => {
- if (name && name != "") {
- dispatch(getPrompts({ promptText: "" }))
- dispatch(getAllConversations({ user: name }))
- }
- }, [name]);
-
- useEffect(() => {
- scrollToBottom()
- }, [onGoingResult, selectedConversationHistory])
-
- const handleKeyDown: KeyboardEventHandler = (event) => {
- setUpdateSystemPrompt(false)
- if (!event.shiftKey && event.key === toSend) {
- handleSubmit()
- setTimeout(() => {
- setPrompt("")
- }, 1)
- }
- }
-
-
-
- const handleNewConversation = () => {
- dispatch(newConversation())
- }
-
- const handleChange = (event: SyntheticEvent) => {
- event.preventDefault()
- setPrompt((event.target as HTMLTextAreaElement).value)
- }
-
- const handleSystemPromptChange = (event: SyntheticEvent) => {
- event.preventDefault()
- dispatch(setSystemPrompt((event.target as HTMLTextAreaElement).value))
- }
- return (
-
-
-
-
-
-
{selectedConversation?.first_query || ""}
-
-
- {(selectedConversation || selectedConversationHistory.length > 0) && (
-
-
-
- )}
-
-
-
-
-
-
- {!(selectedConversation || selectedConversationHistory.length > 0) && (
-
-
-
Start by asking a question
-
You can update the system prompt by clicking on up arrow beside prompt field
-
You can also upload your Documents in the Data Management Tab
-
-
-
-
- )}
-
- {selectedConversationHistory.map((message, index) => {
- return (message.role !== MessageRole.System && (
))
- })
- }
-
- {onGoingResult && (
-
- )}
-
-
-
-
-
-
- setUpdateSystemPrompt((prev) => !prev)} size={32} radius="xl" variant="filled">
- {updateSystemPrompt ? ( ) :
- ( )}
-
-
-
-
-
-
-
-
- )
-}
-export default Conversation;
diff --git a/ProductivitySuite/ui/react/src/components/Conversation/ConversationSideBar.tsx b/ProductivitySuite/ui/react/src/components/Conversation/ConversationSideBar.tsx
deleted file mode 100644
index f495394c8f..0000000000
--- a/ProductivitySuite/ui/react/src/components/Conversation/ConversationSideBar.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-import { ActionIcon, Title } from "@mantine/core"
-
-import contextStyles from "../../styles/components/context.module.scss"
-import { useAppDispatch, useAppSelector } from "../../redux/store"
-import { conversationSelector, deleteConversation, getConversationHistory, setSelectedConversationId } from "../../redux/Conversation/ConversationSlice"
-import { useEffect } from "react"
-import { userSelector } from "../../redux/User/userSlice"
-import { IconTrash } from "@tabler/icons-react"
-import Settings from "./settings"
-// import { userSelector } from "../../redux/User/userSlice"
-
-export interface ConversationContextProps {
- title: string
-}
-
-export function ConversationSideBar({ title }: ConversationContextProps) {
- const { conversations, selectedConversationId } = useAppSelector(conversationSelector)
- const { name } = useAppSelector(userSelector)
- // const user = useAppSelector(userSelector)
- const dispatch = useAppDispatch()
-
- useEffect(() => {
- if (selectedConversationId != "") {
- dispatch(getConversationHistory({ user: name, conversationId: selectedConversationId }))
- }
- }, [selectedConversationId])
-
- const handleDeleteConversation = (id: string) => {
- dispatch(deleteConversation({ user: name, conversationId: id }))
- }
-
- const conversationList = conversations?.map((curr) => (
- {
- event.preventDefault()
- dispatch(setSelectedConversationId(curr.id))
- }}
- key={curr.id}
- >
-
{curr.first_query}
- {selectedConversationId === curr.id && (
-
handleDeleteConversation(curr.id)} size={30} variant="default">
-
-
- )}
-
-
- ))
-
- return (
-
-
- {title}
-
-
{conversationList}
-
-
-
-
- )
-}
diff --git a/ProductivitySuite/ui/react/src/components/Conversation/DataSource.tsx b/ProductivitySuite/ui/react/src/components/Conversation/DataSource.tsx
deleted file mode 100644
index 72575af06e..0000000000
--- a/ProductivitySuite/ui/react/src/components/Conversation/DataSource.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-import { ActionIcon, Button, Container, FileInput, Text, TextInput, Title } from '@mantine/core'
-import { IconFile, IconTrash } from '@tabler/icons-react'
-import { SyntheticEvent, useEffect, useState } from 'react'
-import { useAppDispatch, useAppSelector } from '../../redux/store'
-import { conversationSelector, deleteInDataSource, getAllFilesInDataSource, submitDataSourceURL, uploadFile } from '../../redux/Conversation/ConversationSlice'
-import styleClasses from './dataSource.module.scss'
-
-
-
-export default function DataSource() {
- const [file, setFile] = useState();
- const [isFile, setIsFile] = useState(true);
- const [url, setURL] = useState("");
- const dispatch = useAppDispatch()
- const { filesInDataSource } = useAppSelector(conversationSelector)
-
- const handleFileUpload = () => {
- if (file)
- dispatch(uploadFile({ file }))
- }
-
- const handleChange = (event: SyntheticEvent) => {
- event.preventDefault()
- setURL((event.target as HTMLTextAreaElement).value)
- }
-
- const handleSubmit = () => {
- dispatch(submitDataSourceURL({ link_list: url.split(";") }))
- }
-
- const handleDelete = (file: string) => {
- dispatch(deleteInDataSource({file}))
- }
-
- useEffect(()=>{
- dispatch(getAllFilesInDataSource({knowledgeBaseId:"default"}))
- },[])
-
- return (
-
-
- Data Source
-
-
- Please upload your local file or paste a remote file link, and Chat will respond based on the content of the uploaded file.
-
-
-
-
-
- setIsFile(true)}>Upload File
- setIsFile(false)}>Use Link
-
-
-
-
-
- {isFile ? (
- <>
-
- Upload
- >
- ) : (
- <>
-
- Upload
- >
- )}
-
-
-
-
- Files
-
- {filesInDataSource.map(file=> {
- return (
-
-
-
{file.name}
-
handleDelete(file.name)} size={32} variant="default">
-
-
-
- )})}
-
-
- )
-}
diff --git a/ProductivitySuite/ui/react/src/components/Conversation/PromptTemplate.tsx b/ProductivitySuite/ui/react/src/components/Conversation/PromptTemplate.tsx
deleted file mode 100644
index 723e1c4295..0000000000
--- a/ProductivitySuite/ui/react/src/components/Conversation/PromptTemplate.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useEffect } from "react"
-import { getPrompts, promptSelector } from "../../redux/Prompt/PromptSlice"
-import { useAppDispatch, useAppSelector } from "../../redux/store"
-import styleClasses from './promptTemplate.module.scss'
-import { userSelector } from "../../redux/User/userSlice"
-type PromptTemplateProps = {
- setPrompt: (prompt:string)=>void;
-}
-function PromptTemplate({setPrompt}:PromptTemplateProps) {
- const dispatch = useAppDispatch()
- const {prompts} = useAppSelector(promptSelector)
- const {name} = useAppSelector(userSelector)
- useEffect(()=> {
- if(name && name!=="")
- dispatch(getPrompts({promptText:""}))
- },[])
- return (
-
- Prompt Templates
-
- {
- prompts.map(prompt =>
{
- setPrompt(prompt.prompt_text)
-
- }}>{prompt.prompt_text}
)
- }
-
- )
-}
-
-export default PromptTemplate
diff --git a/ProductivitySuite/ui/react/src/components/Conversation/conversation.module.scss b/ProductivitySuite/ui/react/src/components/Conversation/conversation.module.scss
deleted file mode 100644
index e38290d103..0000000000
--- a/ProductivitySuite/ui/react/src/components/Conversation/conversation.module.scss
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-@import "../../styles/styles";
-@import "../../styles/components/content.scss";
-
-.spacer {
- flex: 1 1 auto;
-}
-
-.conversationWrapper {
- @include flex(row, nowrap, flex-start, flex-start);
- flex: 1 1 auto;
- height: 100%;
- & > * {
- height: 100%;
- }
- .conversationContent {
- flex: 1 1 auto;
- position: relative;
- .conversationContentMessages {
- @include absolutes;
-
- display: grid;
- height: 100vh;
- grid-template-areas:
- "header"
- "messages"
- "inputs";
-
- grid-template-columns: auto;
- grid-template-rows: 60px 1fr 80px;
-
- .conversationTitle {
- grid-area: header;
- width: 100%;
- flex: 1;
- @include flex(row, nowrap, center, flex-start);
- height: 60px;
- padding: 8px 24px;
- border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
- .title {
- @include textWrapEllipsis;
- width: 70vw;
- }
- }
-
- .historyContainer {
- grid-area: messages;
- overflow: auto;
- width: 100%;
- .newConversation {
- @include flex(row, nowrap, flex-start, flex-start);
- .infoMessages {
- padding: 16px 32px;
- flex: 80;
- }
- }
- & > * {
- width: 100%;
- }
- }
-
- .conversationActions {
- grid-area: inputs;
- width: 100%;
- padding: 10px;
- border-top: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
- }
- }
-
- .conversationSplash {
- @include absolutes;
- @include flex(column, nowrap, center, center);
- font-size: 32px;
- }
- }
-}
diff --git a/ProductivitySuite/ui/react/src/components/Conversation/dataSource.module.scss b/ProductivitySuite/ui/react/src/components/Conversation/dataSource.module.scss
deleted file mode 100644
index 140711a0f4..0000000000
--- a/ProductivitySuite/ui/react/src/components/Conversation/dataSource.module.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-@import "../../styles/styles";
-
-.dataSourceWrapper {
- // @include flex(column, nowrap, flex-start, flex-start);
- // flex: 1 1 auto;
- height: 100%;
- padding: 20px;
-}
diff --git a/ProductivitySuite/ui/react/src/components/Conversation/promptTemplate.module.scss b/ProductivitySuite/ui/react/src/components/Conversation/promptTemplate.module.scss
deleted file mode 100644
index 5e133a3de8..0000000000
--- a/ProductivitySuite/ui/react/src/components/Conversation/promptTemplate.module.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-.promptContainer {
- flex: 20;
- border-left: 1px solid var(--mantine-color-gray-3);
- height: 82vh;
- padding: 8px;
- .prompt {
- // height: %;
- margin-top: 8px;
- margin-bottom: 8px;
- cursor: pointer;
- padding: 8px;
- border: 1px solid var(--mantine-color-gray-3);
- }
-}
diff --git a/ProductivitySuite/ui/react/src/components/Conversation/settings.tsx b/ProductivitySuite/ui/react/src/components/Conversation/settings.tsx
deleted file mode 100644
index bb9468413a..0000000000
--- a/ProductivitySuite/ui/react/src/components/Conversation/settings.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { NumberInput, Slider, Text, Title } from "@mantine/core"
-import { useAppDispatch, useAppSelector } from "../../redux/store"
-import { conversationSelector, setTemperature, setToken } from "../../redux/Conversation/ConversationSlice"
-
-
-
-function Settings() {
- const { token,maxTemperature, minTemperature, maxToken, minToken, temperature} = useAppSelector(conversationSelector)
- const dispatch = useAppDispatch();
-
- const onTemperatureChange = (value: number) => {
- dispatch(setTemperature(value))
- }
- const onTokenChange = (value: number | string) => {
- dispatch(setToken(Number(value)))
- }
-
- return (
- <>
-
-
Settings
-
-
- Temperature
-
-
-
- Token ({`${minToken} - ${maxToken}`})
-
-
- >
-
- )
-}
-
-export default Settings
diff --git a/ProductivitySuite/ui/react/src/components/Data_Web/DataWebInput.tsx b/ProductivitySuite/ui/react/src/components/Data_Web/DataWebInput.tsx
new file mode 100644
index 0000000000..ae54cfc92a
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Data_Web/DataWebInput.tsx
@@ -0,0 +1,71 @@
+import ProgressIcon from "@components/ProgressIcon/ProgressIcon";
+import {
+ CustomTextInput,
+ AddIcon,
+} from "@components/Summary_WebInput/WebInput";
+import styles from "@components/Summary_WebInput/WebInput.module.scss";
+import { Box, InputAdornment } from "@mui/material";
+import {
+ conversationSelector,
+ submitDataSourceURL,
+} from "@redux/Conversation/ConversationSlice";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import { useEffect, useState } from "react";
+
+const DataWebInput = () => {
+ const { dataSourceUrlStatus } = useAppSelector(conversationSelector);
+ const [inputValue, setInputValue] = useState("");
+ const [uploading, setUploading] = useState(false);
+ const dispatch = useAppDispatch();
+
+ const handleKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && inputValue) {
+ handleAdd(inputValue);
+ }
+ };
+
+ const handleAdd = (newSource: string) => {
+ dispatch(submitDataSourceURL({ link_list: [newSource] }));
+ setInputValue("");
+ };
+
+ const handleIconClick = () => {
+ if (inputValue) {
+ handleAdd(inputValue);
+ }
+ };
+
+ useEffect(() => {
+ setUploading(dataSourceUrlStatus === "pending");
+ }, [dataSourceUrlStatus]);
+
+ return (
+
+ ) =>
+ setInputValue(e.target.value)
+ }
+ InputProps={{
+ endAdornment: !uploading ? (
+
+
+
+ ) : (
+
+
+
+ ),
+ }}
+ fullWidth
+ />
+
+ );
+};
+
+export default DataWebInput;
diff --git a/ProductivitySuite/ui/react/src/components/DocSum/DocSum.tsx b/ProductivitySuite/ui/react/src/components/DocSum/DocSum.tsx
deleted file mode 100644
index 2fa9fd4a34..0000000000
--- a/ProductivitySuite/ui/react/src/components/DocSum/DocSum.tsx
+++ /dev/null
@@ -1,153 +0,0 @@
-import styleClasses from './docSum.module.scss'
-import { Button, Text, Textarea, Title } from '@mantine/core'
-import { FileUpload } from './FileUpload'
-import { useEffect, useState } from 'react'
-import Markdown from '../Shared/Markdown/Markdown'
-import { fetchEventSource } from '@microsoft/fetch-event-source'
-import { notifications } from '@mantine/notifications'
-import { DOC_SUM_URL } from '../../config'
-import { FileWithPath } from '@mantine/dropzone'
-
-
-const DocSum = () => {
- const [isFile, setIsFile] = useState(false);
- const [files, setFiles] = useState([])
- const [isGenerating, setIsGenerating] = useState(false);
- const [value, setValue] = useState('');
- const [fileContent, setFileContent] = useState('');
- const [response, setResponse] = useState('');
-
- useEffect(() => {
- if(isFile){
- setValue('')
- }
- },[isFile])
-
- useEffect(()=>{
- if (files.length) {
- const reader = new FileReader()
- reader.onload = async () => {
- const text = reader.result?.toString()
- setFileContent(text || '')
- };
- reader.readAsText(files[0])
- }
- },[files])
-
-
- const handleSubmit = async () => {
- setResponse("")
- if(!isFile && !value){
- notifications.show({
- color: "red",
- id: "input",
- message: "Please Upload Content",
- })
- return
- }
-
- setIsGenerating(true)
- const body = {
- messages: isFile ? fileContent : value
- }
- fetchEventSource(DOC_SUM_URL, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "Accept": "*/*"
- },
- body: JSON.stringify(body),
- openWhenHidden: true,
- async onopen(response) {
- if (response.ok) {
- return;
- } else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
- const e = await response.json();
- console.log(e);
- throw Error(e.error.message);
- } else {
- console.log("error", response);
- }
- },
- onmessage(msg) {
- if (msg?.data != "[DONE]") {
- try {
- const res = JSON.parse(msg.data)
- const logs = res.ops;
- logs.forEach((log: { op: string; path: string; value: string }) => {
- if (log.op === "add") {
- if (
- log.value !== "" && log.path.endsWith("/streamed_output/-") && log.path.length > "/streamed_output/-".length
- ) {
- setResponse(prev=>prev+log.value);
- }
- }
- });
- } catch (e) {
- console.log("something wrong in msg", e);
- throw e;
- }
- }
- },
- onerror(err) {
- console.log("error", err);
- setIsGenerating(false)
- throw err;
- },
- onclose() {
- setIsGenerating(false)
- },
- });
-}
-
-
- return (
-
-
-
-
-
Doc Summary
-
-
- Please upload file or paste content for summarization.
-
-
-
- setIsFile(false)}>Paste Text
- setIsFile(true)}>Upload File
-
-
-
- {isFile ? (
-
- { setFiles(files) }} />
-
- ) : (
-
-
- )}
-
-
- Generate Summary
-
- {response && (
-
-
-
- )}
-
-
-
-
- )
-}
-
-export default DocSum
diff --git a/ProductivitySuite/ui/react/src/components/DocSum/FileUpload.tsx b/ProductivitySuite/ui/react/src/components/DocSum/FileUpload.tsx
deleted file mode 100644
index aa5d84a00f..0000000000
--- a/ProductivitySuite/ui/react/src/components/DocSum/FileUpload.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { Group, Text, rem } from '@mantine/core';
-import { IconUpload, IconCloudUpload, IconX, IconFile } from '@tabler/icons-react';
-import { } from '@tabler/icons-react';
-import { Dropzone, DropzoneProps, FileWithPath } from '@mantine/dropzone';
-import '@mantine/dropzone/styles.css';
-import { useState } from 'react';
-
-export function FileUpload(props: Partial) {
- const [files, setFiles] = useState([])
- return (
- { setFiles(files) }}
- onReject={() => { }}
- maxSize={5 * 1024 ** 2}
- multiple={false}
- accept={[
- // MIME_TYPES.png,
- // MIME_TYPES.jpeg,
- // MIME_TYPES.svg,
- // MIME_TYPES.gif,
- // MIME_TYPES.webp,
- // MIME_TYPES.doc,
- // MIME_TYPES.docx,
- // MIME_TYPES.pdf,
- // MIME_TYPES.xls,
- // MIME_TYPES.xlsx,
- "text/plain",
- // "application/json"
- ]}
- style={{ height: '220px', width: '100%', borderColor: 'var(--mantine-color-blue-6)' }}
- {...props}
- >
-
-
-
-
-
-
-
-
- {files.length > 0 ? ( ) : ( )}
-
- {files.length > 0 ? (
-
- {files.map(file => (
-
- {file.name}
-
- ))}
-
-
- ) : (
-
-
- Drag your file here or click to select file
-
-
- .txt
- {/* pdf, txt, doc, docx, png, jpg ..so on */}
-
-
-
- )}
-
-
- );
-}
diff --git a/ProductivitySuite/ui/react/src/components/DocSum/docSum.module.scss b/ProductivitySuite/ui/react/src/components/DocSum/docSum.module.scss
deleted file mode 100644
index 399e979391..0000000000
--- a/ProductivitySuite/ui/react/src/components/DocSum/docSum.module.scss
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-@import "../../styles/styles";
-
-.spacer {
- flex: 1 1 auto;
-}
-
-.docSumWrapper {
- @include flex(row, nowrap, flex-start, flex-start);
- flex: 1 1 auto;
- height: 100%;
- & > * {
- height: 100%;
- }
- .docSumContent {
- flex: 1 1 auto;
- position: relative;
- .docSumContentMessages {
- @include absolutes;
- @include flex(column, nowrap, normal, normal);
- > * {
- padding: 8px 24px;
- }
- .docSumTitle {
- @include flex(row, nowrap, center, flex-start);
- height: 60px;
- border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
- }
- .docSumContentButtonGroup {
- @include flex(row, nowrap, center, center);
- height: 60px;
- }
- .docSumInput {
- .docSumFileUpload {
- @include flex(row, nowrap, center, center);
- }
- }
- .docSumResult {
- }
- }
- }
-}
diff --git a/ProductivitySuite/ui/react/src/components/DropDown/DropDown.module.scss b/ProductivitySuite/ui/react/src/components/DropDown/DropDown.module.scss
new file mode 100644
index 0000000000..a8f0561b62
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/DropDown/DropDown.module.scss
@@ -0,0 +1,63 @@
+.dropDown {
+ .noWrap {
+ white-space: nowrap;
+ display: flex;
+
+ &.ellipsis span {
+ white-space: nowrap;
+ display: inline-block;
+ width: 150px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin: 0;
+ }
+ }
+
+ .unsetMin {
+ min-width: unset;
+ }
+
+ .chevron {
+ transform: rotate(0deg);
+ transition: transform 0.5s;
+
+ &.open {
+ transform: rotate(180deg);
+ }
+ }
+
+ &.border {
+ border-radius: 8px;
+ margin-left: 0.5rem;
+
+ :global {
+ .MuiList-padding {
+ margin-left: 0 !important;
+ }
+
+ .MuiListItemIcon-root {
+ min-width: unset;
+ }
+ }
+
+ :global {
+ .MuiListItemText-root {
+ margin-top: 3px;
+ margin-bottom: 3px;
+ }
+
+ .MuiList-root {
+ padding: 0;
+ margin-left: 0.5rem;
+
+ .MuiButtonBase-root {
+ padding: 0 0.5rem;
+ }
+ }
+ }
+ }
+}
+
+.leftGap {
+ margin-left: 0.5rem !important;
+}
diff --git a/ProductivitySuite/ui/react/src/components/DropDown/DropDown.tsx b/ProductivitySuite/ui/react/src/components/DropDown/DropDown.tsx
new file mode 100644
index 0000000000..dc839f917f
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/DropDown/DropDown.tsx
@@ -0,0 +1,118 @@
+import React, { useState } from "react";
+import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
+import {
+ List,
+ ListItemButton,
+ ListItemText,
+ MenuItem,
+ Menu,
+ Typography,
+ ListItemIcon,
+ styled,
+ Box,
+} from "@mui/material";
+import styles from "./DropDown.module.scss";
+
+interface DropDownProps {
+ options: { name: string; value: string }[];
+ value?: string;
+ handleChange: (value: string) => void;
+ readOnly?: boolean;
+ border?: boolean;
+ ellipsis?: true;
+}
+
+const CustomMenuItem = styled(MenuItem)(({ theme }) => ({
+ ...theme.customStyles.dropDown,
+}));
+
+const DropDownWrapper = styled(Box)(({ theme }) => ({
+ ...theme.customStyles.dropDown.wrapper,
+}));
+
+const DropDown: React.FC = ({
+ options,
+ value,
+ handleChange,
+ readOnly,
+ border,
+ ellipsis,
+}) => {
+ const [anchorEl, setAnchorEl] = useState(null);
+
+ const foundIndex = options.findIndex((option) => option.value === value);
+
+ const [selectedIndex, setSelectedIndex] = useState(
+ foundIndex !== -1 ? foundIndex : 0,
+ );
+
+ const open = Boolean(anchorEl);
+ const handleClickListItem = (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const handleMenuItemClick = (index: number) => {
+ setSelectedIndex(index);
+ setAnchorEl(null);
+ handleChange(options[index].value);
+ };
+
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ if (readOnly) {
+ let name = foundIndex === -1 ? "Unknown" : options[selectedIndex].name;
+ return {name} ;
+ }
+
+ const Wrapper = border ? DropDownWrapper : Box;
+
+ return options.length === 0 ? (
+ <>>
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+ {options.map((option, index) => (
+ handleMenuItemClick(index)}
+ >
+ {option.name}
+
+ ))}
+
+
+ );
+};
+
+export default DropDown;
diff --git a/ProductivitySuite/ui/react/src/components/FaqGen/FaqGen.tsx b/ProductivitySuite/ui/react/src/components/FaqGen/FaqGen.tsx
deleted file mode 100644
index 74f52d3ef3..0000000000
--- a/ProductivitySuite/ui/react/src/components/FaqGen/FaqGen.tsx
+++ /dev/null
@@ -1,167 +0,0 @@
-import styleClasses from './faqGen.module.scss'
-import { Button, Text, Textarea, Title } from '@mantine/core'
-import { FileUpload } from './FileUpload'
-import { useEffect, useState } from 'react'
-import Markdown from '../Shared/Markdown/Markdown'
-import { fetchEventSource } from '@microsoft/fetch-event-source'
-import { notifications } from '@mantine/notifications'
-import { FAQ_GEN_URL } from '../../config'
-import { FileWithPath } from '@mantine/dropzone'
-
-
-const FaqGen = () => {
- const [isFile, setIsFile] = useState(false);
- const [files, setFiles] = useState([])
- const [isGenerating, setIsGenerating] = useState(false);
- const [value, setValue] = useState('');
- const [fileContent, setFileContent] = useState('');
- const [response, setResponse] = useState('');
-
- let messagesEnd:HTMLDivElement;
-
- const scrollToView = () => {
- if (messagesEnd) {
- messagesEnd.scrollTop = messagesEnd.scrollHeight;
- }
- };
- useEffect(()=>{
- scrollToView()
- },[response])
-
- useEffect(() => {
- if(isFile){
- setValue('')
- }
- },[isFile])
-
- useEffect(()=>{
- if (files.length) {
- const reader = new FileReader()
- reader.onload = async () => {
- const text = reader.result?.toString()
- setFileContent(text || '')
- };
- reader.readAsText(files[0])
- }
- },[files])
-
-
- const handleSubmit = async () => {
- setResponse("")
- if(!isFile && !value){
- notifications.show({
- color: "red",
- id: "input",
- message: "Please Upload Content",
- })
- return
- }
-
- setIsGenerating(true)
- const body = {
- messages: isFile ? fileContent : value
- }
- fetchEventSource(FAQ_GEN_URL, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "Accept": "*/*"
- },
- body: JSON.stringify(body),
- openWhenHidden: true,
- async onopen(response) {
- if (response.ok) {
- return;
- } else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
- const e = await response.json();
- console.log(e);
- throw Error(e.error.message);
- } else {
- console.log("error", response);
- }
- },
- onmessage(msg) {
- if (msg?.data != "[DONE]") {
- try {
- const res = JSON.parse(msg.data)
- const logs = res.ops;
- logs.forEach((log: { op: string; path: string; value: string }) => {
- if (log.op === "add") {
- if (
- log.value !== "" && log.path.endsWith("/streamed_output/-") && log.path.length > "/streamed_output/-".length
- ) {
- setResponse(prev=>prev+log.value);
- }
- }
- });
- } catch (e) {
- console.log("something wrong in msg", e);
- throw e;
- }
- }
- },
- onerror(err) {
- console.log("error", err);
- setIsGenerating(false)
- throw err;
- },
- onclose() {
- setIsGenerating(false)
- },
- });
-}
-
-
- return (
-
-
-
-
-
FAQ Generator
-
-
- Please upload file or paste content for generating Faq's.
-
-
-
- setIsFile(false)}>Paste Text
- setIsFile(true)}>Upload File
-
-
-
- {isFile ? (
-
- { setFiles(files) }} />
-
- ) : (
-
-
- )}
-
-
- Generate FAQ's
-
- {response && (
-
{
- if(el)
- messagesEnd = el;
- }}>
-
-
- )}
-
-
-
-
- )
-}
-
-export default FaqGen;
diff --git a/ProductivitySuite/ui/react/src/components/FaqGen/FileUpload.tsx b/ProductivitySuite/ui/react/src/components/FaqGen/FileUpload.tsx
deleted file mode 100644
index aa5d84a00f..0000000000
--- a/ProductivitySuite/ui/react/src/components/FaqGen/FileUpload.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { Group, Text, rem } from '@mantine/core';
-import { IconUpload, IconCloudUpload, IconX, IconFile } from '@tabler/icons-react';
-import { } from '@tabler/icons-react';
-import { Dropzone, DropzoneProps, FileWithPath } from '@mantine/dropzone';
-import '@mantine/dropzone/styles.css';
-import { useState } from 'react';
-
-export function FileUpload(props: Partial) {
- const [files, setFiles] = useState([])
- return (
- { setFiles(files) }}
- onReject={() => { }}
- maxSize={5 * 1024 ** 2}
- multiple={false}
- accept={[
- // MIME_TYPES.png,
- // MIME_TYPES.jpeg,
- // MIME_TYPES.svg,
- // MIME_TYPES.gif,
- // MIME_TYPES.webp,
- // MIME_TYPES.doc,
- // MIME_TYPES.docx,
- // MIME_TYPES.pdf,
- // MIME_TYPES.xls,
- // MIME_TYPES.xlsx,
- "text/plain",
- // "application/json"
- ]}
- style={{ height: '220px', width: '100%', borderColor: 'var(--mantine-color-blue-6)' }}
- {...props}
- >
-
-
-
-
-
-
-
-
- {files.length > 0 ? ( ) : ( )}
-
- {files.length > 0 ? (
-
- {files.map(file => (
-
- {file.name}
-
- ))}
-
-
- ) : (
-
-
- Drag your file here or click to select file
-
-
- .txt
- {/* pdf, txt, doc, docx, png, jpg ..so on */}
-
-
-
- )}
-
-
- );
-}
diff --git a/ProductivitySuite/ui/react/src/components/FaqGen/faqGen.module.scss b/ProductivitySuite/ui/react/src/components/FaqGen/faqGen.module.scss
deleted file mode 100644
index 3c76bbf627..0000000000
--- a/ProductivitySuite/ui/react/src/components/FaqGen/faqGen.module.scss
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-@import "../../styles/styles";
-
-.spacer {
- flex: 1 1 auto;
-}
-
-.faqGenWrapper {
- @include flex(row, nowrap, flex-start, flex-start);
- flex: 1 1 auto;
- height: 100%;
- & > * {
- height: 100%;
- }
- .faqGenContent {
- flex: 1 1 auto;
- position: relative;
- .faqGenContentMessages {
- @include absolutes;
- @include flex(column, nowrap, normal, normal);
- > * {
- padding: 8px 24px;
- }
- .faqGenTitle {
- @include flex(row, nowrap, center, flex-start);
- height: 60px;
- border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
- }
- .faqGenContentButtonGroup {
- @include flex(row, nowrap, center, center);
- height: 60px;
- }
- .faqGenInput {
- .faqGenFileUpload {
- @include flex(row, nowrap, center, center);
- }
- }
- .faqGenResult {
- overflow-y: auto;
- }
- }
- }
-}
diff --git a/ProductivitySuite/ui/react/src/components/File_Display/FileDisplay.module.scss b/ProductivitySuite/ui/react/src/components/File_Display/FileDisplay.module.scss
new file mode 100644
index 0000000000..46cb667e6e
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/File_Display/FileDisplay.module.scss
@@ -0,0 +1,44 @@
+.file {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-start;
+ padding: 5px 10px;
+ border-radius: 5px;
+ margin-right: 0.5rem;
+ margin-bottom: 0.5rem;
+
+ button {
+ margin-left: 0.5rem;
+ }
+
+ .iconWrap {
+ border: none;
+ border-radius: 6px;
+ margin-right: 0.5rem;
+ width: 30px;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .fileName {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 1; // Limits to 2 lines
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: left;
+ max-width: 200px;
+ width: 100%;
+ font-size: 12px;
+ font-weight: 500;
+ }
+
+ .fileExt {
+ font-size: 11px;
+ text-align: left;
+ margin-top: -2px;
+ }
+}
diff --git a/ProductivitySuite/ui/react/src/components/File_Display/FileDisplay.tsx b/ProductivitySuite/ui/react/src/components/File_Display/FileDisplay.tsx
new file mode 100644
index 0000000000..7aaed02320
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/File_Display/FileDisplay.tsx
@@ -0,0 +1,51 @@
+import { IconButton } from "@mui/material";
+import { Close, TaskOutlined, Language } from "@mui/icons-material";
+import styled from "styled-components";
+import styles from "./FileDisplay.module.scss";
+
+const FileWrap = styled("div")(({ theme }) => ({
+ ...theme.customStyles.fileInput.file,
+ ...theme.customStyles.gradientShadow,
+}));
+
+const IconWrap = styled("div")(({ theme }) => ({
+ ...theme.customStyles.sources.iconWrap,
+}));
+
+interface FileProps {
+ file: File;
+ index: number;
+ remove?: (value: number) => void;
+ isWeb?: boolean;
+}
+
+const FileDispaly: React.FC = ({ file, index, remove, isWeb }) => {
+ if (!file) return;
+
+ let fileExtension = file.name.split(".").pop()?.toLowerCase();
+ let fileName = isWeb ? file.name : file.name.split(".").shift();
+
+ return (
+
+
+
+
+
+
+
+ {fileName}
+
+ {!isWeb &&
.{fileExtension}
}
+
+
+ {remove && (
+ remove(index)}>
+
+
+ )}
+ {isWeb && }
+
+ );
+};
+
+export default FileDispaly;
diff --git a/ProductivitySuite/ui/react/src/components/File_Input/FileInput.module.scss b/ProductivitySuite/ui/react/src/components/File_Input/FileInput.module.scss
new file mode 100644
index 0000000000..273afe7e5d
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/File_Input/FileInput.module.scss
@@ -0,0 +1,69 @@
+.fileInputWrapper {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .upload {
+ margin-left: 0.5rem;
+ }
+
+ .inputWrapper {
+ padding: 1rem;
+ text-align: center;
+ box-shadow: none;
+ border-radius: 8px;
+ width: 100%;
+ position: relative;
+ }
+
+ .expand {
+ width: 25px;
+ height: 25px;
+ border-radius: 25px;
+ min-width: unset;
+ border-width: 1px;
+ border-style: solid;
+ transition: transform 0.5s;
+ transform: rotate(0deg);
+ transform-origin: center;
+ margin-left: -12.5px;
+ margin-top: -20px;
+ position: absolute;
+ bottom: -12.5px;
+ z-index: 8;
+
+ &.open {
+ transform: rotate(180deg);
+ }
+ }
+}
+
+.previewFiles {
+ margin-bottom: 0.5rem;
+
+ .fileList {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+
+ label {
+ margin-top: 0.5rem;
+ }
+}
+
+.details {
+ max-height: 0px;
+ transition: max-height 0.4s;
+ overflow: hidden;
+
+ &.detailsOpen {
+ max-height: 400px;
+ }
+}
+
+.detailGap {
+ margin-top: 10px;
+}
diff --git a/ProductivitySuite/ui/react/src/components/File_Input/FileInput.tsx b/ProductivitySuite/ui/react/src/components/File_Input/FileInput.tsx
new file mode 100644
index 0000000000..a5affc0797
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/File_Input/FileInput.tsx
@@ -0,0 +1,374 @@
+import React, { useEffect, useReducer, useRef, useState } from "react";
+import {
+ Box,
+ Button,
+ Typography,
+ Paper,
+ IconButton,
+ styled,
+} from "@mui/material";
+import {
+ UploadFile,
+ Close,
+ ExpandMore,
+ FileUploadOutlined,
+} from "@mui/icons-material";
+import styles from "./FileInput.module.scss";
+import {
+ NotificationSeverity,
+ notify,
+} from "@components/Notification/Notification";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import {
+ conversationSelector,
+ setSourceFiles,
+ setUploadInProgress,
+ uploadFile,
+} from "@redux/Conversation/ConversationSlice";
+import ModalBox from "@shared/ModalBox/ModalBox";
+import { OutlineButton, SolidButton } from "@shared/ActionButtons";
+import { Link } from "react-router-dom";
+import FileDispaly from "@components/File_Display/FileDisplay";
+import ProgressIcon from "@components/ProgressIcon/ProgressIcon";
+
+const ExpandButton = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.promptExpandButton,
+}));
+
+interface FileWithPreview {
+ file: File;
+ preview: string;
+}
+
+interface FileInputProps {
+ imageInput?: boolean;
+ maxFileCount?: number;
+ confirmationModal?: boolean;
+ dataManagement?: boolean;
+}
+
+const imageExtensions = ["jpg", "jpeg", "png", "gif"];
+const docExtensions = ["txt"];
+const dataExtensions = [
+ "txt",
+ "pdf",
+ "csv",
+ "xls",
+ "xlsx",
+ "json" /*"doc", "docx", "md", "ppt", "pptx", "html", "xml", "xsl", "xslt", "rtf", "v", "sv"*/,
+];
+const maxImageSize = 3 * 1024 * 1024; // 3MB
+const maxDocSize = 200 * 1024 * 1024; // 200MB
+
+const FileInputWrapper = styled(Paper)(({ theme }) => ({
+ ...theme.customStyles.fileInput.wrapper,
+}));
+
+const FileInput: React.FC = ({
+ maxFileCount = 5,
+ imageInput,
+ dataManagement,
+}) => {
+ const { model, models, useCase, filesInDataSource, uploadInProgress, type } =
+ useAppSelector(conversationSelector);
+ // const { filesInDataManagement, uploadInProgress } = useAppSelector(dataManagementSelector);
+
+ const dispatch = useAppDispatch();
+ const [confirmUpload, setConfirmUpload] = useState(false);
+ const [filesToUpload, setFilesToUpload] = useState<
+ (FileWithPreview | File)[]
+ >([]);
+ const [details, showDetails] = useState(filesToUpload.length === 0);
+
+ const inputRef = useRef(null);
+
+ const extensions = imageInput
+ ? imageExtensions
+ : dataManagement
+ ? dataExtensions
+ : docExtensions;
+ const maxSize = imageInput ? maxImageSize : maxDocSize;
+
+ const [insightToken, setInsightToken] = useState(0);
+
+ useEffect(() => {
+ showDetails(filesToUpload.length === 0);
+
+ // summary / faq
+ if (!dataManagement && filesToUpload.length > 0) {
+ dispatch(setSourceFiles(filesToUpload));
+ }
+ }, [filesToUpload]);
+
+ useEffect(() => {
+ // model sets insight token in summary/faq
+ if (!dataManagement) {
+ let selectedModel = models.find(
+ (thisModel) => thisModel.model_name === model,
+ );
+ if (selectedModel) setInsightToken(selectedModel.maxToken);
+ }
+ }, [model, models]);
+
+ useEffect(() => {
+ setFilesToUpload([]);
+ dispatch(setSourceFiles([]));
+ }, [type]);
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault();
+ const droppedFiles = Array.from(e.dataTransfer.files);
+ validateFiles(droppedFiles);
+ };
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ if (e.target.files) {
+ const selectedFiles = Array.from(e.target.files);
+ const validated = validateFiles(selectedFiles);
+ if (validated) e.target.value = ""; // Clear input
+ }
+ };
+
+ const validateFiles = (newFiles: File[]) => {
+ if (newFiles.length + filesToUpload.length > maxFileCount) {
+ notify(
+ `You can only upload a maximum of ${maxFileCount} file${maxFileCount > 1 ? "s" : ""}.`,
+ NotificationSeverity.ERROR,
+ );
+ return;
+ }
+
+ const validFiles = newFiles.filter((file) => {
+ const fileExtension = file.name.split(".").pop()?.toLowerCase();
+ const isSupportedExtension = extensions.includes(fileExtension || "");
+ const isWithinSizeLimit = file.size <= maxSize;
+
+ const compareTo = dataManagement ? filesInDataSource : filesToUpload;
+
+ let duplicate = compareTo.some((f: any) => {
+ return f.name === file.name;
+ });
+
+ // duplicate file check, currently data management only (summary/faq single file)
+ if (duplicate) {
+ notify(
+ `File "${file.name}" is already added.`,
+ NotificationSeverity.ERROR,
+ );
+ return false;
+ }
+
+ if (!isSupportedExtension) {
+ notify(
+ `File "${file.name}" has an unsupported file type.`,
+ NotificationSeverity.ERROR,
+ );
+ return false;
+ }
+
+ if (!isWithinSizeLimit) {
+ notify(
+ `File "${file.name}" exceeds the maximum size limit of ${imageInput ? "3MB" : "200MB"}.`,
+ NotificationSeverity.ERROR,
+ );
+ return false;
+ }
+
+ return isSupportedExtension && isWithinSizeLimit;
+ });
+
+ if (validFiles.length > 0) {
+ addToQueue(validFiles);
+ }
+
+ return true;
+ };
+
+ const addToQueue = async (newFiles: File[]) => {
+ const filteredFiles = newFiles.filter((file: File | FileWithPreview) => {
+ let activeFile = "file" in file ? file.file : file;
+ return !filesToUpload.some((f: File | FileWithPreview) => {
+ let comparedFile = "file" in f ? f.file : f;
+ return comparedFile.name === activeFile.name;
+ });
+ });
+
+ const filesWithPreview = filteredFiles.map((file) => ({
+ file,
+ preview: URL.createObjectURL(file),
+ }));
+
+ setFilesToUpload([...filesToUpload, ...filesWithPreview]);
+ };
+
+ const removeFile = (index: number) => {
+ let updatedFiles = filesToUpload.filter(
+ (file, fileIndex) => index !== fileIndex,
+ );
+ setFilesToUpload(updatedFiles);
+ };
+
+ const uploadFiles = async () => {
+ dispatch(setUploadInProgress(true));
+
+ const responses = await Promise.all(
+ filesToUpload.map((file: any) => {
+ dispatch(uploadFile({ file: file.file }));
+ }),
+ );
+
+ dispatch(setUploadInProgress(false));
+
+ setConfirmUpload(false);
+ setFilesToUpload([]);
+ };
+
+ const showConfirmUpload = () => {
+ setConfirmUpload(true);
+ };
+
+ const filePreview = () => {
+ if (filesToUpload.length > 0) {
+ return (
+
+
+ {filesToUpload.map((file, fileIndex) => {
+ let activeFile = "file" in file ? file.file : file;
+ return (
+
+
+
+ );
+ })}
+
+
+ );
+ } else {
+ return (
+
+ Upload or Drop Files Here
+
+ );
+ }
+ };
+
+ const renderConfirmUpload = () => {
+ if (confirmUpload) {
+ return (
+
+
+ Uploading files
+ setConfirmUpload(false)}>
+
+
+
+
+
+ I hereby certify that the content uploaded is free from any
+ personally identifiable information or other private data that
+ would violate applicable privacy laws and regulations.
+
+
+ uploadFiles()}>
+ Agree and Continue
+
+ setConfirmUpload(false)}>
+ Cancel
+
+
+
+
+ );
+ }
+ };
+
+ if (uploadInProgress) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ e.preventDefault()}
+ className={styles.inputWrapper}
+ >
+ {filePreview()}
+
+
+ {filesToUpload.length !== maxFileCount && (
+ inputRef.current?.click()}>
+ Browse Files
+
+
+ )}
+
+ {dataManagement && (
+
+ Upload
+
+ )}
+
+
+ {filesToUpload.length > 0 && (
+ showDetails(!details)}
+ >
+
+
+ )}
+
+
+
+ Limit {imageInput ? "3MB" : "200MB"} per file.
+
+
+
+ Valid file formats are {extensions.join(", ").toUpperCase()}.
+
+
+
+ You can select maximum of {maxFileCount} valid file
+ {maxFileCount > 1 ? "s" : ""}.
+
+
+ {!dataManagement && (
+
+ Max supported input tokens for {imageInput && "images"} data
+ insight is{" "}
+ {insightToken >= 1000 ? insightToken / 1000 + "K" : insightToken}
+
+ )}
+
+
+
+ {renderConfirmUpload()}
+
+ );
+};
+
+export default FileInput;
diff --git a/ProductivitySuite/ui/react/src/components/Header/Header.module.scss b/ProductivitySuite/ui/react/src/components/Header/Header.module.scss
new file mode 100644
index 0000000000..ca93268a10
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Header/Header.module.scss
@@ -0,0 +1,136 @@
+.header {
+ height: var(--header-height);
+ backdrop-filter: blur(5px);
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ width: 100%;
+ padding: var(--header-gutter);
+ position: relative;
+ z-index: 999;
+}
+
+.viewContext {
+ display: inline-flex;
+ max-width: 200px;
+
+ &.titleWrap {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ :global {
+ svg {
+ min-width: 30px;
+ }
+ }
+ }
+
+ &.capitalize {
+ text-transform: capitalize;
+ }
+
+ @media screen and (max-width: 900px) {
+ display: none;
+
+ &.titleWrap {
+ display: none;
+ }
+ }
+}
+
+.sideWrapper {
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ z-index: 999;
+ width: 50px;
+ margin-right: calc(var(--header-gutter) * 2);
+ min-width: 0px;
+ max-width: var(--sidebar-width);
+ transition:
+ width 0.3s,
+ min-width 0.3s;
+
+ .chatCopy {
+ opacity: 0;
+ max-width: 0;
+ transition:
+ opacity 0.3s,
+ max-width 0.3s;
+ font-size: 0.75rem;
+ margin-right: 0.5rem;
+ white-space: nowrap;
+ }
+
+ .chatWrapper {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ }
+
+ &.sideWrapperOpen {
+ width: calc(var(--sidebar-width) - (var(--header-gutter) * 2));
+ min-width: calc(var(--sidebar-width) - (var(--header-gutter) * 2));
+
+ .chatCopy {
+ max-width: 100px; // enough to show the text
+ opacity: 1;
+ }
+ }
+}
+
+.rightSide {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+}
+
+.rightActions {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.companyName {
+ font-weight: 600;
+ @media screen and (max-width: 899px) {
+ position: absolute;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+}
+
+.desktopUser {
+ display: none;
+ @media screen and (min-width: 900px) {
+ display: inline-block;
+ }
+}
+
+.accessDropDown {
+ :global {
+ .MuiList-root {
+ padding: 0;
+
+ .MuiButtonBase-root {
+ padding: 0;
+ margin-left: -10px;
+ padding: 0 10px;
+
+ .MuiListItemText-root {
+ margin: 0px;
+ }
+
+ .MuiTypography-root {
+ font-size: 12px !important;
+ font-style: italic;
+ }
+ }
+ }
+ }
+}
diff --git a/ProductivitySuite/ui/react/src/components/Header/Header.tsx b/ProductivitySuite/ui/react/src/components/Header/Header.tsx
new file mode 100644
index 0000000000..f3931982cf
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Header/Header.tsx
@@ -0,0 +1,225 @@
+import { useEffect, useRef, useState } from "react";
+import { styled } from "@mui/material/styles";
+import { Link, useNavigate } from "react-router-dom";
+import config from "@root/config";
+
+import styles from "./Header.module.scss";
+import { Box, IconButton, Tooltip, Typography } from "@mui/material";
+import { SideBar } from "@components/SideBar/SideBar";
+import DropDown from "@components/DropDown/DropDown";
+import ThemeToggle from "@components/Header_ThemeToggle/ThemeToggle";
+import ViewSidebarOutlinedIcon from "@mui/icons-material/ViewSidebarOutlined";
+import Create from "@mui/icons-material/Create";
+import Home from "@mui/icons-material/Home";
+import ShareOutlinedIcon from "@mui/icons-material/ShareOutlined";
+import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
+import ChatBubbleIcon from "@icons/ChatBubble";
+
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import { userSelector } from "@redux/User/userSlice";
+import {
+ Message,
+ MessageRole,
+ UseCase,
+} from "@redux/Conversation/Conversation";
+import {
+ conversationSelector,
+ setUseCase,
+} from "@redux/Conversation/ConversationSlice";
+import DownloadChat from "@components/Header_DownloadChat/DownloadChat";
+
+interface HeaderProps {
+ asideOpen: boolean;
+ setAsideOpen: (open: boolean) => void;
+ chatView?: boolean;
+ historyView?: boolean;
+ dataView?: boolean;
+}
+
+interface AvailableUseCase {
+ name: string;
+ value: string;
+}
+
+const HeaderWrapper = styled(Box)(({ theme }) => ({
+ ...theme.customStyles.header,
+}));
+
+const Header: React.FC = ({
+ asideOpen,
+ setAsideOpen,
+ chatView,
+ historyView,
+ dataView,
+}) => {
+ const { companyName } = config;
+
+ const sideBarRef = useRef(null);
+ const toggleRef = useRef(null);
+
+ const navigate = useNavigate();
+
+ const dispatch = useAppDispatch();
+ const { role, name } = useAppSelector(userSelector);
+ const { selectedConversationHistory, type } =
+ useAppSelector(conversationSelector);
+
+ const [currentTopic, setCurrentTopic] = useState("");
+
+ useEffect(() => {
+ if (
+ !selectedConversationHistory ||
+ selectedConversationHistory.length === 0
+ ) {
+ setCurrentTopic("");
+ return;
+ }
+ const firstUserPrompt = selectedConversationHistory.find(
+ (message: Message) => message.role === MessageRole.User,
+ );
+ if (firstUserPrompt) setCurrentTopic(firstUserPrompt.content);
+ }, [selectedConversationHistory]);
+
+ const handleChange = (value: string) => {
+ dispatch(setUseCase(value));
+ };
+
+ const newChat = () => {
+ navigate("/");
+ setAsideOpen(false);
+ };
+
+ const handleClickOutside = (event: MouseEvent) => {
+ if (
+ sideBarRef.current &&
+ toggleRef.current &&
+ !sideBarRef.current.contains(event.target as Node) &&
+ !toggleRef.current.contains(event.target as Node)
+ ) {
+ setAsideOpen(false);
+ }
+ };
+
+ useEffect(() => {
+ if (asideOpen) {
+ document.addEventListener("mousedown", handleClickOutside);
+ return () =>
+ document.removeEventListener("mousedown", handleClickOutside);
+ }
+ }, [asideOpen]);
+
+ const userDetails = () => {
+ return (
+
+ {name}
+
+ );
+ };
+
+ const getTitle = () => {
+ if (historyView)
+ return (
+
+
+ Your Chat History
+
+ );
+
+ if (dataView)
+ return (
+
+ Data Source Management
+
+ );
+
+ if (chatView) {
+ if (type !== "chat" && !currentTopic) {
+ return (
+
+ {type}
+
+ );
+ } else {
+ return (
+
+
+
+ {currentTopic}
+
+
+ );
+ }
+ }
+ };
+
+ return (
+
+
+
+ setAsideOpen(!asideOpen)}>
+
+
+
+
+ {/* New Chat */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {companyName}
+
+
+ {getTitle()}
+
+
+ {chatView && (
+ <>
+ {/*
+
+ */}
+
+
+ >
+ )}
+
+ {/* {chatView && { }}> } */}
+
+
+
+
+
+ {userDetails()}
+
+
+
+ );
+};
+
+export default Header;
diff --git a/ProductivitySuite/ui/react/src/components/Header_DownloadChat/DownloadChat.tsx b/ProductivitySuite/ui/react/src/components/Header_DownloadChat/DownloadChat.tsx
new file mode 100644
index 0000000000..0ed8a8c121
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Header_DownloadChat/DownloadChat.tsx
@@ -0,0 +1,74 @@
+import { FileDownloadOutlined } from "@mui/icons-material";
+import { IconButton, Tooltip } from "@mui/material";
+import { conversationSelector } from "@redux/Conversation/ConversationSlice";
+import { useAppSelector } from "@redux/store";
+import { useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+
+const DownloadChat = () => {
+ const { selectedConversationHistory, type, model, token, temperature } =
+ useAppSelector(conversationSelector);
+ const [url, setUrl] = useState(undefined);
+ const [fileName, setFileName] = useState("");
+
+ const safeBtoa = (str: string) => {
+ const encoder = new TextEncoder();
+ const uint8Array = encoder.encode(str);
+ let binaryString = "";
+ for (let i = 0; i < uint8Array.length; i++) {
+ binaryString += String.fromCharCode(uint8Array[i]);
+ }
+ return btoa(binaryString);
+ };
+
+ useEffect(() => {
+ if (selectedConversationHistory.length === 0) return;
+
+ //TODO: if we end up with a systemPrompt for code change this
+ const userPromptIndex = type === "code" ? 0 : 1;
+
+ const conversationObject = {
+ model,
+ token,
+ temperature,
+ messages: [...selectedConversationHistory],
+ type,
+ };
+
+ const newUrl = `data:application/json;charset=utf-8;base64,${safeBtoa(JSON.stringify(conversationObject))}`;
+
+ if (
+ selectedConversationHistory &&
+ selectedConversationHistory.length > 0 &&
+ selectedConversationHistory[userPromptIndex]
+ ) {
+ const firstPrompt = selectedConversationHistory[userPromptIndex].content; // Assuming content is a string
+ if (firstPrompt) {
+ const newFileName = firstPrompt.split(" ").slice(0, 4).join("_");
+ setUrl(newUrl);
+ setFileName(newFileName.toLowerCase());
+ }
+ }
+ }, [selectedConversationHistory]);
+
+ //TODO: only support download for chat for now
+ return (
+ url &&
+ type === "chat" && (
+
+
+
+
+
+
+
+ )
+ );
+};
+
+export default DownloadChat;
diff --git a/ProductivitySuite/ui/react/src/components/Header_ThemeToggle/ThemeToggle.module.scss b/ProductivitySuite/ui/react/src/components/Header_ThemeToggle/ThemeToggle.module.scss
new file mode 100644
index 0000000000..1d69292d92
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Header_ThemeToggle/ThemeToggle.module.scss
@@ -0,0 +1,65 @@
+.toggleWrapper {
+ position: relative;
+ margin-right: 10px;
+ display: flex;
+ align-items: center;
+
+ .toggle {
+ width: 100px;
+ height: 34px;
+ padding: 7px;
+ }
+
+ .copy {
+ position: absolute;
+ z-index: 99;
+ margin: 0 26px;
+ font-size: 14px;
+ }
+
+ :global {
+ .MuiSwitch-switchBase {
+ margin: 1px;
+ padding: 0;
+ transform: translateX(6px);
+ transition: transform 0.3s;
+
+ &.Mui-checked {
+ color: #fff;
+ transform: translateX(62px);
+
+ .MuiSwitch-track {
+ opacity: 1;
+ }
+ }
+ }
+
+ .MuiSwitch-track {
+ opacity: 1;
+ height: 30px;
+ border-radius: 30px;
+ margin-top: -5px;
+ background-color: transparent !important;
+ }
+
+ .MuiSwitch-thumb {
+ // background-color: transparent !important;
+ width: 26px;
+ height: 26px;
+ position: relative;
+ margin-top: 3px;
+ margin-left: 2px;
+ box-shadow: none;
+ &::before {
+ content: "";
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+ background-repeat: no-repeat;
+ background-position: center;
+ }
+ }
+ }
+}
diff --git a/ProductivitySuite/ui/react/src/components/Header_ThemeToggle/ThemeToggle.tsx b/ProductivitySuite/ui/react/src/components/Header_ThemeToggle/ThemeToggle.tsx
new file mode 100644
index 0000000000..69987702f0
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Header_ThemeToggle/ThemeToggle.tsx
@@ -0,0 +1,48 @@
+import React, { useContext } from "react";
+import { styled } from "@mui/material/styles";
+import { Switch, Typography, Box } from "@mui/material";
+import { ThemeContext } from "@contexts/ThemeContext";
+import styles from "./ThemeToggle.module.scss";
+
+const MaterialUISwitch = styled(Switch)(({ theme }) => ({
+ ...theme.customStyles.themeToggle,
+}));
+
+const ThemeToggle: React.FC = () => {
+ const { darkMode, toggleTheme } = useContext(ThemeContext);
+ const [checked, setChecked] = React.useState(darkMode);
+
+ const handleChange = (event: React.ChangeEvent) => {
+ setChecked(event.target.checked);
+ toggleTheme();
+ };
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (event.key === "Enter" || event.key === " ") {
+ handleChange({
+ target: { checked: !checked },
+ } as React.ChangeEvent);
+ }
+ };
+
+ return (
+
+
+ {checked ? "Dark" : "Light"}
+
+
+
+ );
+};
+
+export default ThemeToggle;
diff --git a/ProductivitySuite/ui/react/src/components/Message/conversationMessage.module.scss b/ProductivitySuite/ui/react/src/components/Message/conversationMessage.module.scss
deleted file mode 100644
index b006495530..0000000000
--- a/ProductivitySuite/ui/react/src/components/Message/conversationMessage.module.scss
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-@import "../../styles/styles";
-
-.conversationMessage {
- @include flex(column, nowrap, flex-start, flex-start);
- margin-top: 16px;
- padding: 0 32px;
- width: 100%;
-
- & > * {
- width: 100%;
- }
-}
diff --git a/ProductivitySuite/ui/react/src/components/Message/conversationMessage.tsx b/ProductivitySuite/ui/react/src/components/Message/conversationMessage.tsx
deleted file mode 100644
index e7d5cdae75..0000000000
--- a/ProductivitySuite/ui/react/src/components/Message/conversationMessage.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-import { IconAi, IconPlus, IconUser } from "@tabler/icons-react"
-import style from "./conversationMessage.module.scss"
-import { ActionIcon, Group, Text, Tooltip } from "@mantine/core"
-import { DateTime } from "luxon"
-import Markdown from "../Shared/Markdown/Markdown"
-import { addPrompt } from "../../redux/Prompt/PromptSlice"
-import { useAppDispatch } from "../../redux/store"
-
-export interface ConversationMessageProps {
- message: string
- human: boolean
- date: number
-}
-
-export function ConversationMessage({ human, message, date }: ConversationMessageProps) {
- const dispatch = useAppDispatch();
- const dateFormat = () => {
- // console.log(date)
- // console.log(new Date(date))
- return DateTime.fromJSDate(new Date(date)).toLocaleString(DateTime.DATETIME_MED)
- }
-
- return (
-
-
- {human && }
- {!human && }
-
-
-
- {human && "You"} {!human && "Assistant"}
-
-
- {dateFormat()}
-
-
- {human && (
-
- dispatch(addPrompt({promptText:message}))
- } size={20} variant="filled">
-
-
- )
- }
-
-
- {human? message : ()}
-
-
- {/*
- {human && }
- {!human && }
-
-
-
{message}
*/}
-
- )
-}
diff --git a/ProductivitySuite/ui/react/src/components/Notification/Notification.tsx b/ProductivitySuite/ui/react/src/components/Notification/Notification.tsx
new file mode 100644
index 0000000000..f3948c52f9
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Notification/Notification.tsx
@@ -0,0 +1,144 @@
+import { AlertColor, IconButton, styled } from "@mui/material";
+import {
+ SnackbarProvider,
+ useSnackbar,
+ MaterialDesignContent,
+ closeSnackbar,
+} from "notistack";
+import { useEffect } from "react";
+import { Subject } from "rxjs";
+import {
+ TaskAlt,
+ WarningAmberOutlined,
+ ErrorOutlineOutlined,
+ InfoOutlined,
+ Close,
+} from "@mui/icons-material";
+
+interface NotificationDataProps {
+ message: string;
+ variant: AlertColor;
+}
+
+type NotificationSeverity = "error" | "info" | "success" | "warning";
+
+export const NotificationSeverity = {
+ SUCCESS: "success" as NotificationSeverity,
+ ERROR: "error" as NotificationSeverity,
+ WARNING: "warning" as NotificationSeverity,
+ INFO: "info" as NotificationSeverity,
+};
+
+const severityColor = (variant: string) => {
+ switch (variant) {
+ case "success":
+ return "#388e3c";
+ case "error":
+ return "#d32f2f";
+ case "warning":
+ return "#f57c00";
+ case "info":
+ return "#0288d1";
+ default:
+ return "rgba(0, 0, 0, 0.87)";
+ }
+};
+
+const StyledMaterialDesignContent = styled(MaterialDesignContent)<{
+ severity: AlertColor;
+}>(({ variant }) => ({
+ backgroundColor: (() => {
+ switch (variant) {
+ case "success":
+ return "rgb(225,238,226)";
+ case "error":
+ return "rgb(248,224,224)";
+ case "warning":
+ return "rgb(254,235,217)";
+ case "info":
+ return "rgb(217,237,248)";
+ default:
+ return "rgb(225,238,226)";
+ }
+ })(),
+ border: `1px solid ${severityColor(variant)}`,
+ color: severityColor(variant),
+ ".MuiAlert-action": {
+ paddingTop: 0,
+ scale: 0.8,
+ borderLeft: `1px solid ${severityColor(variant)}`,
+ marginLeft: "1rem",
+ },
+ svg: {
+ marginRight: "1rem",
+ },
+ "button svg": {
+ marginRight: "0",
+ path: {
+ fill: severityColor(variant),
+ },
+ },
+}));
+
+const CloseIcon = styled(IconButton)(() => ({
+ minWidth: "unset",
+}));
+
+const Notify = new Subject();
+
+export const notify = (message: string, variant: AlertColor) => {
+ if (!variant) variant = NotificationSeverity.SUCCESS;
+ Notify.next({ message, variant });
+};
+
+const NotificationComponent = () => {
+ const { enqueueSnackbar } = useSnackbar();
+
+ useEffect(() => {
+ const subscription = Notify.subscribe({
+ next: (notification) => {
+ enqueueSnackbar(notification.message, {
+ variant: notification.variant,
+ action: (key) => (
+ closeSnackbar(key)}
+ variant={notification.variant}
+ >
+
+
+ ),
+ });
+ },
+ });
+
+ return () => subscription.unsubscribe();
+ }, []);
+
+ return <>>;
+};
+
+const Notification = () => {
+ return (
+ ,
+ warning: ,
+ error: ,
+ info: ,
+ }}
+ Components={{
+ success: StyledMaterialDesignContent,
+ warning: StyledMaterialDesignContent,
+ error: StyledMaterialDesignContent,
+ info: StyledMaterialDesignContent,
+ }}
+ >
+
+
+ );
+};
+
+export default Notification;
diff --git a/ProductivitySuite/ui/react/src/components/PrimaryInput/PrimaryInput.module.scss b/ProductivitySuite/ui/react/src/components/PrimaryInput/PrimaryInput.module.scss
new file mode 100644
index 0000000000..184c4d79c4
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/PrimaryInput/PrimaryInput.module.scss
@@ -0,0 +1,44 @@
+.inputWrapper {
+ position: relative;
+}
+
+.primaryInput {
+ border-radius: var(--input-radius);
+ overflow: hidden;
+ position: relative;
+ display: flex;
+
+ .inputActions {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ position: absolute;
+ right: 10px;
+ bottom: 10px;
+ }
+
+ .circleButton {
+ border-radius: 40px;
+ width: 40px;
+ height: 40px;
+ min-width: 40px;
+ margin-left: 10px;
+ }
+
+ .textAreaAuto {
+ font-family: "Inter", serif;
+ padding: var(--header-gutter) 100px var(--header-gutter) var(--header-gutter);
+ border: 0;
+ width: 100%;
+ resize: none;
+ background-color: transparent;
+
+ &:focus {
+ outline: none;
+ }
+
+ &.summaryInput {
+ padding: var(--header-gutter) 70px var(--header-gutter) var(--header-gutter);
+ }
+ }
+}
diff --git a/ProductivitySuite/ui/react/src/components/PrimaryInput/PrimaryInput.tsx b/ProductivitySuite/ui/react/src/components/PrimaryInput/PrimaryInput.tsx
new file mode 100644
index 0000000000..4e05045edb
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/PrimaryInput/PrimaryInput.tsx
@@ -0,0 +1,197 @@
+import { useEffect, useRef, useState } from "react";
+import { Box, Button, styled, TextareaAutosize } from "@mui/material";
+import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
+import styles from "./PrimaryInput.module.scss";
+import { Stop } from "@mui/icons-material";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import {
+ abortStream,
+ conversationSelector,
+ saveConversationtoDatabase,
+ setSourceFiles,
+ setSourceLinks,
+} from "@redux/Conversation/ConversationSlice";
+import AudioInput from "@components/PrimaryInput_AudioInput/AudioInput";
+import PromptSelector from "@components/PrimparyInput_PromptSelector/PromptSelector";
+import {
+ NotificationSeverity,
+ notify,
+} from "@components/Notification/Notification";
+
+const InputWrapper = styled(Box)(({ theme }) => ({
+ ...theme.customStyles.primaryInput.inputWrapper,
+}));
+
+const TextInput = styled(TextareaAutosize)(({ theme }) => ({
+ ...theme.customStyles.primaryInput.textInput,
+}));
+
+const CircleButton = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.primaryInput.circleButton,
+}));
+
+interface PrimaryInputProps {
+ onSend: (messageContent: string) => Promise;
+ type?: string;
+ home?: boolean;
+}
+
+const PrimaryInput: React.FC = ({
+ onSend,
+ home = false,
+}) => {
+ const {
+ onGoingResult,
+ type,
+ selectedConversationId,
+ sourceLinks,
+ sourceFiles,
+ } = useAppSelector(conversationSelector);
+ const dispatch = useAppDispatch();
+
+ const [promptText, setPromptText] = useState("");
+ const clearText = useRef(true);
+
+ const isSummary = type === "summary";
+ const isFaq = type === "faq";
+
+ useEffect(() => {
+ if (clearText.current) setPromptText("");
+ clearText.current = true;
+ }, [type, sourceFiles, sourceLinks]);
+
+ const handleSubmit = () => {
+ if (
+ (isSummary || isFaq) &&
+ sourceLinks &&
+ sourceLinks.length === 0 &&
+ sourceFiles &&
+ sourceFiles.length === 0 &&
+ promptText === ""
+ ) {
+ notify("Please provide content process", NotificationSeverity.ERROR);
+ return;
+ } else if (!(isSummary || isFaq) && promptText === "") {
+ notify("Please provide a message", NotificationSeverity.ERROR);
+ return;
+ }
+
+ let textToSend = promptText;
+ onSend(textToSend);
+ setPromptText("");
+ };
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (!event.shiftKey && event.key === "Enter") {
+ handleSubmit();
+ }
+ };
+
+ const updatePromptText = (value: string) => {
+ setPromptText(value);
+ if (sourceFiles.length > 0) {
+ clearText.current = false;
+ dispatch(setSourceFiles([]));
+ }
+ if (sourceLinks.length > 0) {
+ clearText.current = false;
+ dispatch(setSourceLinks([]));
+ }
+ };
+
+ const cancelStream = () => {
+ dispatch(abortStream());
+ if (type === "chat") {
+ dispatch(
+ saveConversationtoDatabase({
+ conversation: { id: selectedConversationId },
+ }),
+ );
+ }
+ };
+
+ const isActive = () => {
+ if ((isSummary || isFaq) && sourceFiles.length > 0) {
+ return true;
+ } else if (promptText !== "") return true;
+ return false;
+ };
+
+ const submitButton = () => {
+ if (!onGoingResult) {
+ return (
+
+
+
+ );
+ }
+ return;
+ };
+
+ const placeHolderCopy = () => {
+ if (home && (isSummary || isFaq)) return "Enter text here or sources below";
+ else return "Enter your message";
+ };
+
+ const renderInput = () => {
+ if (!home && onGoingResult && (isSummary || isFaq)) {
+ return (
+
+
+
+
+
+ );
+ } else if ((!home && !isSummary && !isFaq) || home) {
+ return (
+
+
+ ) =>
+ updatePromptText(e.target.value)
+ }
+ onKeyDown={handleKeyDown}
+ sx={{
+ resize: "none",
+ backgroundColor: "transparent",
+ }}
+ />
+
+
+
+
+ {onGoingResult && (
+
+
+
+ )}
+
+ {submitButton()}
+
+
+
+ {home && !isSummary && !isFaq && (
+
+ )}
+
+ );
+ }
+ };
+
+ return renderInput();
+};
+
+export default PrimaryInput;
diff --git a/ProductivitySuite/ui/react/src/components/PrimaryInput_AudioInput/AudioInput.tsx b/ProductivitySuite/ui/react/src/components/PrimaryInput_AudioInput/AudioInput.tsx
new file mode 100644
index 0000000000..d304ebd38d
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/PrimaryInput_AudioInput/AudioInput.tsx
@@ -0,0 +1,85 @@
+import { Mic } from "@mui/icons-material";
+import { Button, styled, Tooltip } from "@mui/material";
+import { useState } from "react";
+import styles from "@components/PrimaryInput/PrimaryInput.module.scss";
+import {
+ NotificationSeverity,
+ notify,
+} from "@components/Notification/Notification";
+import { useAppSelector } from "@redux/store";
+import { conversationSelector } from "@redux/Conversation/ConversationSlice";
+import ProgressIcon from "@components/ProgressIcon/ProgressIcon";
+
+interface AudioInputProps {
+ setSearchText: (value: string) => void;
+}
+
+const AudioButton = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.audioEditButton,
+}));
+
+const AudioInput: React.FC = ({ setSearchText }) => {
+ const isSpeechRecognitionSupported =
+ ("webkitSpeechRecognition" in window || "SpeechRecognition" in window) &&
+ window.isSecureContext;
+
+ const { type } = useAppSelector(conversationSelector);
+ const [isListening, setIsListening] = useState(false);
+
+ const handleMicClick = () => {
+ const SpeechRecognition =
+ (window as any).SpeechRecognition ||
+ (window as any).webkitSpeechRecognition;
+ const recognition = new SpeechRecognition();
+ recognition.lang = "en-US"; // Set language for recognition
+ recognition.interimResults = false; // Only process final results
+
+ if (!isListening) {
+ setIsListening(true);
+ recognition.start();
+
+ recognition.onresult = (event: SpeechRecognitionEvent) => {
+ const transcript = event.results[0][0].transcript;
+ setSearchText(transcript); // Update search text with recognized speech
+ setIsListening(false);
+ };
+
+ recognition.onerror = (event: SpeechRecognitionErrorEvent) => {
+ notify(
+ `Speech recognition error:${event.error}`,
+ NotificationSeverity.ERROR,
+ );
+ console.error("Speech recognition error:", event);
+ setIsListening(false);
+ };
+
+ recognition.onend = () => {
+ setIsListening(false);
+ };
+ } else {
+ recognition.stop();
+ setIsListening(false);
+ }
+ };
+
+ const renderMic = () => {
+ if (type === "summary" || type === "faq" || !isSpeechRecognitionSupported)
+ return <>>;
+
+ if (isListening) {
+ return ;
+ } else {
+ return (
+
+
+
+
+
+ );
+ }
+ };
+
+ return renderMic();
+};
+
+export default AudioInput;
diff --git a/ProductivitySuite/ui/react/src/components/PrimparyInput_PromptSelector/PromptSelector.module.scss b/ProductivitySuite/ui/react/src/components/PrimparyInput_PromptSelector/PromptSelector.module.scss
new file mode 100644
index 0000000000..e90edb4a55
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/PrimparyInput_PromptSelector/PromptSelector.module.scss
@@ -0,0 +1,87 @@
+.promptsWrapper {
+ position: absolute;
+
+ z-index: 99;
+ width: 100%;
+ padding: 0 40px;
+
+ .expand {
+ width: 25px;
+ height: 25px;
+ border-radius: 25px;
+ min-width: unset;
+ border-width: 1px;
+ border-style: solid;
+ transform: rotate(180deg);
+ transition: transform 0.5s;
+ margin-top: -20px;
+ position: relative;
+ z-index: 9999;
+
+ &.open {
+ transform: rotate(0deg);
+ }
+ }
+}
+
+.promptText {
+ color: var(--copy-color) !important;
+}
+
+.promptsListWrapper {
+ margin-top: -23px;
+ max-height: 0px;
+ transition: max-height 0.5s;
+ overflow: hidden;
+ // background: #fff;
+ width: 100%;
+ z-index: 999;
+
+ &.open {
+ max-height: 250px;
+ overflow-y: auto;
+ }
+
+ ul {
+ padding: 0;
+ }
+
+ li {
+ border-bottom: 1px solid;
+ padding: 0;
+ justify-content: space-between;
+
+ button {
+ padding: 1rem;
+ width: 100%;
+ justify-content: flex-start;
+ text-align: left;
+ border-radius: 0px;
+ box-shadow: none;
+ }
+
+ &:first-of-type button {
+ padding-top: 1.2rem;
+
+ span {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 3; // Limits to 2 lines
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+
+ .delete {
+ width: 40px;
+ height: 40px;
+ border-radius: 40px;
+ margin: 0 0.5rem;
+ justify-content: center;
+
+ path {
+ fill: #cc0000;
+ }
+ }
+ }
+}
diff --git a/ProductivitySuite/ui/react/src/components/PrimparyInput_PromptSelector/PromptSelector.tsx b/ProductivitySuite/ui/react/src/components/PrimparyInput_PromptSelector/PromptSelector.tsx
new file mode 100644
index 0000000000..00d58c5b37
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/PrimparyInput_PromptSelector/PromptSelector.tsx
@@ -0,0 +1,113 @@
+import {
+ Box,
+ Button,
+ IconButton,
+ List,
+ ListItem,
+ styled,
+ Tooltip,
+} from "@mui/material";
+import { deletePrompt, promptSelector } from "@redux/Prompt/PromptSlice";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import { useEffect, useRef, useState } from "react";
+import { Delete, ExpandMore } from "@mui/icons-material";
+import styles from "./PromptSelector.module.scss";
+
+const ExpandButton = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.promptExpandButton,
+}));
+
+const PromptButton = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.promptButton,
+}));
+
+const PromptListWrapper = styled(Box)(({ theme }) => ({
+ ...theme.customStyles.promptListWrapper,
+}));
+
+interface PromptSelectorProps {
+ setSearchText: (value: string) => void;
+}
+
+const PromptSelector: React.FC = ({ setSearchText }) => {
+ const dispatch = useAppDispatch();
+ const { prompts } = useAppSelector(promptSelector);
+ const [showPrompts, setShowPrompts] = useState(false);
+ const wrapperRef = useRef(null);
+
+ useEffect(() => {
+ if (!showPrompts) return;
+
+ const handleClickOutside = (event: MouseEvent) => {
+ if (
+ wrapperRef.current &&
+ !wrapperRef.current.contains(event.target as Node)
+ ) {
+ setShowPrompts(false);
+ }
+ };
+
+ document.addEventListener("mousedown", handleClickOutside);
+
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, [showPrompts]);
+
+ const handleDelete = (id: string, text: string) => {
+ dispatch(deletePrompt({ promptId: id, promptText: text }));
+ };
+
+ const handleSelect = (promptText: string) => {
+ setSearchText(promptText);
+ setShowPrompts(false);
+ };
+
+ return (
+ prompts &&
+ prompts.length > 0 && (
+
+
+ setShowPrompts(!showPrompts)}
+ >
+
+
+
+
+
+
+ {prompts?.map((prompt, promptIndex) => {
+ return (
+
+ handleSelect(prompt.prompt_text)}
+ >
+ {prompt.prompt_text}
+
+
+
+
+ handleDelete(prompt.id, prompt.prompt_text)
+ }
+ >
+
+
+
+
+ );
+ })}
+
+
+
+ )
+ );
+};
+
+export default PromptSelector;
diff --git a/ProductivitySuite/ui/react/src/components/ProgressIcon/ProgressIcon.tsx b/ProductivitySuite/ui/react/src/components/ProgressIcon/ProgressIcon.tsx
new file mode 100644
index 0000000000..aa8d3ec122
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/ProgressIcon/ProgressIcon.tsx
@@ -0,0 +1,13 @@
+import { CircularProgress, styled } from "@mui/material";
+
+const ProgressIconStyle = styled(CircularProgress)(({ theme }) => ({
+ "svg circle": {
+ stroke: theme.customStyles.audioProgress?.stroke,
+ },
+}));
+
+const ProgressIcon = () => {
+ return ;
+};
+
+export default ProgressIcon;
diff --git a/ProductivitySuite/ui/react/src/components/PromptSettings/PromptSettings.module.scss b/ProductivitySuite/ui/react/src/components/PromptSettings/PromptSettings.module.scss
new file mode 100644
index 0000000000..748c8427ad
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/PromptSettings/PromptSettings.module.scss
@@ -0,0 +1,76 @@
+.promptSettingsWrapper {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ max-width: 775px;
+
+ .summarySource {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ :global {
+ .MuiFormGroup-root {
+ margin-bottom: 0.75rem;
+
+ label {
+ margin-left: 0;
+ }
+
+ &:not(:last-of-type) {
+ margin-right: 1.5rem;
+ }
+ }
+ }
+}
+
+.promptSettings {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ margin-top: calc(var(--vertical-spacer) / 2);
+ flex-wrap: wrap;
+ width: 100%;
+
+ @media screen and (max-width: 900px) {
+ padding: 0 var(--content-gutter);
+ }
+
+ :global {
+ .MuiFormControlLabel-label,
+ .MuiTypography-root {
+ font-size: 13px;
+ font-weight: 400;
+ }
+ }
+
+ &.readOnly {
+ flex-direction: column;
+ align-items: flex-start;
+ padding: 0;
+ margin-top: 0;
+
+ :global {
+ .MuiFormGroup-root {
+ margin-bottom: 0;
+ margin-right: 0;
+
+ label {
+ width: 100%;
+ align-items: flex-start;
+ margin: 0;
+ }
+ }
+
+ @media screen and (max-width: 900px) {
+ .MuiFormGroup-root:not(:last-of-type) {
+ margin-bottom: 0;
+ }
+ }
+ }
+ }
+}
diff --git a/ProductivitySuite/ui/react/src/components/PromptSettings/PromptSettings.tsx b/ProductivitySuite/ui/react/src/components/PromptSettings/PromptSettings.tsx
new file mode 100644
index 0000000000..9a16ecd3b5
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/PromptSettings/PromptSettings.tsx
@@ -0,0 +1,215 @@
+import { useEffect, useState } from "react";
+
+import DropDown from "@components/DropDown/DropDown";
+import CustomSlider from "@components/PromptSettings_Slider/Slider";
+import { Box, FormGroup, FormControlLabel } from "@mui/material";
+import styles from "./PromptSettings.module.scss";
+import TokensInput from "@components/PromptSettings_Tokens/TokensInput";
+import FileInput from "@components/File_Input/FileInput";
+import WebInput from "@components/Summary_WebInput/WebInput";
+
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import { Model } from "@redux/Conversation/Conversation";
+import {
+ conversationSelector,
+ setModel,
+ setSourceType,
+ setTemperature,
+ setToken,
+} from "@redux/Conversation/ConversationSlice";
+
+interface AvailableModel {
+ name: string;
+ value: string;
+}
+
+interface PromptSettingsProps {
+ readOnly?: boolean;
+}
+
+const PromptSettings: React.FC = ({
+ readOnly = false,
+}) => {
+ const dispatch = useAppDispatch();
+
+ const { models, type, sourceType, model, token, maxToken, temperature } =
+ useAppSelector(conversationSelector);
+
+ const [tokenError, setTokenError] = useState(false);
+
+ const filterAvailableModels = (): AvailableModel[] => {
+ if (!models || !type) return [];
+
+ let typeModels: AvailableModel[] = [];
+
+ models.map((model: Model) => {
+ if (model.types.includes(type)) {
+ typeModels.push({
+ name: model.displayName,
+ value: model.model_name,
+ });
+ }
+ });
+
+ return typeModels;
+ };
+
+ const [formattedModels, setFormattedModels] = useState(
+ filterAvailableModels(),
+ );
+
+ useEffect(() => {
+ setFormattedModels(filterAvailableModels());
+ }, [type, models]);
+
+ useEffect(() => {
+ if (!model) return;
+ setTokenError(token > maxToken);
+ }, [model, token]);
+
+ const updateTemperature = (value: number) => {
+ dispatch(setTemperature(Number(value)));
+ };
+
+ const updateTokens = (value: number) => {
+ dispatch(setToken(Number(value)));
+ };
+
+ const updateModel = (value: string) => {
+ const selectedModel = models.find(
+ (model: Model) => model.model_name === value,
+ );
+ if (selectedModel) {
+ dispatch(setModel(selectedModel));
+ }
+ };
+
+ const updateSource = (value: string) => {
+ dispatch(setSourceType(value));
+ };
+
+ const cursorDisable = () => {
+ return readOnly ? { pointerEvents: "none" } : {};
+ };
+
+ const displaySummarySource = () => {
+ if ((type !== "summary" && type !== "faq") || readOnly) return;
+
+ let input = null;
+ if (sourceType === "documents") input = ;
+ if (sourceType === "web") input = ;
+ if (sourceType === "images" && type === "summary")
+ input = ;
+
+ return {input}
;
+ };
+
+ // in the off chance specific types do not use these,
+ // they have been pulled into their own function
+ const tokenAndTemp = () => {
+ return (
+ <>
+
+
+
+
+ }
+ label={`Tokens${readOnly ? ": " : ""}`}
+ labelPlacement="start"
+ />
+
+
+
+
+ }
+ label={`Temperature${readOnly ? ": " : ""}`}
+ labelPlacement="start"
+ />
+
+ >
+ );
+ };
+
+ const displaySettings = () => {
+ if (type === "summary" || type === "faq") {
+ //TODO: Supporting only documents to start
+ return (
+ <>
+
+
+ }
+ label="Summary Source"
+ labelPlacement="start"
+ />
+
+ >
+ );
+ } else {
+ return <>>; // tokenAndTemp() // see line 113, conditional option
+ }
+ };
+
+ return (
+
+
+ {formattedModels && formattedModels.length > 0 && (
+
+
+ }
+ label={`Model${readOnly ? ": " : ""}`}
+ labelPlacement="start"
+ />
+
+ )}
+
+ {tokenAndTemp()}
+
+
+ {/* TODO: Expand source options and show label with dropdown after expansion */}
+ {/* {displaySettings()} */}
+
+ {displaySummarySource()}
+
+ );
+};
+
+export default PromptSettings;
diff --git a/ProductivitySuite/ui/react/src/components/PromptSettings_Slider/Slider.module.scss b/ProductivitySuite/ui/react/src/components/PromptSettings_Slider/Slider.module.scss
new file mode 100644
index 0000000000..bdb124598f
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/PromptSettings_Slider/Slider.module.scss
@@ -0,0 +1,88 @@
+.sliderWrapper {
+ flex-direction: row;
+ align-items: center;
+ width: 100%;
+ max-width: 320px;
+
+ flex-wrap: nowrap !important;
+
+ font-size: 13px;
+ font-weight: 400;
+
+ .start {
+ margin-left: 0.5rem;
+ }
+
+ .trackWrapper {
+ margin: 0 0.5rem;
+ width: 100px;
+ display: flex;
+ }
+
+ .styledSlider {
+ height: 2px;
+ width: 100%;
+ padding: 16px 0;
+ display: inline-flex;
+ align-items: center;
+ position: relative;
+ cursor: pointer;
+ touch-action: none;
+ -webkit-tap-highlight-color: transparent;
+ flex-wrap: nowrap;
+
+ &.disabled {
+ pointer-events: none;
+ cursor: default;
+ opacity: 0.4;
+ }
+
+ :global {
+ .MuiSlider-rail {
+ display: block;
+ position: absolute;
+ width: 100%;
+ height: 2px;
+ border-radius: 2px;
+ opacity: 0.3;
+ }
+
+ .MuiSlider-track {
+ display: block;
+ position: absolute;
+ height: 0px;
+ }
+
+ .MuiSlider-thumb {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ margin-left: -2px;
+ width: 10px;
+ height: 10px;
+ box-sizing: border-box;
+ border-radius: 50%;
+ outline: 0;
+ transition-property: box-shadow, transform;
+ transition-timing-function: ease;
+ transition-duration: 120ms;
+ transform-origin: center;
+
+ &:hover {
+ }
+
+ &.focusVisible {
+ outline: none;
+ }
+
+ &.active {
+ outline: none;
+ }
+
+ &.disabled {
+ }
+ }
+ }
+ }
+}
diff --git a/ProductivitySuite/ui/react/src/components/PromptSettings_Slider/Slider.tsx b/ProductivitySuite/ui/react/src/components/PromptSettings_Slider/Slider.tsx
new file mode 100644
index 0000000000..93bbd2442a
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/PromptSettings_Slider/Slider.tsx
@@ -0,0 +1,49 @@
+import * as React from "react";
+import { styled } from "@mui/material/styles";
+import { Slider, Grid2, Typography } from "@mui/material";
+import styles from "./Slider.module.scss";
+
+const StyledSlider = styled(Slider)(({ theme }) => ({
+ ...theme.customStyles.styledSlider,
+}));
+
+interface CustomSliderProps {
+ value: number;
+ handleChange: (value: number) => void;
+ readOnly?: boolean;
+}
+
+const CustomSlider: React.FC = ({
+ value,
+ handleChange,
+ readOnly,
+}) => {
+ if (readOnly) {
+ return {value} ;
+ }
+
+ const handleSlideUpdate = (event: Event, value: number) => {
+ handleChange(value);
+ };
+
+ return (
+
+ 0
+
+
+
+ 1
+
+ );
+};
+
+export default CustomSlider;
diff --git a/ProductivitySuite/ui/react/src/components/PromptSettings_Tokens/TokensInput.module.scss b/ProductivitySuite/ui/react/src/components/PromptSettings_Tokens/TokensInput.module.scss
new file mode 100644
index 0000000000..4d0f13d418
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/PromptSettings_Tokens/TokensInput.module.scss
@@ -0,0 +1,49 @@
+.numberInput {
+ font-weight: 400;
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: center;
+ align-items: center;
+
+ input {
+ font-size: 13px;
+ font-family: inherit;
+ font-weight: 400;
+ line-height: 1.375;
+ border-radius: 8px;
+ margin: 0 8px 0 0;
+ padding: 3px 5px;
+ outline: 0;
+ min-width: 0;
+ width: 3.5rem;
+ text-align: center;
+ background: transparent;
+
+ &:hover {
+ }
+
+ &:focus {
+ }
+
+ &:focus-visible {
+ outline: 0;
+ }
+
+ /* Chrome, Safari, Edge, Opera */
+ &::-webkit-outer-spin-button,
+ &::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+
+ /* Firefox */
+ &[type="number"] {
+ -moz-appearance: textfield;
+ }
+ }
+
+ .error,
+ .error:focus {
+ border: 1px solid #cc0000;
+ }
+}
diff --git a/ProductivitySuite/ui/react/src/components/PromptSettings_Tokens/TokensInput.tsx b/ProductivitySuite/ui/react/src/components/PromptSettings_Tokens/TokensInput.tsx
new file mode 100644
index 0000000000..6b4bd216cd
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/PromptSettings_Tokens/TokensInput.tsx
@@ -0,0 +1,46 @@
+import * as React from "react";
+import { styled } from "@mui/material/styles";
+
+import { Typography } from "@mui/material";
+import styles from "./TokensInput.module.scss";
+
+interface NumberInputProps {
+ value?: number;
+ handleChange: (value: number) => void;
+ error: boolean;
+ readOnly?: boolean;
+}
+
+const StyledInput = styled("input")(({ theme }) => ({
+ ...theme.customStyles.tokensInput,
+}));
+
+const TokensInput: React.FC = ({
+ value = 1,
+ handleChange,
+ error,
+ readOnly,
+}) => {
+ if (readOnly) {
+ return {value} ;
+ }
+
+ return (
+
+ ) =>
+ handleChange(parseInt(e.target.value, 10))
+ }
+ aria-label="Quantity Input"
+ />
+
+ );
+};
+
+export default TokensInput;
diff --git a/ProductivitySuite/ui/react/src/components/SearchInput/SearchInput.module.scss b/ProductivitySuite/ui/react/src/components/SearchInput/SearchInput.module.scss
new file mode 100644
index 0000000000..33207e8883
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/SearchInput/SearchInput.module.scss
@@ -0,0 +1,17 @@
+.searchInput {
+ width: 100%;
+ margin-bottom: 1rem;
+ border-radius: var(--input-radius);
+ border: 0;
+ margin-bottom: 2rem;
+
+ &:focus {
+ outline: none;
+ }
+
+ :global {
+ .MuiInputBase-root {
+ border-radius: var(--input-radius);
+ }
+ }
+}
diff --git a/ProductivitySuite/ui/react/src/components/SearchInput/SearchInput.tsx b/ProductivitySuite/ui/react/src/components/SearchInput/SearchInput.tsx
new file mode 100644
index 0000000000..49a11f6c40
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/SearchInput/SearchInput.tsx
@@ -0,0 +1,63 @@
+import { InputAdornment, styled, TextField } from "@mui/material";
+import styles from "./SearchInput.module.scss";
+import { Close, Search } from "@mui/icons-material";
+import { useRef, useState } from "react";
+
+const StyledSearchInput = styled(TextField)(({ theme }) => ({
+ ...theme.customStyles.webInput,
+}));
+
+interface SearchInputProps {
+ handleSearch: (value: string) => void;
+}
+
+const SearchInput: React.FC = ({ handleSearch }) => {
+ const [hasValue, setHasValue] = useState(false);
+
+ const inputRef = useRef(null);
+
+ const clearSearch = () => {
+ if (inputRef.current) {
+ const input = inputRef.current.querySelector("input");
+ if (input) input.value = "";
+ }
+ handleSearch("");
+ setHasValue(false);
+ };
+
+ const search = (value: string) => {
+ handleSearch(value);
+ setHasValue(value !== "");
+ };
+
+ return (
+ ) =>
+ search(e.target.value)
+ }
+ InputProps={{
+ startAdornment: (
+
+
+
+ ),
+ endAdornment: hasValue && (
+
+
+
+ ),
+ }}
+ fullWidth
+ />
+ );
+};
+
+export default SearchInput;
diff --git a/ProductivitySuite/ui/react/src/components/Shared/CodeRender/CodeRender.tsx b/ProductivitySuite/ui/react/src/components/Shared/CodeRender/CodeRender.tsx
deleted file mode 100644
index a21f7acc59..0000000000
--- a/ProductivitySuite/ui/react/src/components/Shared/CodeRender/CodeRender.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import styles from './codeRender.module.scss'
-import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
-import { tomorrow } from "react-syntax-highlighter/dist/esm/styles/prism";
-import { IconCopy } from '@tabler/icons-react';
-import { Button, CopyButton } from '@mantine/core';
-
-type CodeRenderProps = {
- cleanCode: React.ReactNode,
- language: string,
- inline: boolean
-}
-const CodeRender = ({ cleanCode, language, inline }:CodeRenderProps) => {
- cleanCode = String(cleanCode).replace(/\n$/, '').replace(/^\s*[\r\n]/gm, '') //right trim and remove empty lines from the input
- console.log(styles)
- try {
- return inline ? ({cleanCode}
) : (
-
-
-
- {language || "language not detected"}
-
-
-
- {({ copied, copy }) => (
- } onClick={copy}>
- {copied ? 'Copied' : 'Copy'}
-
- )}
-
-
-
-
-
)
- } catch (err) {
- return (
-
- {cleanCode}
-
- )
- }
-
-}
-
-
-export default CodeRender;
diff --git a/ProductivitySuite/ui/react/src/components/Shared/CodeRender/codeRender.module.scss b/ProductivitySuite/ui/react/src/components/Shared/CodeRender/codeRender.module.scss
deleted file mode 100644
index a62f00d408..0000000000
--- a/ProductivitySuite/ui/react/src/components/Shared/CodeRender/codeRender.module.scss
+++ /dev/null
@@ -1,23 +0,0 @@
-@import "../../../styles/styles";
-
-.code {
- margin: 7px 0px;
- .codeHead {
- background: #379af1;
-
- padding: 0px 10px !important;
- @include flex(row, nowrap, center, space-between);
- .codeTitle {
- }
- .codeActionGroup {
- @include flex(row, nowrap, center, flex-start);
- }
- }
- .codeHighlighterDiv {
- margin: 0px !important;
- }
-}
-
-.inlineCode {
- background: #d7d7d7;
-}
diff --git a/ProductivitySuite/ui/react/src/components/Shared/Markdown/Markdown.tsx b/ProductivitySuite/ui/react/src/components/Shared/Markdown/Markdown.tsx
deleted file mode 100644
index 2726e14b2e..0000000000
--- a/ProductivitySuite/ui/react/src/components/Shared/Markdown/Markdown.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import markdownStyles from './markdown.module.scss'
-import ReactMarkdown from 'react-markdown';
-import remarkGfm from 'remark-gfm';
-import remarkFrontmatter from 'remark-frontmatter';
-// import Mermaid from '../../shared/Mermaid/Mermaid';
-import CodeRender from '../CodeRender/CodeRender';
-
-type MarkdownProps = {
- content: string
-}
-const Markdown = ({ content }: MarkdownProps) => {
- return (
- {
- return (
-
- {children}
-
- );
- },
- a: ({ children, ...props }) => {
- return (
-
- {children}
-
- );
- },
- table: ({ children, ...props }) => {
- return (
-
- );
- },
- //@ts-expect-error inline can undefined sometimes
- code({ inline, className, children, }) {
- const lang = /language-(\w+)/.exec(className || '')
- // if (lang && lang[1] === "mermaid") {
- // return
- // }
- return
- }
- }}
- />)
-}
-
-export default Markdown;
diff --git a/ProductivitySuite/ui/react/src/components/Shared/Markdown/markdown.module.scss b/ProductivitySuite/ui/react/src/components/Shared/Markdown/markdown.module.scss
deleted file mode 100644
index e796f836f7..0000000000
--- a/ProductivitySuite/ui/react/src/components/Shared/Markdown/markdown.module.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-.tableDiv {
- table,
- th,
- td {
- border: 1px solid black;
- border-collapse: collapse;
- }
-}
-
-.md {
- li {
- margin-left: 35px; /* Adjust the value based on your preference */
- }
-}
diff --git a/ProductivitySuite/ui/react/src/components/SideBar/SideBar.module.scss b/ProductivitySuite/ui/react/src/components/SideBar/SideBar.module.scss
new file mode 100644
index 0000000000..67b98b7b0c
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/SideBar/SideBar.module.scss
@@ -0,0 +1,117 @@
+.aside {
+ max-width: var(--sidebar-width);
+ position: fixed;
+ width: 0;
+ overflow: hidden;
+ transition: width 0.3s;
+ height: 100vh;
+ top: 0;
+ left: 0;
+ z-index: 998;
+ padding-top: var(--header-height);
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+ &.open {
+ max-width: var(--sidebar-width);
+ width: var(--sidebar-width);
+ }
+
+ .asideContent {
+ width: var(--sidebar-width);
+ min-width: var(--sidebar-width);
+ display: flex;
+ flex-direction: column;
+ list-style: none;
+ overflow: auto;
+ max-height: 100%;
+ margin-bottom: auto;
+
+ .emptySvg {
+ width: 24px;
+ height: 24px;
+ min-width: 24px;
+ }
+
+ li,
+ a {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ }
+
+ li {
+ padding-left: var(--header-gutter);
+ padding-right: var(--header-gutter);
+ }
+
+ a {
+ width: 100%;
+ max-width: 100%;
+ text-decoration: none;
+ }
+
+ :global {
+ .MuiListItemText-root {
+ margin-left: 10px;
+ }
+
+ .MuiTypography-root {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+
+ .viewAll span {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-start;
+
+ svg {
+ margin-left: 0.5rem;
+ transform: rotate(180deg);
+ }
+ }
+
+ .divider {
+ height: 0;
+ margin: 10px var(--header-gutter);
+ }
+ }
+}
+
+.asideSpacer {
+ width: 0;
+ transition: width 0.3s;
+
+ &.asideSpacerOpen {
+ width: var(--sidebar-width);
+ }
+}
+
+@media screen and (max-width: 1200px) {
+ .asideSpacer {
+ display: none;
+ }
+}
+
+.mobileUser {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ width: var(--sidebar-width);
+ min-width: var(--sidebar-width);
+ padding: var(--header-gutter);
+
+ :global {
+ .themeToggle {
+ margin-left: -15px;
+ margin-bottom: 1rem;
+ }
+ }
+ @media screen and (min-width: 900px) {
+ display: none;
+ }
+}
diff --git a/ProductivitySuite/ui/react/src/components/SideBar/SideBar.tsx b/ProductivitySuite/ui/react/src/components/SideBar/SideBar.tsx
new file mode 100644
index 0000000000..c812284388
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/SideBar/SideBar.tsx
@@ -0,0 +1,226 @@
+import { Link } from "react-router-dom";
+import { useTheme } from "@mui/material/styles";
+import { SvgIconProps } from "@mui/material/SvgIcon";
+import styles from "./SideBar.module.scss";
+import LogoutIcon from "@mui/icons-material/Logout";
+import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
+import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings";
+import DatabaseIcon from "@icons/Database";
+import RecentIcon from "@icons/Recent";
+import PeopleOutlineOutlinedIcon from "@mui/icons-material/PeopleOutlineOutlined";
+import FileUploadOutlinedIcon from "@mui/icons-material/FileUploadOutlined";
+import { Box, ListItemText, MenuItem, MenuList } from "@mui/material";
+
+import { JSX, MouseEventHandler } from "react";
+import ThemeToggle from "@components/Header_ThemeToggle/ThemeToggle";
+
+import { useAppSelector } from "@redux/store";
+import { userSelector } from "@redux/User/userSlice";
+import {
+ conversationSelector,
+ newConversation,
+} from "@redux/Conversation/ConversationSlice";
+import { Conversation } from "@redux/Conversation/Conversation";
+import { useKeycloak } from "@react-keycloak/web";
+import UploadChat from "@components/SideBar_UploadChat/UploadChat";
+import { KeyboardBackspace } from "@mui/icons-material";
+import { useDispatch } from "react-redux";
+
+interface SideBarProps {
+ asideOpen: boolean;
+ setAsideOpen?: (open: boolean) => void;
+ userDetails?: () => JSX.Element;
+}
+
+interface NavIconProps {
+ component: React.ComponentType;
+}
+
+export const NavIcon: React.FC = ({
+ component: ListItemIcon,
+}) => {
+ const theme = useTheme();
+ return ;
+};
+
+const EmptySvg: React.FC = () => {
+ return (
+
+ );
+};
+
+interface LinkedMenuItemProps {
+ to: string;
+ children: React.ReactNode;
+ onClick?: MouseEventHandler;
+ sx?: any;
+ open?: boolean;
+}
+
+export const LinkedMenuItem: React.FC = ({
+ to,
+ children,
+ onClick,
+ sx,
+ open,
+}) => {
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+const SideBar: React.FC = ({
+ asideOpen,
+ setAsideOpen = () => {},
+ userDetails,
+}) => {
+ const dispatch = useDispatch();
+ const theme = useTheme();
+
+ const { keycloak } = useKeycloak();
+ const { role } = useAppSelector(userSelector);
+ const { conversations } = useAppSelector(conversationSelector);
+
+ const asideBackgroundColor = {
+ backgroundColor: theme.customStyles.aside?.main,
+ };
+
+ const dividerColor = {
+ borderBottom: `1px solid ${theme.customStyles.customDivider?.main}`,
+ };
+
+ const handleLinkedMenuItemClick = (
+ event: React.MouseEvent,
+ ) => {
+ event.currentTarget.blur(); // so we can apply the aria hidden attribute while menu closed
+ dispatch(newConversation(true));
+ setAsideOpen(false);
+ };
+
+ const history = (type: Conversation[]) => {
+ if (type && type.length > 0) {
+ return type.map((conversation: Conversation, index: number) => {
+ if (index > 2) return null;
+ return (
+
+
+ {conversation.first_query}
+
+ );
+ });
+ }
+ };
+
+ const handleLogout = () => {
+ keycloak.logout();
+ setAsideOpen(false);
+ };
+
+ const viewAll = (path: string) => {
+ if (conversations.length > 0) {
+ return (
+
+
+
+ View All
+
+
+ );
+ } else {
+ return (
+
+
+ No recent conversations
+
+ );
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ Recents
+
+
+ {history(conversations)}
+
+ {viewAll("/history")}
+
+ {/*
+
+ Shared
+ */}
+
+ {/* {history(allSharedConversations)} */}
+
+ {/* {viewAll('/shared')} */}
+
+
+
+ {role === "Admin" && (
+ <>
+
+
+ Data Management
+
+ >
+ )}
+
+
+
+ Log Out
+
+
+
+
+
+ {userDetails && userDetails()}
+
+
+ );
+};
+
+const SideBarSpacer: React.FC = ({ asideOpen }) => {
+ return (
+
+ );
+};
+
+export { SideBar, SideBarSpacer };
diff --git a/ProductivitySuite/ui/react/src/components/SideBar_UploadChat/UploadChat.tsx b/ProductivitySuite/ui/react/src/components/SideBar_UploadChat/UploadChat.tsx
new file mode 100644
index 0000000000..17ab636436
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/SideBar_UploadChat/UploadChat.tsx
@@ -0,0 +1,141 @@
+import {
+ NotificationSeverity,
+ notify,
+} from "@components/Notification/Notification";
+import { LinkedMenuItem, NavIcon } from "@components/SideBar/SideBar";
+import { FileUploadOutlined } from "@mui/icons-material";
+import { ListItemText } from "@mui/material";
+import {
+ conversationSelector,
+ getAllConversations,
+ saveConversationtoDatabase,
+ uploadChat,
+} from "@redux/Conversation/ConversationSlice";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import { userSelector } from "@redux/User/userSlice";
+import { useEffect, useRef } from "react";
+import { useNavigate } from "react-router-dom";
+
+interface UploadChatProps {
+ asideOpen: boolean;
+ setAsideOpen: (open: boolean) => void;
+}
+
+const UploadChat: React.FC = ({ asideOpen, setAsideOpen }) => {
+ const dispatch = useAppDispatch();
+ const { selectedConversationHistory } = useAppSelector(conversationSelector);
+
+ const navigate = useNavigate();
+
+ const fileInputRef = useRef(null);
+ const newUpload = useRef(false);
+
+ useEffect(() => {
+ if (newUpload.current && selectedConversationHistory) {
+ saveConversation();
+ }
+ }, [selectedConversationHistory]);
+
+ const handleUploadClick = (event: React.MouseEvent) => {
+ event.preventDefault();
+ event.currentTarget.blur(); // so we can apply the aria hidden attribute while menu closed
+ if (fileInputRef.current) {
+ fileInputRef.current.click();
+ }
+ };
+
+ const saveConversation = async () => {
+ try {
+ const resultAction = await dispatch(
+ saveConversationtoDatabase({ conversation: { id: "" } }),
+ );
+
+ if (saveConversationtoDatabase.fulfilled.match(resultAction)) {
+ const responseData = resultAction.payload;
+ setAsideOpen(false);
+ newUpload.current = false;
+ notify(
+ "Conversation successfully uploaded",
+ NotificationSeverity.SUCCESS,
+ );
+ navigate(`/chat/${responseData}`);
+ } else {
+ newUpload.current = false;
+ notify("Error saving conversation", NotificationSeverity.ERROR);
+ console.error("Error saving conversation:", resultAction.error);
+ }
+ } catch (error) {
+ newUpload.current = false;
+ notify("Error saving conversation", NotificationSeverity.ERROR);
+ console.error("An unexpected error occurred:", error);
+ }
+ };
+
+ const handleFileChange = (event: React.ChangeEvent) => {
+ const file = event.target.files?.[0];
+
+ if (file) {
+ newUpload.current = true;
+ const reader = new FileReader();
+
+ reader.onload = () => {
+ try {
+ const fileContent = JSON.parse(reader.result as string);
+
+ if (
+ !fileContent.messages ||
+ !fileContent.model ||
+ !fileContent.token ||
+ !fileContent.temperature ||
+ fileContent.type
+ ) {
+ throw "Incorrect Format";
+ }
+
+ dispatch(
+ uploadChat({
+ messages: fileContent.messages,
+ model: fileContent.model,
+ token: fileContent.token,
+ temperature: fileContent.temperature,
+ type: fileContent.type,
+ }),
+ );
+ } catch (error) {
+ notify(
+ `Error parsing JSON file: ${error}`,
+ NotificationSeverity.ERROR,
+ );
+ console.error("Error parsing JSON file:", error);
+ }
+ };
+
+ reader.readAsText(file);
+ }
+ };
+
+ return (
+ <>
+
+
+ Upload Chat
+
+
+ {/* Hidden file input */}
+
+ >
+ );
+};
+
+export default UploadChat;
diff --git a/ProductivitySuite/ui/react/src/components/Summary_WebInput/WebInput.module.scss b/ProductivitySuite/ui/react/src/components/Summary_WebInput/WebInput.module.scss
new file mode 100644
index 0000000000..069a27a330
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Summary_WebInput/WebInput.module.scss
@@ -0,0 +1,19 @@
+.inputWrapper {
+ width: 100%;
+ max-width: 700px;
+ margin-top: 1rem;
+}
+
+.dataList {
+ width: 100%;
+ margin-top: 2rem;
+ max-height: 300;
+ overflow: auto;
+ border: 1px solid;
+ border-radius: 8px;
+ padding: 1rem;
+
+ li:not(:last-of-type) {
+ margin-bottom: 1rem;
+ }
+}
diff --git a/ProductivitySuite/ui/react/src/components/Summary_WebInput/WebInput.tsx b/ProductivitySuite/ui/react/src/components/Summary_WebInput/WebInput.tsx
new file mode 100644
index 0000000000..693e824423
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/components/Summary_WebInput/WebInput.tsx
@@ -0,0 +1,120 @@
+import { AddCircle, Delete } from "@mui/icons-material";
+import {
+ IconButton,
+ InputAdornment,
+ List,
+ ListItem,
+ ListItemText,
+ styled,
+ TextField,
+ useTheme,
+} from "@mui/material";
+import { useState } from "react";
+import styles from "./WebInput.module.scss";
+import { Language } from "@mui/icons-material";
+
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import {
+ conversationSelector,
+ setSourceLinks,
+} from "@redux/Conversation/ConversationSlice";
+
+export const CustomTextInput = styled(TextField)(({ theme }) => ({
+ ...theme.customStyles.webInput,
+}));
+
+export const AddIcon = styled(AddCircle)(({ theme }) => ({
+ path: {
+ fill: theme.customStyles.icon?.main,
+ },
+}));
+
+const WebInput = () => {
+ const [inputValue, setInputValue] = useState("");
+
+ const theme = useTheme();
+
+ const { sourceLinks } = useAppSelector(conversationSelector);
+ const dispatch = useAppDispatch();
+
+ const handleAdd = (newSource: string) => {
+ if (!newSource) return;
+ const prevSource = sourceLinks ?? [];
+ dispatch(setSourceLinks([...prevSource, newSource]));
+ setInputValue("");
+ };
+
+ const handleDelete = (index: number) => {
+ const newSource = sourceLinks.filter((s: any, i: number) => i !== index);
+ dispatch(setSourceLinks([...newSource]));
+ };
+
+ const handleKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && inputValue) {
+ handleAdd(inputValue);
+ }
+ };
+
+ const handleIconClick = () => {
+ if (inputValue) {
+ handleAdd(inputValue);
+ }
+ };
+
+ const sourcesDisplay = () => {
+ if (!sourceLinks || sourceLinks.length === 0) return;
+
+ return (
+
+ {sourceLinks.map((sourceItem: string, index: number) => (
+ handleDelete(index)}>
+
+
+ }
+ >
+ 30
+ ? `${sourceItem.substring(0, 27)}...`
+ : sourceItem
+ }
+ />
+
+
+ ))}
+
+ );
+ };
+
+ return (
+
+
) =>
+ setInputValue(e.target.value)
+ }
+ InputProps={{
+ endAdornment: (
+
+
+
+ ),
+ }}
+ fullWidth
+ />
+
+ {sourcesDisplay()}
+
+ );
+};
+
+export default WebInput;
diff --git a/ProductivitySuite/ui/react/src/components/sidebar/sidebar.module.scss b/ProductivitySuite/ui/react/src/components/sidebar/sidebar.module.scss
deleted file mode 100644
index cf8061e1e0..0000000000
--- a/ProductivitySuite/ui/react/src/components/sidebar/sidebar.module.scss
+++ /dev/null
@@ -1,87 +0,0 @@
-/**
- Copyright (c) 2024 Intel Corporation
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
- **/
-
-@import "../../styles/styles";
-
-.navbar {
- width: 100%;
- @include flex(column, nowrap, center, flex-start);
- padding: var(--mantine-spacing-md);
- background-color: var(--mantine-color-blue-filled);
- // background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6));
- // border-right: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
-}
-
-.navbarMain {
- // flex: 1;
- height: 655px;
-}
-
-.navbarLogo {
- width: 100%;
- display: flex;
- justify-content: center;
- padding-top: var(--mantine-spacing-md);
- margin-bottom: var(--mantine-spacing-xl);
-}
-
-.link {
- width: 44px;
- height: 44px;
- border-radius: var(--mantine-radius-md);
- text-align: center;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- color: var(--mantine-color-white);
-
- &:hover {
- background-color: var(--mantine-color-blue-7);
- }
-
- &[data-active] {
- &,
- &:hover {
- box-shadow: var(--mantine-shadow-sm);
- background-color: var(--mantine-color-white);
- color: var(--mantine-color-blue-6);
- }
- }
-}
-
-.aside {
- flex: 0 0 60px;
- background-color: var(--mantine-color-body);
- display: flex;
- flex-direction: column;
- align-items: center;
- border-right: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7));
-}
-
-.logo {
- width: 100%;
- display: flex;
- justify-content: center;
- height: 60px;
- padding-top: var(--mantine-spacing-s);
- border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7));
- margin-bottom: var(--mantine-spacing-xl);
-}
-.logoImg {
- width: 30px;
-}
diff --git a/ProductivitySuite/ui/react/src/components/sidebar/sidebar.tsx b/ProductivitySuite/ui/react/src/components/sidebar/sidebar.tsx
deleted file mode 100644
index e35b4f0117..0000000000
--- a/ProductivitySuite/ui/react/src/components/sidebar/sidebar.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-import { useState } from "react"
-import { Tooltip, UnstyledButton, Stack, rem } from "@mantine/core"
-import { IconHome2, IconLogout } from "@tabler/icons-react"
-import classes from "./sidebar.module.scss"
-import OpeaLogo from "../../assets/opea-icon-color.svg"
-import { useAppDispatch } from "../../redux/store"
-import { removeUser } from "../../redux/User/userSlice"
-import { logout } from "../../redux/Conversation/ConversationSlice"
-import { useNavigate } from "react-router-dom"
-import { clearPrompts } from "../../redux/Prompt/PromptSlice"
-import { useKeycloak } from "@react-keycloak/web"
-
-interface NavbarLinkProps {
- icon: typeof IconHome2
- label: string
- path?: string
- active?: boolean
- onClick?(): void
-}
-
-function NavbarLink({ icon: Icon, label, active,path , onClick }: NavbarLinkProps) {
- const navigate = useNavigate();
-
- return (
-
- {onClick ? onClick() : navigate(path || "")}} className={classes.link} data-active={active || undefined}>
-
- {/* {label} */}
-
-
- )
-}
-
-export interface SidebarNavItem {
- icon: typeof IconHome2
- label: string,
- path:string,
- children: React.ReactNode
-}
-
-export type SidebarNavList = SidebarNavItem[]
-
-export interface SideNavbarProps {
- navList: SidebarNavList
-}
-
-export function SideNavbar({ navList }: SideNavbarProps) {
- const dispatch =useAppDispatch()
- const [active, setActive] = useState(0)
- const navigate = useNavigate();
- const {keycloak} = useKeycloak()
-
-
- const handleLogout = () => {
- dispatch(logout())
- dispatch(removeUser())
- dispatch(clearPrompts())
- keycloak.logout({})
- }
-
- const links = navList.map((link, index) => (
- {
- setActive(index)
- navigate(link.path)
- }} />
- ))
-
- return (
-
-
-
-
-
-
-
- {links}
-
-
-
-
-
-
- )
-}
diff --git a/ProductivitySuite/ui/react/src/config.ts b/ProductivitySuite/ui/react/src/config.ts
index a17ae2bf16..d9ff2db0e5 100644
--- a/ProductivitySuite/ui/react/src/config.ts
+++ b/ProductivitySuite/ui/react/src/config.ts
@@ -1,12 +1,25 @@
-// Copyright (C) 2024 Intel Corporation
+// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
-console.log(import.meta.env.VITE_KEYCLOAK_SERVICE_ENDPOINT);
+const config = {
+ companyName: "My Company",
+ logo: "/logo512.png",
+ tagline: "What can I help with?",
+ disclaimer:
+ "Generative AI provides significant benefits for enhancing the productivity of quality engineers, production support teams, software developers, and DevOps professionals. With a secure and scalable toolbox, it offers a flexible architecture capable of connecting to various data sources and models, enabling it to address a wide range of Generative AI use cases.
This platform saves your user ID to retain chat history, which you can choose to delete from the app at any time.
",
+ // defaultSummaryPrompt: `You are a professional summarizer good at summarizing documents. Provide a detailed summary of all documents while adhering to these guidelines:
+ // 1. Do not repeat similar information in the summary and use a formal tone
+ // 2. Format the summary in bullet form for easy understanding.`,
+ defaultChatPrompt: `You are a helpful assistant`,
+};
+
+export default config;
+
export const CHAT_QNA_URL = import.meta.env.VITE_BACKEND_SERVICE_ENDPOINT_CHATQNA;
export const CODE_GEN_URL = import.meta.env.VITE_BACKEND_SERVICE_ENDPOINT_CODEGEN;
export const DOC_SUM_URL = import.meta.env.VITE_BACKEND_SERVICE_ENDPOINT_DOCSUM;
export const FAQ_GEN_URL = import.meta.env.VITE_BACKEND_SERVICE_ENDPOINT_FAQGEN;
-export const KEYCLOACK_SERVICE_URL = import.meta.env.VITE_KEYCLOAK_SERVICE_ENDPOINT;
+export const KEYCLOAK_SERVICE_URL = import.meta.env.VITE_KEYCLOAK_SERVICE_ENDPOINT;
export const DATA_PREP_URL = import.meta.env.VITE_DATAPREP_SERVICE_ENDPOINT;
export const DATA_PREP_GET_URL = import.meta.env.VITE_DATAPREP_GET_FILE_ENDPOINT;
@@ -15,5 +28,7 @@ export const DATA_PREP_DELETE_URL = import.meta.env.VITE_DATAPREP_DELETE_FILE_EN
export const CHAT_HISTORY_CREATE = import.meta.env.VITE_CHAT_HISTORY_CREATE_ENDPOINT;
export const CHAT_HISTORY_GET = import.meta.env.VITE_CHAT_HISTORY_GET_ENDPOINT;
export const CHAT_HISTORY_DELETE = import.meta.env.VITE_CHAT_HISTORY_DELETE_ENDPOINT;
+
export const PROMPT_MANAGER_GET = import.meta.env.VITE_PROMPT_SERVICE_GET_ENDPOINT;
export const PROMPT_MANAGER_CREATE = import.meta.env.VITE_PROMPT_SERVICE_CREATE_ENDPOINT;
+export const PROMPT_MANAGER_DELETE = import.meta.env.VITE_PROMPT_SERVICE_DELETE_ENDPOINT;
diff --git a/ProductivitySuite/ui/react/src/contexts/ThemeContext.tsx b/ProductivitySuite/ui/react/src/contexts/ThemeContext.tsx
new file mode 100644
index 0000000000..94ec15bc29
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/contexts/ThemeContext.tsx
@@ -0,0 +1,39 @@
+import React, { createContext, useState, useEffect, useCallback } from "react";
+import { ThemeProvider as MuiThemeProvider, CssBaseline } from "@mui/material";
+import { themeCreator } from "../theme/theme";
+
+interface ThemeContextType {
+ darkMode: boolean;
+ toggleTheme: () => void;
+}
+
+export const ThemeContext = createContext({
+ darkMode: false,
+ toggleTheme: () => {},
+});
+
+export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ const savedTheme = localStorage.getItem("theme") === "dark";
+ const [darkMode, setDarkMode] = useState(savedTheme);
+
+ const toggleTheme = useCallback(() => {
+ setDarkMode((prevMode) => !prevMode);
+ }, []);
+
+ useEffect(() => {
+ localStorage.setItem("theme", darkMode ? "dark" : "light");
+ }, [darkMode]);
+
+ const theme = themeCreator(darkMode ? "dark" : "light");
+
+ return (
+
+
+
+ {children}
+
+
+ );
+};
diff --git a/ProductivitySuite/ui/react/src/icons/Atom.tsx b/ProductivitySuite/ui/react/src/icons/Atom.tsx
new file mode 100644
index 0000000000..039b640317
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/icons/Atom.tsx
@@ -0,0 +1,134 @@
+import { useTheme } from "@mui/material";
+
+interface AtomIconProps {
+ className?: string;
+}
+
+const AtomIcon: React.FC = ({ className }) => {
+ const theme = useTheme();
+
+ const iconColor = theme.customStyles.icon?.main;
+
+ return (
+
+
+
+
+ );
+};
+
+const AtomAnimation: React.FC = ({ className }) => {
+ const theme = useTheme();
+
+ const iconColor = theme.customStyles.icon?.main;
+
+ return (
+
+
+ {/* Grouping each ellipse with a circle */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export { AtomAnimation, AtomIcon };
diff --git a/ProductivitySuite/ui/react/src/icons/ChatBubble.tsx b/ProductivitySuite/ui/react/src/icons/ChatBubble.tsx
new file mode 100644
index 0000000000..9ba2c4ac80
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/icons/ChatBubble.tsx
@@ -0,0 +1,38 @@
+import { useTheme } from "@mui/material";
+
+interface ChatBubbleIconProps {
+ className?: string;
+}
+
+const ChatBubbleIcon: React.FC = ({ className }) => {
+ const theme = useTheme();
+
+ const iconColor = theme.customStyles.icon?.main;
+
+ return (
+
+
+
+
+ );
+};
+
+export default ChatBubbleIcon;
diff --git a/ProductivitySuite/ui/react/src/icons/Database.tsx b/ProductivitySuite/ui/react/src/icons/Database.tsx
new file mode 100644
index 0000000000..a74130d499
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/icons/Database.tsx
@@ -0,0 +1,29 @@
+import { SvgIcon, useTheme } from "@mui/material";
+
+interface DatabaseIconProps {
+ className?: string;
+}
+
+const DatabaseIcon: React.FC = ({ className }) => {
+ const theme = useTheme();
+ const iconColor = theme.customStyles.icon?.main;
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default DatabaseIcon;
diff --git a/ProductivitySuite/ui/react/src/icons/Recent.tsx b/ProductivitySuite/ui/react/src/icons/Recent.tsx
new file mode 100644
index 0000000000..6018916654
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/icons/Recent.tsx
@@ -0,0 +1,29 @@
+import { SvgIcon, useTheme } from "@mui/material";
+
+interface RecentIconProps {
+ className?: string;
+}
+
+const RecentIcon: React.FC = ({ className }) => {
+ const theme = useTheme();
+ const iconColor = theme.customStyles.icon?.main;
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default RecentIcon;
diff --git a/ProductivitySuite/ui/react/src/icons/Waiting.tsx b/ProductivitySuite/ui/react/src/icons/Waiting.tsx
new file mode 100644
index 0000000000..f2767d6884
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/icons/Waiting.tsx
@@ -0,0 +1,45 @@
+import { useTheme } from "styled-components";
+
+const WaitingIcon = () => {
+ const theme = useTheme();
+ const iconColor = theme.customStyles.icon?.main;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default WaitingIcon;
diff --git a/ProductivitySuite/ui/react/src/index.scss b/ProductivitySuite/ui/react/src/index.scss
index 53e71621ea..bf8ec543cb 100644
--- a/ProductivitySuite/ui/react/src/index.scss
+++ b/ProductivitySuite/ui/react/src/index.scss
@@ -1,20 +1,56 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
+// Before javascript styles
-@import "@mantine/core/styles.css";
+html {
+ font-size: 16px;
+}
-:root {
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+body {
+ margin: 0;
+ font-family: "Inter", serif;
+ font-optical-sizing: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ height: 100vh;
line-height: 1.5;
- font-weight: 400;
}
-html,
-body {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- overflow: hidden;
+#root {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
+}
+
+:root {
+ --header-height: 72px;
+ --header-gutter: 1.5rem;
+ --sidebar-width: 260px;
+ --vertical-spacer: 2rem;
+ --content-width: 800px;
+ --content-gutter: 3rem;
+ --input-radius: 30px;
+ --copy-color: #3d447f;
+}
+
+::-webkit-scrollbar {
+ background: transparent;
+ width: 10px;
+}
+
+/* Style the thumb (the draggable part of the scrollbar) */
+::-webkit-scrollbar-thumb {
+ height: 20px;
+ background-color: rgba(0, 0, 0, 0.3);
+ /* Thumb color */
+ border-radius: 5px;
+ /* Optional, for rounded corners */
+}
+
+/* Optionally, you can add hover effects for the thumb */
+::-webkit-scrollbar-thumb:hover {
+ background-color: rgba(0, 0, 0, 0.5);
+ /* Darker thumb when hovered */
}
diff --git a/ProductivitySuite/ui/react/src/index.tsx b/ProductivitySuite/ui/react/src/index.tsx
new file mode 100644
index 0000000000..04fb23312b
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/index.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+import { createRoot } from "react-dom/client";
+import "./index.scss";
+import App from "./App";
+import { Provider } from "react-redux";
+import { store } from "@redux/store";
+import { ThemeProvider } from "@contexts/ThemeContext";
+import keycloak from "@root/keycloak";
+import { ReactKeycloakProvider } from "@react-keycloak/web";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+ //@ts-ignore
+
+
+
+
+
+
+ ,
+);
diff --git a/ProductivitySuite/ui/react/src/keycloack.ts b/ProductivitySuite/ui/react/src/keycloack.ts
deleted file mode 100644
index 26ba47511c..0000000000
--- a/ProductivitySuite/ui/react/src/keycloack.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-import Keycloak from "keycloak-js";
-import { KEYCLOACK_SERVICE_URL } from "./config";
-const keycloak = new Keycloak({
- url: KEYCLOACK_SERVICE_URL,
- realm: "productivitysuite",
- clientId: "productivitysuite",
-});
-
-export default keycloak;
diff --git a/ProductivitySuite/ui/react/src/keycloak.ts b/ProductivitySuite/ui/react/src/keycloak.ts
new file mode 100644
index 0000000000..870888253b
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/keycloak.ts
@@ -0,0 +1,17 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+//@ts-ignore
+import Keycloak from "keycloak-js";
+
+import { KEYCLOAK_SERVICE_URL } from "@root/config";
+
+const keyCloakConfig = {
+ url: KEYCLOAK_SERVICE_URL,
+ realm: "productivitysuite",
+ clientId: "productivitysuite",
+};
+
+const keycloak = new Keycloak(keyCloakConfig);
+
+export default keycloak;
diff --git a/ProductivitySuite/ui/react/src/layouts/Main/MainLayout.module.scss b/ProductivitySuite/ui/react/src/layouts/Main/MainLayout.module.scss
new file mode 100644
index 0000000000..0736eaa367
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/layouts/Main/MainLayout.module.scss
@@ -0,0 +1,21 @@
+.mainLayout {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ max-height: 100%;
+ overflow: hidden;
+}
+
+.mainWrapper {
+ display: flex;
+ flex-direction: row;
+ flex-grow: 1;
+ max-height: 100%;
+ overflow: auto;
+ overflow-x: hidden;
+}
+
+.contentWrapper {
+ max-height: 100%;
+ width: 100%;
+}
diff --git a/ProductivitySuite/ui/react/src/layouts/Main/MainLayout.tsx b/ProductivitySuite/ui/react/src/layouts/Main/MainLayout.tsx
new file mode 100644
index 0000000000..2965e70555
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/layouts/Main/MainLayout.tsx
@@ -0,0 +1,39 @@
+import Header from "@components/Header/Header";
+import { SideBarSpacer } from "@components/SideBar/SideBar";
+import { useState } from "react";
+import { Outlet } from "react-router-dom";
+import styles from "./MainLayout.module.scss";
+
+interface MainLayoutProps {
+ chatView?: boolean;
+ historyView?: boolean;
+ dataView?: boolean;
+}
+
+const MainLayout: React.FC = ({
+ chatView = false,
+ historyView = false,
+ dataView = false,
+}) => {
+ const [asideOpen, setAsideOpen] = useState(false);
+
+ return (
+
+ );
+};
+
+export default MainLayout;
diff --git a/ProductivitySuite/ui/react/src/layouts/Minimal/MinimalLayout.module.scss b/ProductivitySuite/ui/react/src/layouts/Minimal/MinimalLayout.module.scss
new file mode 100644
index 0000000000..2a2ff37fee
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/layouts/Minimal/MinimalLayout.module.scss
@@ -0,0 +1,10 @@
+.minimalLayout {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+ padding: 2rem;
+ text-align: center;
+}
diff --git a/ProductivitySuite/ui/react/src/layouts/Minimal/MinimalLayout.tsx b/ProductivitySuite/ui/react/src/layouts/Minimal/MinimalLayout.tsx
new file mode 100644
index 0000000000..0ccc5aea88
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/layouts/Minimal/MinimalLayout.tsx
@@ -0,0 +1,13 @@
+// About pages or privacy policy are likely minimal layouts
+import { Outlet } from "react-router-dom";
+import styles from "./MinimalLayout.module.scss";
+
+const MinimalLayout = () => {
+ return (
+
+
+
+ );
+};
+
+export default MinimalLayout;
diff --git a/ProductivitySuite/ui/react/src/layouts/ProtectedRoute/ProtectedRoute.tsx b/ProductivitySuite/ui/react/src/layouts/ProtectedRoute/ProtectedRoute.tsx
new file mode 100644
index 0000000000..521e766840
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/layouts/ProtectedRoute/ProtectedRoute.tsx
@@ -0,0 +1,29 @@
+import { useAppSelector } from "@redux/store";
+import { userSelector } from "@redux/User/userSlice";
+import React, { useEffect } from "react";
+
+interface ProtectedRouteProps {
+ component: React.ComponentType;
+ requiredRoles: string[];
+}
+
+const ProtectedRoute: React.FC = ({
+ component: Component,
+ requiredRoles,
+}) => {
+ const { isAuthenticated, role } = useAppSelector(userSelector);
+
+ const isAllowed = React.useMemo(() => {
+ return isAuthenticated && requiredRoles.includes(role);
+ }, [isAuthenticated, role, requiredRoles.join(",")]);
+
+ if (!isAllowed) {
+ return (
+ Access Denied: You do not have permission to view this page.
+ );
+ }
+
+ return ;
+};
+
+export default ProtectedRoute;
diff --git a/ProductivitySuite/ui/react/src/logo.svg b/ProductivitySuite/ui/react/src/logo.svg
new file mode 100644
index 0000000000..9dfc1c058c
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ProductivitySuite/ui/react/src/main.tsx b/ProductivitySuite/ui/react/src/main.tsx
deleted file mode 100644
index a1949c00fe..0000000000
--- a/ProductivitySuite/ui/react/src/main.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-import ReactDOM from "react-dom/client"
-import App from "./App.tsx"
-import "./index.scss"
-import { Provider } from 'react-redux'
-import { store } from "./redux/store.ts"
-import keycloak from "./keycloack.ts"
-import { ReactKeycloakProvider } from "@react-keycloak/web";
-
-
-
-ReactDOM.createRoot(document.getElementById("root")!).render(
- <>
-
-
-
-
-
- >
-)
diff --git a/ProductivitySuite/ui/react/src/pages/Chat/ChatView.module.scss b/ProductivitySuite/ui/react/src/pages/Chat/ChatView.module.scss
new file mode 100644
index 0000000000..023bcd485d
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/pages/Chat/ChatView.module.scss
@@ -0,0 +1,39 @@
+.chatView {
+ display: flex;
+ flex-direction: column;
+ max-height: 100%;
+ height: 100%;
+
+ .messagesWrapper {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ padding: calc(var(--header-gutter) * 2) calc(var(--header-gutter));
+ max-height: 100%;
+ overflow-x: auto;
+
+ @media screen and (min-width: 1200px) {
+ padding: calc(var(--header-gutter) * 2);
+ }
+
+ .messageContent {
+ width: 100%;
+ max-width: var(--content-width);
+ margin: 0px auto;
+
+ pre {
+ margin: 0;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ }
+ }
+ }
+
+ .inputWrapper {
+ display: block;
+ margin: 0px auto;
+ padding: calc(var(--header-gutter) * 1.5) calc(var(--header-gutter) * 1);
+ max-width: calc((var(--header-gutter) * 2) + 800px);
+ width: 100%;
+ }
+}
diff --git a/ProductivitySuite/ui/react/src/pages/Chat/ChatView.tsx b/ProductivitySuite/ui/react/src/pages/Chat/ChatView.tsx
new file mode 100644
index 0000000000..6bbeaa06a8
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/pages/Chat/ChatView.tsx
@@ -0,0 +1,349 @@
+import { useEffect, useRef, JSX } from "react";
+import styles from "./ChatView.module.scss";
+import { useLocation, useNavigate, useParams } from "react-router-dom";
+
+import { Box } from "@mui/material";
+import PrimaryInput from "@components/PrimaryInput/PrimaryInput";
+
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import {
+ abortStream,
+ conversationSelector,
+ doCodeGen,
+ doConversation,
+ doSummaryFaq,
+ getConversationHistory,
+ newConversation,
+ setSelectedConversationId,
+} from "@redux/Conversation/ConversationSlice";
+import { userSelector } from "@redux/User/userSlice";
+import ChatUser from "@components/Chat_User/ChatUser";
+import ChatAssistant from "@components/Chat_Assistant/ChatAssistant";
+import { Message, MessageRole } from "@redux/Conversation/Conversation";
+import { getCurrentTimeStamp, readFilesAndSummarize } from "@utils/utils";
+import ChatSources from "@components/Chat_Sources/ChatSources";
+
+const ChatView = () => {
+ const { name } = useAppSelector(userSelector);
+ const {
+ selectedConversationHistory,
+ type,
+ sourceLinks,
+ sourceFiles,
+ temperature,
+ token,
+ model,
+ systemPrompt,
+ selectedConversationId,
+ onGoingResult,
+ isPending,
+ } = useAppSelector(conversationSelector);
+
+ const systemPromptObject: Message = {
+ role: MessageRole.System,
+ content: systemPrompt,
+ };
+
+ const dispatch = useAppDispatch();
+ const navigate = useNavigate();
+
+ // existing chat
+ const { conversation_id } = useParams();
+
+ // new chat
+ const { state } = useLocation();
+ const initialMessage = state?.initialMessage || null;
+ const isSummary = type === "summary" || false;
+ const isCodeGen = type === "code" || false;
+ const isChat = type === "chat" || false;
+ const isFaq = type === "faq" || false;
+
+ const fromHome = useRef(false);
+ const newMessage = useRef(false);
+
+ const scrollContainer = useRef(null);
+ const autoScroll = useRef(true);
+ const scrollTimeout = useRef(null);
+
+ const messagesBeginRef = useRef(null);
+ const messagesEndRef = useRef(null);
+
+ // Scroll to top of fetched message
+ const scrollToTop = () => {
+ messagesBeginRef.current?.scrollIntoView({ behavior: "smooth" });
+ };
+
+ // Scroll to the latest message
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+ };
+
+ const handleUserScroll = () => {
+ if (scrollContainer.current) {
+ const { scrollTop, scrollHeight, clientHeight } = scrollContainer.current;
+
+ // Disable autoscroll if the user scrolls up significantly
+ if (scrollTop + clientHeight < scrollHeight - 50) {
+ autoScroll.current = false;
+ } else {
+ // Use a timeout to delay re-enabling autoscroll, preventing rapid toggling
+ if (scrollTimeout.current) clearTimeout(scrollTimeout.current);
+ scrollTimeout.current = setTimeout(() => {
+ autoScroll.current = true;
+ }, 500); // Delay auto-scroll reactivation
+ }
+ }
+ };
+
+ useEffect(() => {
+ const container = scrollContainer.current;
+ if (!container) return;
+
+ container.addEventListener("scroll", handleUserScroll);
+
+ return () => {
+ container.removeEventListener("scroll", handleUserScroll);
+ if (scrollTimeout.current) clearTimeout(scrollTimeout.current);
+ if (onGoingResult) dispatch(abortStream());
+ console.log("Reset Convo, preserve settings");
+ dispatch(newConversation(false));
+ };
+ }, []);
+
+ useEffect(() => {
+ if (onGoingResult && autoScroll.current) {
+ scrollToBottom();
+ }
+ }, [onGoingResult]);
+
+ useEffect(() => {
+ if (!name) return;
+
+ // reset view (not full reset)
+ // dispatch(newConversation(false)) // moved to useEffect unmount
+
+ // convo starting, new conversation id inboud
+ if (!conversation_id) fromHome.current = true;
+
+ // existing convo, load and scroll up
+ if (conversation_id && conversation_id !== "new") {
+ dispatch(setSelectedConversationId(conversation_id));
+ dispatch(
+ getConversationHistory({ user: name, conversationId: conversation_id }),
+ );
+ scrollToTop();
+ return;
+ } else if (conversation_id === "new") {
+ // new convo
+ fromHome.current = true;
+
+ if (
+ (isSummary || isFaq) &&
+ ((sourceLinks && sourceLinks.length > 0) ||
+ (sourceFiles && sourceFiles.length > 0) ||
+ initialMessage)
+ ) {
+ // console.log('SUMMARY/FAQ')
+ newSummaryOrFaq();
+ return;
+ }
+
+ if (isCodeGen && initialMessage) {
+ // console.log('CODE')
+ newCodeGen();
+ return;
+ }
+
+ if (isChat && initialMessage) {
+ // console.log('NEW CHAT')
+ newChat();
+ return;
+ }
+
+ // no match for view, go home
+ console.log("Go Home");
+ navigate("/");
+ }
+ }, [conversation_id, name]);
+
+ const newSummaryOrFaq = async () => {
+ const userPrompt: Message = {
+ role: MessageRole.User,
+ content: initialMessage,
+ time: getCurrentTimeStamp().toString(),
+ };
+
+ let prompt = {
+ conversationId: selectedConversationId,
+ userPrompt,
+ messages: initialMessage,
+ model,
+ files: sourceFiles,
+ temperature,
+ token,
+ type, // TODO: cannot past type
+ };
+
+ doSummaryFaq(prompt);
+ };
+
+ const newChat = () => {
+ const userPrompt: Message = {
+ role: MessageRole.User,
+ content: initialMessage,
+ time: getCurrentTimeStamp().toString(),
+ };
+
+ let messages: Message[] = [];
+ messages = [systemPromptObject, ...selectedConversationHistory];
+
+ let prompt = {
+ conversationId: selectedConversationId,
+ userPrompt,
+ messages,
+ model,
+ temperature,
+ token,
+ time: getCurrentTimeStamp().toString(), // TODO: cannot past time
+ type, // TODO: cannot past type
+ };
+
+ doConversation(prompt);
+ };
+
+ const newCodeGen = () => {
+ const userPrompt: Message = {
+ role: MessageRole.User,
+ content: initialMessage,
+ time: getCurrentTimeStamp().toString(),
+ };
+
+ let prompt = {
+ conversationId: selectedConversationId,
+ userPrompt: userPrompt,
+ messages: [],
+ model,
+ temperature,
+ token,
+ time: getCurrentTimeStamp().toString(), // TODO: cannot past time
+ type, // TODO: cannot past type
+ };
+
+ doCodeGen(prompt);
+ };
+
+ // ADD to existing conversation
+ const addMessage = (query: string) => {
+ const userPrompt: Message = {
+ role: MessageRole.User,
+ content: query,
+ time: getCurrentTimeStamp().toString(),
+ };
+
+ let messages: Message[] = [];
+
+ messages = [...selectedConversationHistory];
+
+ let prompt = {
+ conversationId: selectedConversationId,
+ userPrompt,
+ messages,
+ model,
+ temperature,
+ token,
+ type,
+ };
+
+ doConversation(prompt);
+ };
+
+ const handleSendMessage = async (messageContent: string) => {
+ newMessage.current = true;
+ addMessage(messageContent);
+ };
+
+ const displayChatUser = (message: Message) => {
+ // file post will not have message, will display file.extension instead
+ if ((isSummary || isFaq) && !message.content) return;
+
+ // normal message
+ if (message.role === MessageRole.User) {
+ return ;
+ }
+ };
+
+ const displayMessage = () => {
+ let messagesDisplay: JSX.Element[] = [];
+
+ selectedConversationHistory.map((message, messageIndex) => {
+ const timestamp = message.time || Math.random();
+ if (message.role !== MessageRole.System) {
+ messagesDisplay.push(
+
+ {displayChatUser(message)}
+ {message.role === MessageRole.Assistant && (
+
+ )}
+ ,
+ );
+ }
+ });
+
+ if (onGoingResult) {
+ const continueMessage: Message = {
+ role: MessageRole.Assistant,
+ content: onGoingResult,
+ time: Date.now().toString(),
+ };
+
+ messagesDisplay.push(
+
+
+ ,
+ );
+ } else if (isPending) {
+ const continueMessage: Message = {
+ role: MessageRole.Assistant,
+ content: "",
+ time: Date.now().toString(),
+ };
+
+ messagesDisplay.push(
+
+
+ ,
+ );
+ }
+
+ return messagesDisplay;
+ };
+
+ return !selectedConversationHistory ? (
+ <>>
+ ) : (
+
+
+
+
+
+
+ {displayMessage()}
+
+
+
+
+
+
+ );
+};
+
+export default ChatView;
diff --git a/ProductivitySuite/ui/react/src/pages/DataSource/DataSourceManagement.module.scss b/ProductivitySuite/ui/react/src/pages/DataSource/DataSourceManagement.module.scss
new file mode 100644
index 0000000000..28e6863781
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/pages/DataSource/DataSourceManagement.module.scss
@@ -0,0 +1,71 @@
+.dataView {
+ height: 100%;
+ width: 100%;
+ max-width: var(--content-width);
+ width: 100%;
+ margin: 0px auto;
+ padding: calc(var(--header-gutter) * 2);
+}
+
+.dataItem {
+ margin-bottom: 1rem;
+ position: relative;
+ padding: 0;
+
+ :global {
+ .MuiCheckbox-root {
+ position: absolute;
+ right: 100%;
+ margin-right: 0.25rem;
+ top: 50%;
+ transform: translateY(-50%);
+ @media screen and (min-width: 901px) {
+ margin-right: 1rem;
+ }
+ }
+ }
+}
+
+.dataName {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: flex-start;
+ width: 100%;
+ padding: 1rem;
+ margin: 0;
+ line-height: 1.5;
+}
+
+.searchInput {
+ width: 100%;
+ margin-bottom: 1rem;
+ background: none;
+
+ // padding: var(--header-gutter);
+ border: 0;
+ margin-bottom: 2rem;
+ // margin-right: 45px;
+ &:focus {
+ outline: none;
+ }
+
+ :global {
+ .MuiInputBase-root {
+ border-radius: var(--input-radius);
+ }
+ }
+}
+
+.dataInputWrapper {
+ width: 100%;
+ margin-top: 2rem;
+ margin-bottom: 2rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.actions button {
+ margin-left: 0.5rem;
+}
diff --git a/ProductivitySuite/ui/react/src/pages/DataSource/DataSourceManagement.tsx b/ProductivitySuite/ui/react/src/pages/DataSource/DataSourceManagement.tsx
new file mode 100644
index 0000000000..cb0fe9a31d
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/pages/DataSource/DataSourceManagement.tsx
@@ -0,0 +1,242 @@
+import {
+ Box,
+ Checkbox,
+ FormControlLabel,
+ List,
+ ListItem,
+ Typography,
+ FormGroup,
+} from "@mui/material";
+import { useTheme } from "@mui/material/styles";
+import { useEffect, useState } from "react";
+import styles from "./DataSourceManagement.module.scss";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import {
+ conversationSelector,
+ deleteInDataSource,
+ getAllFilesInDataSource,
+ deleteMultipleInDataSource,
+} from "@redux/Conversation/ConversationSlice";
+import { file } from "@redux/Conversation/Conversation";
+import DropDown from "@components/DropDown/DropDown";
+import DataWebInput from "@components/Data_Web/DataWebInput";
+import FileInput from "@components/File_Input/FileInput";
+import SearchInput from "@components/SearchInput/SearchInput";
+import {
+ DeleteButton,
+ SolidButton,
+ TextButton,
+} from "@root/shared/ActionButtons";
+
+const DataSourceManagement = () => {
+ const dispatch = useAppDispatch();
+
+ const theme = useTheme();
+
+ const { filesInDataSource } = useAppSelector(conversationSelector);
+
+ const [dataList, setDataList] = useState([]);
+ const [activeSourceType, setActiveSourceType] = useState("documents");
+ const [selectActive, setSelectActive] = useState(false);
+ const [selectAll, setSelectAll] = useState(false);
+ const [checkedItems, setCheckedItems] = useState>({});
+
+ useEffect(() => {
+ dispatch(getAllFilesInDataSource({ knowledgeBaseId: "default" }));
+ }, []);
+
+ const sortFiles = () => {
+ if (activeSourceType === "web") {
+ let webFiles = filesInDataSource.filter((file) =>
+ file.name.startsWith("http"),
+ );
+ return webFiles;
+ } else {
+ let otherFiles = filesInDataSource.filter(
+ (file) => !file.name.startsWith("http"),
+ );
+ return otherFiles;
+ }
+ };
+
+ useEffect(() => {
+ setDataList(sortFiles());
+ }, [filesInDataSource, activeSourceType]);
+
+ const handleCheckboxChange = (conversationId: string) => {
+ setCheckedItems((prev) => ({
+ ...prev,
+ [conversationId]: !prev[conversationId],
+ }));
+ };
+
+ const displayFiles = () => {
+ return dataList.map((file: file) => {
+ const isChecked = !!checkedItems[file.id];
+
+ const fileText = (
+ <>
+ {file.name}
+ {/* TODO: timestamp for all conversations? */}
+ {/* Last message {convertTime(conversation.updated_at)} */}
+ >
+ );
+
+ const controlCheckBox = (
+ handleCheckboxChange(file.id)}
+ checked={isChecked}
+ />
+ );
+
+ return (
+
+ {selectActive ? (
+
+ ) : (
+
+ {fileText}
+
+ )}
+
+ );
+ });
+ };
+
+ const cancelSelect = () => {
+ setSelectActive(false);
+ setSelectAll(false);
+ setCheckedItems({});
+ };
+
+ const deleteSelected = () => {
+ setSelectActive(false);
+
+ let files = [];
+ for (const [key, value] of Object.entries(checkedItems)) {
+ if (value === true) {
+ files.push(key);
+ }
+ }
+
+ if (files.length > 0) {
+ //update current state
+ setDataList((prev) => prev.filter((item) => !checkedItems[item.id]));
+ dispatch(deleteMultipleInDataSource({ files: files }));
+ }
+ };
+
+ const handleSelectAll = () => {
+ const newSelectAll = !selectAll;
+ setSelectAll(newSelectAll);
+
+ // Add all items' checked state
+ const updatedCheckedItems: Record = {};
+ dataList.forEach((file) => {
+ updatedCheckedItems[file.id] = newSelectAll;
+ });
+
+ setCheckedItems(updatedCheckedItems);
+ };
+
+ const handleSearch = (value: string) => {
+ const filteredList = dataList;
+ const searchResults = filteredList.filter((file: file) =>
+ file.name?.toLowerCase().includes(value.toLowerCase()),
+ );
+ setDataList(value ? searchResults : sortFiles());
+ };
+
+ const updateSource = (value: string) => {
+ setActiveSourceType(value);
+ };
+
+ const displayInput = () => {
+ let input = null;
+ if (activeSourceType === "documents")
+ input = ;
+ if (activeSourceType === "web") input = ;
+ if (activeSourceType === "images")
+ input = ;
+
+ return {input} ;
+ };
+
+ return (
+
+
+
+
+ }
+ />
+
+
+
+ {displayInput()}
+
+
+
+
+
+ You have {dataList.length} file{dataList.length !== 1 && "s"}
+
+
+ {dataList.length > 0 && (
+
+ {selectActive ? (
+ handleSelectAll()}>
+ Select All
+
+ ) : (
+ setSelectActive(true)}>
+ Select
+
+ )}
+
+ {selectActive && (
+ <>
+ cancelSelect()}>Cancel
+ deleteSelected()}>
+ Delete Selected
+
+ >
+ )}
+
+ )}
+
+
+ {displayFiles()}
+
+ );
+};
+
+export default DataSourceManagement;
diff --git a/ProductivitySuite/ui/react/src/pages/History/HistoryView.module.scss b/ProductivitySuite/ui/react/src/pages/History/HistoryView.module.scss
new file mode 100644
index 0000000000..6c4c6d5b15
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/pages/History/HistoryView.module.scss
@@ -0,0 +1,82 @@
+.historyView {
+ height: 100%;
+ width: 100%;
+ max-width: var(--content-width);
+ width: 100%;
+ margin: 0px auto;
+ padding: calc(var(--header-gutter) * 2);
+
+ .historyListWrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ @media screen and (min-width: 901px) {
+ flex-direction: row;
+ justify-content: space-between;
+ }
+ }
+
+ .actions button {
+ margin-left: 0.5rem;
+ margin-top: 0.5rem;
+ margin-bottom: 0.5rem;
+ &:first-of-type {
+ margin-left: 0;
+ }
+ @media screen and (min-width: 901px) {
+ margin-left: 0.5rem;
+ margin-bottom: 0;
+ margin-top: 0;
+ &:first-of-type {
+ margin-left: 0.5rem;
+ }
+ }
+ }
+}
+
+.historyItem {
+ margin-bottom: 1rem;
+ position: relative;
+ padding: 0;
+
+ :global {
+ .MuiCheckbox-root {
+ position: absolute;
+ right: 100%;
+ margin-right: 0.25rem;
+ top: 50%;
+ transform: translateY(-50%);
+
+ @media screen and (min-width: 901px) {
+ margin-right: 1rem;
+ }
+ }
+ }
+
+ a {
+ text-decoration: none;
+ }
+
+ .title {
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ word-break: break-word;
+ max-width: 100%;
+ }
+}
+
+.historyLink {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: flex-start;
+ width: 100%;
+ padding: 1rem;
+ margin: 0;
+ line-height: 1.5;
+}
diff --git a/ProductivitySuite/ui/react/src/pages/History/HistoryView.tsx b/ProductivitySuite/ui/react/src/pages/History/HistoryView.tsx
new file mode 100644
index 0000000000..3a59e5e68e
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/pages/History/HistoryView.tsx
@@ -0,0 +1,214 @@
+import {
+ Box,
+ Checkbox,
+ FormControlLabel,
+ List,
+ ListItem,
+ Typography,
+ Link,
+} from "@mui/material";
+import { useTheme } from "@mui/material/styles";
+import { useState } from "react";
+import styles from "./HistoryView.module.scss";
+
+import { Link as RouterLink } from "react-router-dom";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import {
+ conversationSelector,
+ deleteConversation,
+ deleteConversations,
+} from "@redux/Conversation/ConversationSlice";
+import { Conversation } from "@redux/Conversation/Conversation";
+import { userSelector } from "@redux/User/userSlice";
+import SearchInput from "@components/SearchInput/SearchInput";
+import {
+ DeleteButton,
+ SolidButton,
+ TextButton,
+} from "@root/shared/ActionButtons";
+
+interface HistoryViewProps {
+ shared: boolean;
+}
+
+const HistoryView: React.FC = ({ shared }) => {
+ const dispatch = useAppDispatch();
+ const { name } = useAppSelector(userSelector);
+
+ const theme = useTheme();
+
+ const { conversations, sharedConversations } =
+ useAppSelector(conversationSelector);
+
+ const [historyList, setHistoryList] = useState(
+ shared ? sharedConversations : conversations,
+ );
+ const [selectActive, setSelectActive] = useState(false);
+ const [selectAll, setSelectAll] = useState(false);
+ const [checkedItems, setCheckedItems] = useState>({});
+
+ const convertTime = (timestamp: number) => {
+ const now = Math.floor(Date.now() / 1000);
+ const diffInSeconds = now - timestamp;
+
+ const diffInMinutes = Math.floor(diffInSeconds / 60);
+ const diffInHours = Math.floor(diffInSeconds / 3600);
+ const diffInDays = Math.floor(diffInSeconds / 86400);
+
+ if (diffInDays > 0) {
+ return `${diffInDays} day${diffInDays > 1 ? "s" : ""} ago`;
+ } else if (diffInHours > 0) {
+ return `${diffInHours} hour${diffInHours > 1 ? "s" : ""} ago`;
+ } else {
+ return `${diffInMinutes} minute${diffInMinutes > 1 ? "s" : ""} ago`;
+ }
+ };
+
+ const handleCheckboxChange = (conversationId: string) => {
+ setCheckedItems((prev) => ({
+ ...prev,
+ [conversationId]: !prev[conversationId],
+ }));
+ };
+
+ const displayHistory = () => {
+ return historyList.map((conversation: Conversation) => {
+ const isChecked = !!checkedItems[conversation.id];
+
+ const itemText = (
+ <>
+
+ {conversation.first_query}
+
+ {/* TODO: timestamp for all conversations? */}
+ {/* Last message {convertTime(conversation.updated_at)} */}
+ >
+ );
+
+ const controlCheckBox = (
+ handleCheckboxChange(conversation.id)}
+ checked={isChecked}
+ />
+ );
+
+ return (
+
+ {selectActive ? (
+
+ ) : (
+
+ {/* body1 Typography is automatically applied in label above, added here to match for spacing */}
+ {itemText}
+
+ )}
+
+ );
+ });
+ };
+
+ const cancelSelect = () => {
+ setSelectActive(false);
+ setSelectAll(false);
+ setCheckedItems({});
+ };
+
+ const deleteSelected = () => {
+ setSelectActive(false);
+
+ let ids = [];
+ for (const [key, value] of Object.entries(checkedItems)) {
+ if (value === true) {
+ ids.push(key);
+ }
+ }
+
+ if (ids.length > 0) {
+ //update current state
+ setHistoryList((prev) =>
+ prev.filter((conversation) => !checkedItems[conversation.id]),
+ );
+ dispatch(
+ deleteConversations({ user: name, conversationIds: ids, useCase: "" }),
+ );
+ }
+ };
+
+ const handleSelectAll = () => {
+ const newSelectAll = !selectAll;
+ setSelectAll(newSelectAll);
+
+ // Add all items' checked state
+ const updatedCheckedItems: Record = {};
+ historyList.forEach((conversation) => {
+ updatedCheckedItems[conversation.id] = newSelectAll;
+ });
+
+ setCheckedItems(updatedCheckedItems);
+ };
+
+ const handleSearch = (value: string) => {
+ const filteredList = shared ? sharedConversations : conversations;
+ const searchResults = filteredList.filter((conversation: Conversation) =>
+ conversation.first_query?.toLowerCase().includes(value.toLowerCase()),
+ );
+ setHistoryList(
+ value ? searchResults : shared ? sharedConversations : conversations,
+ );
+ };
+
+ return (
+
+
+
+
+
+ You have {historyList.length} previous chat
+ {historyList.length > 1 && "s"}
+
+
+ {historyList.length > 0 && (
+
+ {selectActive ? (
+ handleSelectAll()}>
+ Select All
+
+ ) : (
+ setSelectActive(true)}>
+ Select
+
+ )}
+
+ {selectActive && (
+ <>
+ cancelSelect()}>Cancel
+ deleteSelected()}>
+ Delete Selected
+
+ >
+ )}
+
+ )}
+
+
+
{displayHistory()}
+
+ );
+};
+
+export default HistoryView;
diff --git a/ProductivitySuite/ui/react/src/pages/Home/Home.module.scss b/ProductivitySuite/ui/react/src/pages/Home/Home.module.scss
new file mode 100644
index 0000000000..b4fd3df4cb
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/pages/Home/Home.module.scss
@@ -0,0 +1,39 @@
+.homeView {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 100%;
+ padding: calc(var(--header-gutter) * 2);
+
+ .title {
+ text-align: center;
+
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ }
+
+ .buttonRow {
+ margin-top: var(--vertical-spacer);
+ justify-content: center;
+ }
+
+ .promptWrapper {
+ width: 100%;
+ max-width: 775px;
+ }
+
+ .inputContainer {
+ width: 100%;
+ max-width: 800px;
+ margin-top: var(--vertical-spacer);
+ }
+
+ .disclaimer {
+ width: 100%;
+ max-width: 600px;
+ margin-top: var(--vertical-spacer);
+ font-size: 14px;
+ }
+}
diff --git a/ProductivitySuite/ui/react/src/pages/Home/Home.tsx b/ProductivitySuite/ui/react/src/pages/Home/Home.tsx
new file mode 100644
index 0000000000..503794cdbc
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/pages/Home/Home.tsx
@@ -0,0 +1,102 @@
+import { Button, Typography, Grid2, styled } from "@mui/material";
+import { AtomIcon, AtomAnimation } from "@icons/Atom";
+import PrimaryInput from "@components/PrimaryInput/PrimaryInput";
+import config from "@root/config";
+import PromptSettings from "@components/PromptSettings/PromptSettings";
+import styles from "./Home.module.scss";
+
+import { useNavigate } from "react-router-dom";
+import { useAppDispatch, useAppSelector } from "@redux/store";
+import { userSelector } from "@redux/User/userSlice";
+import {
+ conversationSelector,
+ setType,
+ newConversation,
+} from "@redux/Conversation/ConversationSlice";
+import { useEffect } from "react";
+
+interface InitialStateProps {
+ initialMessage: string;
+}
+
+const HomeButton = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.homeButtons,
+}));
+
+const HomeTitle = styled(Typography)(({ theme }) => ({
+ ...theme.customStyles.homeTitle,
+}));
+
+const Home = () => {
+ const { disclaimer } = config;
+
+ const { type, types, token, model, temperature } =
+ useAppSelector(conversationSelector);
+ const dispatch = useAppDispatch();
+
+ const { name } = useAppSelector(userSelector);
+
+ const navigate = useNavigate();
+
+ const handleSendMessage = async (messageContent: string) => {
+ const initialState: InitialStateProps = {
+ initialMessage: messageContent,
+ };
+
+ navigate(`/${type}/new`, { state: initialState });
+ };
+
+ const handleTypeChange = (updateType: string) => {
+ dispatch(setType(updateType));
+ };
+
+ useEffect(() => {
+ // clean up and reset. Can happen on going home from history/upload convo
+ // if convo is missing one of these
+ if (!model || !token || !temperature) {
+ dispatch(newConversation(true));
+ }
+ }, []);
+
+ return (
+
+ {/*
*/}
+
+
+
+ Hi, {name}. {config.tagline}
+
+
+
+ {types.map((interactionType, index) => (
+ handleTypeChange(interactionType.key)}
+ aria-selected={type === interactionType.key}
+ startIcon={
+
+ }
+ variant="contained"
+ >
+ {interactionType.name}
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Home;
diff --git a/ProductivitySuite/ui/react/src/redux/Conversation/Conversation.ts b/ProductivitySuite/ui/react/src/redux/Conversation/Conversation.ts
index 57ebb5ece2..0714533a8e 100644
--- a/ProductivitySuite/ui/react/src/redux/Conversation/Conversation.ts
+++ b/ProductivitySuite/ui/react/src/redux/Conversation/Conversation.ts
@@ -1,6 +1,21 @@
-// Copyright (C) 2024 Intel Corporation
+// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
+export interface UseCase {
+ use_case: string;
+ display_name: string;
+ access_level: string;
+}
+
+export interface Model {
+ displayName: string;
+ endpoint?: string;
+ maxToken: number;
+ minToken: number;
+ model_name: string;
+ types: string[];
+}
+
export type ConversationRequest = {
conversationId: string;
userPrompt: Message;
@@ -8,7 +23,32 @@ export type ConversationRequest = {
model: string;
temperature: number;
token: number;
+ files?: any[];
+ time?: string;
+ type: string;
+};
+
+export type CodeRequest = {
+ conversationId: string;
+ userPrompt: Message;
+ messages: any[];
+ model: string;
+ type: string;
+ token?: number;
+ temperature?: number;
+};
+
+export type SummaryFaqRequest = {
+ conversationId: string;
+ userPrompt: Message;
+ messages: Message[] | string;
+ files?: any[];
+ model: string;
+ temperature: number;
+ token: number;
+ type: string;
};
+
export enum MessageRole {
Assistant = "assistant",
User = "user",
@@ -16,32 +56,57 @@ export enum MessageRole {
}
export interface Message {
+ message_id?: string;
role: MessageRole;
content: string;
time?: string;
}
+export interface ChatMessageProps {
+ message: Message;
+ pending?: boolean;
+}
+
export interface Conversation {
id: string;
first_query?: string;
}
-type file = {
+export type file = {
name: string;
+ id: string;
+ type: string;
+ parent: string;
};
export interface ConversationReducer {
selectedConversationId: string;
conversations: Conversation[];
+ sharedConversations: Conversation[];
selectedConversationHistory: Message[];
onGoingResult: string;
+ isPending: boolean;
filesInDataSource: file[];
- systemPrompt: string;
+ dataSourceUrlStatus: string;
+
+ useCase: string;
+ useCases: UseCase[];
model: string;
+ models: Model[];
+ type: string;
+ types: any[];
+ systemPrompt: string;
minToken: number;
maxToken: number;
token: number;
minTemperature: number;
maxTemperature: number;
temperature: number;
+ sourceType: string;
+ sourceLinks: string[];
+ sourceFiles: any[];
+
+ abortController: AbortController | null;
+
+ uploadInProgress: boolean;
}
diff --git a/ProductivitySuite/ui/react/src/redux/Conversation/ConversationSlice.ts b/ProductivitySuite/ui/react/src/redux/Conversation/ConversationSlice.ts
index ea7617d82c..f5071f0349 100644
--- a/ProductivitySuite/ui/react/src/redux/Conversation/ConversationSlice.ts
+++ b/ProductivitySuite/ui/react/src/redux/Conversation/ConversationSlice.ts
@@ -1,15 +1,25 @@
-// Copyright (C) 2024 Intel Corporation
+// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
-import { RootState, store } from "../store";
+import { RootState, store } from "@redux/store";
import { fetchEventSource } from "@microsoft/fetch-event-source";
-import { Message, MessageRole, ConversationReducer, ConversationRequest, Conversation } from "./Conversation";
-import { getCurrentTimeStamp } from "../../common/util";
-import { createAsyncThunkWrapper } from "../thunkUtil";
-import client from "../../common/client";
-import { notifications } from "@mantine/notifications";
import {
+ Message,
+ MessageRole,
+ ConversationReducer,
+ ConversationRequest,
+ Conversation,
+ Model,
+ UseCase,
+ CodeRequest,
+ SummaryFaqRequest,
+} from "./Conversation";
+import { getCurrentTimeStamp } from "@utils/utils";
+import { createAsyncThunkWrapper } from "@redux/thunkUtil";
+import axios from "axios";
+
+import config, {
CHAT_QNA_URL,
DATA_PREP_URL,
DATA_PREP_GET_URL,
@@ -17,22 +27,78 @@ import {
CHAT_HISTORY_CREATE,
CHAT_HISTORY_GET,
CHAT_HISTORY_DELETE,
-} from "../../config";
+ CODE_GEN_URL,
+ DOC_SUM_URL,
+ FAQ_GEN_URL,
+} from "@root/config";
+import { NotificationSeverity, notify } from "@components/Notification/Notification";
+import { ChatBubbleOutline, CodeOutlined, Description, QuizOutlined } from "@mui/icons-material";
+
+const urlMap: any = {
+ summary: DOC_SUM_URL,
+ faq: FAQ_GEN_URL,
+ chat: CHAT_QNA_URL,
+ code: CODE_GEN_URL,
+};
+
+const interactionTypes = [
+ {
+ key: "chat",
+ name: "Chat Q&A",
+ icon: ChatBubbleOutline,
+ color: "#0ACA00",
+ },
+ {
+ key: "summary",
+ name: "Summarize Content",
+ icon: Description,
+ color: "#FF4FFC",
+ },
+ {
+ key: "code",
+ name: "Generate Code",
+ icon: CodeOutlined,
+ color: "#489BEA",
+ },
+ // TODO: Enable file upload support for faqgen endpoint similar to summary
+ // {
+ // key: 'faq',
+ // name: 'Generate FAQ',
+ // icon: QuizOutlined,
+ // color: '#9D00FF'
+ // },
+];
const initialState: ConversationReducer = {
conversations: [],
+ sharedConversations: [],
selectedConversationId: "",
selectedConversationHistory: [],
onGoingResult: "",
+ isPending: false,
filesInDataSource: [],
- model: "Intel/neural-chat-7b-v3-3",
- systemPrompt: "You are helpful assistant",
+ dataSourceUrlStatus: "",
+
+ useCase: "",
+ useCases: [],
+ model: "",
+ models: [],
+ type: "chat",
+ types: interactionTypes,
+ systemPrompt: config.defaultChatPrompt,
minToken: 100,
maxToken: 1000,
token: 100,
minTemperature: 0,
maxTemperature: 1,
temperature: 0.4,
+ sourceType: "documents",
+ sourceLinks: [],
+ sourceFiles: [],
+
+ abortController: null,
+
+ uploadInProgress: false,
};
export const ConversationSlice = createSlice({
@@ -46,67 +112,160 @@ export const ConversationSlice = createSlice({
state.selectedConversationHistory = [];
state.filesInDataSource = [];
},
-
+ setIsPending: (state, action: PayloadAction) => {
+ state.isPending = action.payload;
+ },
setOnGoingResult: (state, action: PayloadAction) => {
state.onGoingResult = action.payload;
},
addMessageToMessages: (state, action: PayloadAction) => {
state.selectedConversationHistory.push(action.payload);
},
- newConversation: (state) => {
- (state.selectedConversationId = ""), (state.onGoingResult = ""), (state.selectedConversationHistory = []);
+ newConversation: (state, action: PayloadAction) => {
+ state.selectedConversationId = "";
+ state.onGoingResult = "";
+ state.selectedConversationHistory = [];
+
+ // full reset if true
+ if (action.payload) {
+ (state.sourceLinks = []), (state.sourceFiles = []);
+
+ // in case of upload / history conversation that clears model name, we want to reset to defaults
+ const currentType = state.type;
+ if (currentType) {
+ const approvedModel = state.models.find((item: Model) => item.types.includes(currentType));
+ if (approvedModel) {
+ state.model = approvedModel.model_name;
+ state.token = approvedModel.minToken;
+ state.temperature = 0.4;
+ }
+ }
+ }
+ },
+ updatePromptSettings: (state, action: PayloadAction) => {
+ state.model = action.payload.model;
+ state.token = action.payload.token;
+ state.temperature = action.payload.temperature;
+ state.type = action.payload.type;
},
setSelectedConversationId: (state, action: PayloadAction) => {
state.selectedConversationId = action.payload;
},
+ setSelectedConversationHistory: (state, action: PayloadAction) => {
+ state.selectedConversationHistory = action.payload;
+ },
setTemperature: (state, action: PayloadAction) => {
state.temperature = action.payload;
},
setToken: (state, action: PayloadAction) => {
state.token = action.payload;
},
+ setModel: (state, action: PayloadAction) => {
+ state.model = action.payload.model_name;
+ state.maxToken = action.payload.maxToken;
+ state.minToken = action.payload.minToken;
+ },
+ setModelName: (state, action: PayloadAction) => {
+ state.model = action.payload;
+ },
+ setModels: (state, action: PayloadAction<[]>) => {
+ state.models = action.payload;
+ },
+ setUseCase: (state, action: PayloadAction) => {
+ state.useCase = action.payload;
+ },
+ setUseCases: (state, action: PayloadAction<[]>) => {
+ state.useCases = action.payload;
+ },
+ setType: (state, action: PayloadAction) => {
+ state.type = action.payload;
+
+ switch (action.payload) {
+ case "summary":
+ case "faq":
+ state.systemPrompt = "";
+ state.sourceType = "documents";
+ break;
+ case "chat":
+ case "code":
+ state.systemPrompt = config.defaultChatPrompt;
+ state.sourceFiles = [];
+ state.sourceLinks = [];
+ break;
+ }
+
+ let firstModel = state.models.find((model: Model) => model.types.includes(action.payload));
+ state.model = firstModel?.model_name || state.models[0].model_name;
+ },
+ setUploadInProgress: (state, action: PayloadAction) => {
+ state.uploadInProgress = action.payload;
+ },
+ setSourceLinks: (state, action: PayloadAction) => {
+ state.sourceLinks = action.payload;
+ },
+ setSourceFiles: (state, action: PayloadAction) => {
+ state.sourceFiles = action.payload;
+ },
+ setSourceType: (state, action: PayloadAction) => {
+ state.sourceType = action.payload;
+ },
setSystemPrompt: (state, action: PayloadAction) => {
state.systemPrompt = action.payload;
},
+ setAbortController: (state, action: PayloadAction) => {
+ state.abortController = action.payload;
+ },
+ abortStream: (state) => {
+ if (state.abortController) state.abortController.abort();
+
+ const m: Message = {
+ role: MessageRole.Assistant,
+ content: state.onGoingResult,
+ time: getCurrentTimeStamp().toString(),
+ };
+
+ // add last message before ending
+ state.selectedConversationHistory.push(m);
+ state.onGoingResult = "";
+ state.abortController = null;
+ },
+ setDataSourceUrlStatus: (state, action: PayloadAction) => {
+ state.dataSourceUrlStatus = action.payload;
+ },
+ uploadChat: (state, action: PayloadAction) => {
+ state.selectedConversationHistory = action.payload.messages;
+ state.model = action.payload.model;
+ state.token = action.payload.token;
+ state.temperature = action.payload.temperature;
+ state.type = action.payload.type;
+ state.sourceFiles = []; // only chat can be uploaded, empty if set
+ state.sourceLinks = []; // only chat can be uploaded, empty if set
+ },
},
extraReducers(builder) {
builder.addCase(uploadFile.fulfilled, () => {
- notifications.update({
- id: "upload-file",
- message: "File Uploaded Successfully",
- loading: false,
- autoClose: 3000,
- });
+ notify("File Uploaded Successfully", NotificationSeverity.SUCCESS);
});
builder.addCase(uploadFile.rejected, () => {
- notifications.update({
- color: "red",
- id: "upload-file",
- message: "Failed to Upload file",
- loading: false,
- });
+ notify("Failed to Upload file", NotificationSeverity.ERROR);
});
-
- builder.addCase(submitDataSourceURL.fulfilled, () => {
- notifications.show({
- message: "Submitted Successfully",
- });
+ builder.addCase(submitDataSourceURL.fulfilled, (state) => {
+ notify("Submitted Successfully", NotificationSeverity.SUCCESS);
+ state.dataSourceUrlStatus = ""; // watching for pending only on front
});
- builder.addCase(submitDataSourceURL.rejected, () => {
- notifications.show({
- color: "red",
- message: "Submit Failed",
- });
+ builder.addCase(submitDataSourceURL.rejected, (state) => {
+ notify("Submit Failed", NotificationSeverity.ERROR);
+ state.dataSourceUrlStatus = ""; // watching for pending only on front
+ });
+ builder.addCase(deleteConversation.rejected, () => {
+ notify("Failed to Delete Conversation", NotificationSeverity.ERROR);
});
-
builder.addCase(getAllConversations.fulfilled, (state, action) => {
state.conversations = action.payload;
});
-
builder.addCase(getConversationHistory.fulfilled, (state, action) => {
state.selectedConversationHistory = action.payload;
});
-
builder.addCase(saveConversationtoDatabase.fulfilled, (state, action) => {
if (state.selectedConversationId == "") {
state.selectedConversationId = action.payload;
@@ -114,25 +273,100 @@ export const ConversationSlice = createSlice({
id: action.payload,
first_query: state.selectedConversationHistory[1].content,
});
+ window.history.pushState({}, "", `/chat/${action.payload}`);
}
});
builder.addCase(getAllFilesInDataSource.fulfilled, (state, action) => {
state.filesInDataSource = action.payload;
});
- builder.addCase(deleteConversation.fulfilled, () => {
- notifications.show({
- message: "Conversation Deleted Successfully",
- });
- });
},
});
+export const getSupportedUseCases = createAsyncThunkWrapper(
+ "public/usecase_configs.json",
+ async (_: void, { getState }) => {
+ const response = await axios.get("/usecase_configs.json");
+ store.dispatch(setUseCases(response.data));
+
+ // @ts-ignore
+ const state: RootState = getState();
+ const userAccess = state.userReducer.role;
+ const currentUseCase = state.conversationReducer.useCase;
+
+ // setDefault use case if not stored / already set by localStorage
+ if (!currentUseCase) {
+ const approvedAccess = response.data.find((item: UseCase) => item.access_level === userAccess);
+ if (approvedAccess) store.dispatch(setUseCase(approvedAccess));
+ }
+
+ return response.data;
+ },
+);
+
+export const getSupportedModels = createAsyncThunkWrapper(
+ "public/model_configs.json",
+ async (_: void, { getState }) => {
+ const response = await axios.get("/model_configs.json");
+ store.dispatch(setModels(response.data));
+
+ // @ts-ignore
+ const state: RootState = getState();
+ const currentModel = state.conversationReducer.model;
+ const currentType = state.conversationReducer.type;
+
+ // setDefault use case if not stored / already set by localStorage
+ // TODO: revisit if type also gets stored and not defaulted on state
+ if (!currentModel && currentType) {
+ const approvedModel = response.data.find((item: Model) => item.types.includes(currentType));
+ if (approvedModel) store.dispatch(setModel(approvedModel));
+ }
+
+ return response.data;
+ },
+);
+
+export const getAllConversations = createAsyncThunkWrapper(
+ "conversation/getAllConversations",
+ async ({ user, useCase }: { user: string; useCase: string }, {}) => {
+ //TODO: Add useCase
+ const response = await axios.post(CHAT_HISTORY_GET, {
+ user,
+ });
+
+ return response.data.reverse();
+ },
+);
+
+export const getConversationHistory = createAsyncThunkWrapper(
+ "conversation/getConversationHistory",
+ async ({ user, conversationId }: { user: string; conversationId: string }, {}) => {
+ const response = await axios.post(CHAT_HISTORY_GET, {
+ user,
+ id: conversationId,
+ });
+
+ // update settings for response settings modal
+ store.dispatch(
+ updatePromptSettings({
+ model: response.data.model,
+ token: response.data.max_tokens,
+ temperature: response.data.temperature,
+ type: response.data.request_type,
+ }),
+ );
+
+ return response.data.messages;
+ },
+);
+
export const submitDataSourceURL = createAsyncThunkWrapper(
"conversation/submitDataSourceURL",
async ({ link_list }: { link_list: string[] }, { dispatch }) => {
+ dispatch(setDataSourceUrlStatus("pending"));
const body = new FormData();
body.append("link_list", JSON.stringify(link_list));
- const response = await client.post(DATA_PREP_URL, body);
+ // body.append("parent", "appData"); // TODO: this did not work, in an attempt to sort data types
+ const response = await axios.post(DATA_PREP_URL, body);
dispatch(getAllFilesInDataSource({ knowledgeBaseId: "default" }));
return response.data;
},
@@ -144,31 +378,57 @@ export const getAllFilesInDataSource = createAsyncThunkWrapper(
const body = {
knowledge_base_id: knowledgeBaseId,
};
- const response = await client.post(DATA_PREP_GET_URL, body);
+ const response = await axios.post(DATA_PREP_GET_URL, body);
return response.data;
},
);
+
export const uploadFile = createAsyncThunkWrapper(
"conversation/uploadFile",
async ({ file }: { file: File }, { dispatch }) => {
const body = new FormData();
body.append("files", file);
-
- notifications.show({
- id: "upload-file",
- message: "uploading File",
- loading: true,
- });
- const response = await client.post(DATA_PREP_URL, body);
+ const response = await axios.post(DATA_PREP_URL, body);
dispatch(getAllFilesInDataSource({ knowledgeBaseId: "default" }));
return response.data;
},
);
+export const deleteMultipleInDataSource = createAsyncThunkWrapper(
+ "conversation/deleteConversations",
+ async ({ files }: { files: string[] }, { dispatch }) => {
+ const promises = files.map((file) =>
+ axios
+ .post(DATA_PREP_DELETE_URL, {
+ file_path: file.split("_")[1],
+ })
+ .then((response) => {
+ return response.data;
+ })
+ .catch((err) => {
+ notify("Error deleting file", NotificationSeverity.ERROR);
+ console.error(`Error deleting file`, file, err);
+ }),
+ );
+
+ await Promise.all(promises)
+ .then(() => {
+ notify("Files deleted successfully", NotificationSeverity.SUCCESS);
+ })
+ .catch((err) => {
+ notify("Error deleting on or more of your files", NotificationSeverity.ERROR);
+ console.error("Error deleting on or more of your files", err);
+ })
+ .finally(() => {
+ dispatch(getAllFilesInDataSource({ knowledgeBaseId: "default" }));
+ });
+ },
+);
+
export const deleteInDataSource = createAsyncThunkWrapper(
"conversation/deleteInDataSource",
async ({ file }: { file: any }, { dispatch }) => {
- const response = await client.post(DATA_PREP_DELETE_URL, {
+ const response = await axios.post(DATA_PREP_DELETE_URL, {
file_path: file,
});
dispatch(getAllFilesInDataSource({ knowledgeBaseId: "default" }));
@@ -178,84 +438,186 @@ export const deleteInDataSource = createAsyncThunkWrapper(
export const saveConversationtoDatabase = createAsyncThunkWrapper(
"conversation/saveConversationtoDatabase",
- async ({ conversation }: { conversation: Conversation }, { getState }) => {
+ async ({ conversation }: { conversation: Conversation }, { dispatch, getState }) => {
// @ts-ignore
const state: RootState = getState();
const selectedConversationHistory = state.conversationReducer.selectedConversationHistory;
- const response = await client.post(CHAT_HISTORY_CREATE, {
+
+ //TODO: if we end up with a systemPrompt for code change this
+ const firstMessageIndex = state.conversationReducer.type === "code" ? 0 : 1;
+
+ const response = await axios.post(CHAT_HISTORY_CREATE, {
data: {
user: state.userReducer.name,
messages: selectedConversationHistory,
+ time: getCurrentTimeStamp().toString(),
+ model: state.conversationReducer.model,
+ temperature: state.conversationReducer.temperature,
+ max_tokens: state.conversationReducer.token,
+ request_type: state.conversationReducer.type,
},
id: conversation.id == "" ? null : conversation.id,
- first_query: selectedConversationHistory[1].content,
+ first_query: selectedConversationHistory[firstMessageIndex].content,
});
- return response.data;
- },
-);
-export const getAllConversations = createAsyncThunkWrapper(
- "conversation/getAllConversations",
- async ({ user }: { user: string }, {}) => {
- const response = await client.post(CHAT_HISTORY_GET, {
- user,
- });
+ dispatch(
+ getAllConversations({
+ user: state.userReducer.name,
+ useCase: state.conversationReducer.useCase,
+ }),
+ );
return response.data;
},
);
-export const getConversationHistory = createAsyncThunkWrapper(
- "conversation/getConversationHistory",
- async ({ user, conversationId }: { user: string; conversationId: string }, {}) => {
- const response = await client.post(CHAT_HISTORY_GET, {
- user,
- id: conversationId,
- });
- return response.data.messages;
+export const deleteConversations = createAsyncThunkWrapper(
+ "conversation/deleteConversations",
+ async (
+ { user, conversationIds, useCase }: { user: string; conversationIds: string[]; useCase: string },
+ { dispatch },
+ ) => {
+ const promises = conversationIds.map((id) =>
+ axios
+ .post(CHAT_HISTORY_DELETE, {
+ user,
+ id: id,
+ })
+ .then((response) => {
+ return response.data;
+ })
+ .catch((err) => {
+ notify("Error deleting conversation", NotificationSeverity.ERROR);
+ console.error(`Error deleting conversation ${id}`, err);
+ }),
+ );
+
+ await Promise.all(promises)
+ .then(() => {
+ notify("Conversations deleted successfully", NotificationSeverity.SUCCESS);
+ })
+ .catch((err) => {
+ notify("Error deleting on or more of your conversations", NotificationSeverity.ERROR);
+ console.error("Error deleting on or more of your conversations", err);
+ })
+ .finally(() => {
+ dispatch(getAllConversations({ user, useCase }));
+ });
},
);
export const deleteConversation = createAsyncThunkWrapper(
"conversation/delete",
- async ({ user, conversationId }: { user: string; conversationId: string }, { dispatch }) => {
- const response = await client.post(CHAT_HISTORY_DELETE, {
+ async (
+ { user, conversationId, useCase }: { user: string; conversationId: string; useCase: string },
+ { dispatch },
+ ) => {
+ const response = await axios.post(CHAT_HISTORY_DELETE, {
user,
id: conversationId,
});
- dispatch(newConversation());
- dispatch(getAllConversations({ user }));
+ dispatch(newConversation(false));
+ dispatch(getAllConversations({ user, useCase }));
return response.data;
},
);
export const doConversation = (conversationRequest: ConversationRequest) => {
- const { conversationId, userPrompt, messages, model, token, temperature } = conversationRequest;
- store.dispatch(addMessageToMessages(messages[0]));
+ store.dispatch(setIsPending(true));
+
+ const { conversationId, userPrompt, messages, model, token, temperature, type } = conversationRequest;
+
+ // TODO: MAYBE... check first message if 'system' already exists... on dev during page edits the
+ // hot module reloads and instantly adds more system messages to the total messages
+ if (messages.length === 1) store.dispatch(addMessageToMessages(messages[0])); // do not re-add system prompt
store.dispatch(addMessageToMessages(userPrompt));
- const userPromptWithoutTime = {
+
+ const userPromptWithTime = {
role: userPrompt.role,
content: userPrompt.content,
+ time: getCurrentTimeStamp().toString(),
+ };
+
+ const body = {
+ messages: [...messages, userPromptWithTime],
+ model: model,
+ max_tokens: token,
+ temperature: temperature,
};
+
+ eventStream(type, body, conversationId);
+};
+
+export const doSummaryFaq = (summaryFaqRequest: SummaryFaqRequest) => {
+ store.dispatch(setIsPending(true));
+
+ const { conversationId, model, token, temperature, type, messages, files, userPrompt } = summaryFaqRequest;
+
+ const postWithFiles = files && files.length > 0;
+
+ const body: any = {};
+ const formData = new FormData();
+
+ store.dispatch(addMessageToMessages(userPrompt));
+
+ if (postWithFiles) {
+ formData.append("messages", "");
+ formData.append("model", model);
+ formData.append("max_tokens", token.toString());
+ formData.append("type", "text");
+ formData.append("temperature", temperature.toString());
+
+ files.forEach((file) => {
+ formData.append("files", file.file);
+ });
+
+ formDataEventStream(urlMap[type], formData);
+ } else {
+ body.messages = messages;
+ body.model = model;
+ (body.max_tokens = token), (body.temperature = temperature);
+ body.type = "text";
+
+ eventStream(type, body, conversationId);
+ }
+};
+
+export const doCodeGen = (codeRequest: CodeRequest) => {
+ store.dispatch(setIsPending(true));
+
+ const { conversationId, userPrompt, model, token, temperature, type } = codeRequest;
+
+ store.dispatch(addMessageToMessages(userPrompt));
+
const body = {
- messages: [...messages, userPromptWithoutTime],
- model,
- max_new_tokens: token,
+ messages: userPrompt.content,
+ model: model, //'meta-llama/Llama-3.3-70B-Instruct',
+ max_tokens: token,
temperature: temperature,
};
- // let conversation: Conversation;
+ eventStream(type, body, conversationId);
+};
+
+const eventStream = (type: string, body: any, conversationId: string = "") => {
+ const abortController = new AbortController();
+ store.dispatch(setAbortController(abortController));
+ const signal = abortController.signal;
+
let result = "";
+
try {
- fetchEventSource(CHAT_QNA_URL, {
+ fetchEventSource(urlMap[type], {
method: "POST",
+ body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
},
- body: JSON.stringify(body),
+ signal,
openWhenHidden: true,
async onopen(response) {
if (response.ok) {
+ store.dispatch(setIsPending(false));
return;
} else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
const e = await response.json();
@@ -263,19 +625,45 @@ export const doConversation = (conversationRequest: ConversationRequest) => {
throw Error(e.error.message);
} else {
console.log("error", response);
+ notify("Error in opening stream", NotificationSeverity.ERROR);
}
},
onmessage(msg) {
if (msg?.data != "[DONE]") {
try {
- const match = msg.data.match(/b'([^']*)'/);
- if (match && match[1] != "") {
- const extractedText = match[1];
- result += extractedText;
+ if (type === "code") {
+ const parsedData = JSON.parse(msg.data);
+ result += parsedData.choices[0].text;
store.dispatch(setOnGoingResult(result));
}
+ if (type !== "summary" && type !== "faq") {
+ //parse content for data: "b"
+ const match = msg.data.match(/b'([^']*)'/);
+ if (match && match[1] != "") {
+ const extractedText = match[1];
+ result += extractedText;
+ store.dispatch(setOnGoingResult(result));
+ }
+ } else {
+ //text summary/faq for data: "ops string"
+ const res = JSON.parse(msg.data); // Parse valid JSON
+ const logs = res.ops;
+ logs.forEach((log: { op: string; path: string; value: string }) => {
+ if (log.op === "add") {
+ if (
+ log.value !== "" &&
+ log.path.endsWith("/streamed_output/-") &&
+ log.path.length > "/streamed_output/-".length
+ ) {
+ result += log.value;
+ if (log.value) store.dispatch(setOnGoingResult(result));
+ }
+ }
+ });
+ }
} catch (e) {
console.log("something wrong in msg", e);
+ notify("Error in message response", NotificationSeverity.ERROR);
throw e;
}
}
@@ -283,28 +671,29 @@ export const doConversation = (conversationRequest: ConversationRequest) => {
onerror(err) {
console.log("error", err);
store.dispatch(setOnGoingResult(""));
- //notify here
+ notify("Error streaming response", NotificationSeverity.ERROR);
throw err;
- //handle error
},
onclose() {
- //handle close
const m: Message = {
role: MessageRole.Assistant,
content: result,
time: getCurrentTimeStamp().toString(),
};
- store.dispatch(setOnGoingResult(""));
+ store.dispatch(setOnGoingResult(""));
+ store.dispatch(setAbortController(null));
store.dispatch(addMessageToMessages(m));
- store.dispatch(
- saveConversationtoDatabase({
- conversation: {
- id: conversationId,
- },
- }),
- );
+ if (type === "chat") {
+ store.dispatch(
+ saveConversationtoDatabase({
+ conversation: {
+ id: conversationId,
+ },
+ }),
+ );
+ }
},
});
} catch (err) {
@@ -312,15 +701,121 @@ export const doConversation = (conversationRequest: ConversationRequest) => {
}
};
+const formDataEventStream = async (url: string, formData: any) => {
+ const abortController = new AbortController();
+ store.dispatch(setAbortController(abortController));
+ const signal = abortController.signal;
+
+ let result = "";
+
+ try {
+ const response = await fetch(url, {
+ method: "POST",
+ body: formData,
+ signal,
+ });
+
+ if (!response.ok) {
+ throw new Error("Network response was not ok");
+ }
+
+ if (response && response.body) {
+ store.dispatch(setIsPending(false));
+
+ const reader = response.body.getReader();
+
+ // Read the stream in chunks
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) {
+ break;
+ }
+
+ // Process the chunk of data (e.g., convert to text)
+ const textChunk = new TextDecoder().decode(value).trim();
+
+ // sometimes double lines return
+ const lines = textChunk.split("\n");
+
+ for (let line of lines) {
+ if (line.startsWith("data:")) {
+ const jsonStr = line.replace(/^data:\s*/, ""); // Remove "data: "
+
+ if (jsonStr !== "[DONE]") {
+ try {
+ // API Response for final output regularly returns incomplete JSON,
+ // due to final response containing source summary content and exceeding
+ // token limit in the response. We don't use it anyway so don't parse it.
+ if (!jsonStr.includes('"path":"/streamed_output/-"')) {
+ const res = JSON.parse(jsonStr); // Parse valid JSON
+
+ const logs = res.ops;
+ logs.forEach((log: { op: string; path: string; value: string }) => {
+ if (log.op === "add") {
+ if (
+ log.value !== "" &&
+ log.path.endsWith("/streamed_output/-") &&
+ log.path.length > "/streamed_output/-".length
+ ) {
+ result += log.value;
+ if (log.value) store.dispatch(setOnGoingResult(result));
+ }
+ }
+ });
+ }
+ } catch (error) {
+ console.warn("Error parsing JSON:", error, "Raw Data:", jsonStr);
+ }
+ } else {
+ const m: Message = {
+ role: MessageRole.Assistant,
+ content: result,
+ time: getCurrentTimeStamp().toString(),
+ };
+
+ store.dispatch(setOnGoingResult(""));
+ store.dispatch(addMessageToMessages(m));
+ store.dispatch(setAbortController(null));
+ }
+ }
+ }
+ }
+ }
+ } catch (error: any) {
+ if (error.name === "AbortError") {
+ console.log("Fetch aborted successfully.");
+ } else {
+ console.error("Fetch error:", error);
+ }
+ }
+};
+
export const {
logout,
setOnGoingResult,
+ setIsPending,
newConversation,
+ updatePromptSettings,
addMessageToMessages,
setSelectedConversationId,
+ setSelectedConversationHistory,
setTemperature,
setToken,
+ setModel,
+ setModelName,
+ setModels,
+ setType,
+ setUploadInProgress,
+ setSourceLinks,
+ setSourceFiles,
+ setSourceType,
+ setUseCase,
+ setUseCases,
setSystemPrompt,
+ setAbortController,
+ abortStream,
+ setDataSourceUrlStatus,
+ uploadChat,
} = ConversationSlice.actions;
export const conversationSelector = (state: RootState) => state.conversationReducer;
export default ConversationSlice.reducer;
diff --git a/ProductivitySuite/ui/react/src/redux/Prompt/PromptSlice.ts b/ProductivitySuite/ui/react/src/redux/Prompt/PromptSlice.ts
index 19a479ab07..e53e3e5190 100644
--- a/ProductivitySuite/ui/react/src/redux/Prompt/PromptSlice.ts
+++ b/ProductivitySuite/ui/react/src/redux/Prompt/PromptSlice.ts
@@ -1,21 +1,22 @@
-// Copyright (C) 2024 Intel Corporation
+// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
-import { createAsyncThunkWrapper } from "../thunkUtil";
-import client from "../../common/client";
-import { RootState } from "../store";
-import { notifications } from "@mantine/notifications";
-import { PROMPT_MANAGER_CREATE, PROMPT_MANAGER_GET } from "../../config";
+import { createAsyncThunkWrapper } from "@redux/thunkUtil";
+import { RootState } from "@redux/store";
+import { PROMPT_MANAGER_CREATE, PROMPT_MANAGER_GET, PROMPT_MANAGER_DELETE } from "@root/config";
+import { NotificationSeverity, notify } from "@components/Notification/Notification";
+import axios from "axios";
type promptReducer = {
prompts: Prompt[];
};
-type Prompt = {
+export type Prompt = {
id: string;
prompt_text: string;
user: string;
+ type: string;
};
const initialState: promptReducer = {
@@ -31,14 +32,14 @@ export const PromptSlice = createSlice({
},
},
extraReducers(builder) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
builder.addCase(getPrompts.fulfilled, (state, action: PayloadAction) => {
state.prompts = action.payload;
});
builder.addCase(addPrompt.fulfilled, () => {
- notifications.show({
- message: "Prompt added SuccessFully",
- });
+ notify("Prompt added Successfully", NotificationSeverity.SUCCESS);
+ });
+ builder.addCase(deletePrompt.fulfilled, () => {
+ notify("Prompt deleted Successfully", NotificationSeverity.SUCCESS);
});
},
});
@@ -47,28 +48,49 @@ export const { clearPrompts } = PromptSlice.actions;
export const promptSelector = (state: RootState) => state.promptReducer;
export default PromptSlice.reducer;
-export const getPrompts = createAsyncThunkWrapper(
- "prompts/getPrompts",
- async ({ promptText }: { promptText: string | null }, { getState }) => {
+export const getPrompts = createAsyncThunkWrapper("prompts/getPrompts", async (_: void, { getState }) => {
+ // @ts-ignore
+ const state: RootState = getState();
+ const response = await axios.post(PROMPT_MANAGER_GET, {
+ user: state.userReducer.name,
+ });
+ return response.data;
+});
+
+export const addPrompt = createAsyncThunkWrapper(
+ "prompts/addPrompt",
+ async ({ promptText }: { promptText: string }, { dispatch, getState }) => {
// @ts-ignore
const state: RootState = getState();
- const response = await client.post(PROMPT_MANAGER_GET, {
- promptText: promptText,
+ const response = await axios.post(PROMPT_MANAGER_CREATE, {
+ prompt_text: promptText,
user: state.userReducer.name,
+ //TODO: Would be nice to support type to set prompts for each
+ // type: state.conversationReducer.type // TODO: this might be crashing chatqna endpoint?
});
+
+ dispatch(getPrompts());
+
return response.data;
},
);
-export const addPrompt = createAsyncThunkWrapper(
- "prompts/addPrompt",
- async ({ promptText }: { promptText: string }, { getState }) => {
+//TODO delete prompt doesn't actually work, but responds 200
+export const deletePrompt = createAsyncThunkWrapper(
+ "prompts/deletePrompt",
+ async ({ promptId, promptText }: { promptId: string; promptText: string }, { dispatch, getState }) => {
// @ts-ignore
const state: RootState = getState();
- const response = await client.post(PROMPT_MANAGER_CREATE, {
+ const user = state.userReducer.name;
+
+ const response = await axios.post(PROMPT_MANAGER_DELETE, {
+ user: user,
+ prompt_id: promptId,
prompt_text: promptText,
- user: state.userReducer.name,
});
+
+ dispatch(getPrompts());
+
return response.data;
},
);
diff --git a/ProductivitySuite/ui/react/src/redux/User/user.d.ts b/ProductivitySuite/ui/react/src/redux/User/user.d.ts
index ce5a32e8ce..25b2e6b45b 100644
--- a/ProductivitySuite/ui/react/src/redux/User/user.d.ts
+++ b/ProductivitySuite/ui/react/src/redux/User/user.d.ts
@@ -1,6 +1,8 @@
-// Copyright (C) 2024 Intel Corporation
+// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
export interface User {
name: string;
+ isAuthenticated: boolean;
+ role: "Admin" | "User";
}
diff --git a/ProductivitySuite/ui/react/src/redux/User/userSlice.ts b/ProductivitySuite/ui/react/src/redux/User/userSlice.ts
index ff8c09940b..8dd7d23bc0 100644
--- a/ProductivitySuite/ui/react/src/redux/User/userSlice.ts
+++ b/ProductivitySuite/ui/react/src/redux/User/userSlice.ts
@@ -1,20 +1,24 @@
-// Copyright (C) 2024 Intel Corporation
+// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
-import { RootState } from "../store";
+import { RootState } from "@redux/store";
import { User } from "./user";
const initialState: User = {
name: "",
+ isAuthenticated: false,
+ role: "User",
};
export const userSlice = createSlice({
- name: "user",
+ name: "init user",
initialState,
reducers: {
- setUser: (state, action: PayloadAction) => {
- state.name = action.payload;
+ setUser: (state, action: PayloadAction) => {
+ state.name = action.payload.name;
+ state.isAuthenticated = action.payload.isAuthenticated;
+ state.role = action.payload.role;
},
removeUser: (state) => {
state.name = "";
diff --git a/ProductivitySuite/ui/react/src/redux/store.ts b/ProductivitySuite/ui/react/src/redux/store.ts
index 4d33a85754..5de6ac7aca 100644
--- a/ProductivitySuite/ui/react/src/redux/store.ts
+++ b/ProductivitySuite/ui/react/src/redux/store.ts
@@ -1,10 +1,10 @@
-// Copyright (C) 2024 Intel Corporation
+// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
import { combineReducers, configureStore } from "@reduxjs/toolkit";
-import userReducer from "./User/userSlice";
-import conversationReducer from "./Conversation/ConversationSlice";
-import promptReducer from "./Prompt/PromptSlice";
+import userReducer from "@redux/User/userSlice";
+import conversationReducer from "@redux/Conversation/ConversationSlice";
+import promptReducer from "@redux/Prompt/PromptSlice";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
export const store = configureStore({
diff --git a/ProductivitySuite/ui/react/src/redux/thunkUtil.ts b/ProductivitySuite/ui/react/src/redux/thunkUtil.ts
index 5df362fd3d..8db3b30330 100644
--- a/ProductivitySuite/ui/react/src/redux/thunkUtil.ts
+++ b/ProductivitySuite/ui/react/src/redux/thunkUtil.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Intel Corporation
+// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
import { createAsyncThunk, AsyncThunkPayloadCreator, AsyncThunk } from "@reduxjs/toolkit";
diff --git a/ProductivitySuite/ui/react/src/shared/ActionButtons.tsx b/ProductivitySuite/ui/react/src/shared/ActionButtons.tsx
new file mode 100644
index 0000000000..55abeeda92
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/shared/ActionButtons.tsx
@@ -0,0 +1,94 @@
+import { Button, styled } from "@mui/material";
+
+const TextOnlyStyle = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.actionButtons.text,
+}));
+
+const DeleteStyle = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.actionButtons.delete,
+}));
+
+const SolidStyle = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.actionButtons.solid,
+}));
+
+const OutlineStyle = styled(Button)(({ theme }) => ({
+ ...theme.customStyles.actionButtons.outline,
+}));
+
+type ButtonProps = {
+ onClick: (value: boolean) => void;
+ children: React.ReactNode | React.ReactNode[];
+ disabled?: boolean;
+ className?: string;
+};
+
+const TextButton: React.FC = ({
+ onClick,
+ children,
+ disabled = false,
+ className,
+}) => {
+ return (
+ onClick(true)}
+ className={className}
+ >
+ {children}
+
+ );
+};
+
+const DeleteButton: React.FC = ({
+ onClick,
+ children,
+ disabled = false,
+ className,
+}) => {
+ return (
+ onClick(true)}
+ className={className}
+ >
+ {children}
+
+ );
+};
+
+const SolidButton: React.FC = ({
+ onClick,
+ children,
+ disabled = false,
+ className,
+}) => {
+ return (
+ onClick(true)}
+ className={className}
+ >
+ {children}
+
+ );
+};
+
+const OutlineButton: React.FC = ({
+ onClick,
+ children,
+ disabled = false,
+ className,
+}) => {
+ return (
+ onClick(true)}
+ className={className}
+ >
+ {children}
+
+ );
+};
+
+export { TextButton, DeleteButton, SolidButton, OutlineButton };
diff --git a/ProductivitySuite/ui/react/src/shared/ModalBox/Modal.module.scss b/ProductivitySuite/ui/react/src/shared/ModalBox/Modal.module.scss
new file mode 100644
index 0000000000..ae9b7d00da
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/shared/ModalBox/Modal.module.scss
@@ -0,0 +1,50 @@
+.modal {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ max-width: 400px;
+ width: 100%;
+ padding: 0;
+ min-width: 300px;
+ z-index: 9999;
+
+ :global {
+ #modal-modal-title {
+ padding: 0.75rem 1rem;
+ font-weight: 600;
+ font-size: 0.8rem;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ #modal-modal-description {
+ padding: 1.5rem 1rem 1rem;
+ margin-top: -1rem;
+
+ .MuiFormControlLabel-label,
+ .MuiTypography-root {
+ font-weight: 300;
+ font-size: 0.8rem;
+ margin-top: 0.5rem;
+ }
+
+ .MuiBox-root {
+ align-items: flex-start;
+ }
+
+ .MuiButton-root {
+ padding: 5px 10px;
+
+ + .MuiButton-root {
+ margin-left: 0.5rem;
+ }
+ }
+ }
+ button {
+ padding: 0;
+ }
+ }
+}
diff --git a/ProductivitySuite/ui/react/src/shared/ModalBox/ModalBox.tsx b/ProductivitySuite/ui/react/src/shared/ModalBox/ModalBox.tsx
new file mode 100644
index 0000000000..0f3c9b912a
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/shared/ModalBox/ModalBox.tsx
@@ -0,0 +1,29 @@
+import { Modal, styled } from "@mui/material";
+
+import styles from "./Modal.module.scss";
+
+const StyledModalBox = styled("div")(({ theme }) => ({
+ ...theme.customStyles.settingsModal,
+}));
+
+const ModalBox: React.FC<{
+ children: React.ReactNode;
+ open?: boolean;
+ onClose?: () => void;
+}> = ({ children, open = true, onClose }) => {
+ let props: any = {};
+ if (onClose) props.onClose = onClose;
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default ModalBox;
diff --git a/ProductivitySuite/ui/react/src/styles/components/_sidebar.scss b/ProductivitySuite/ui/react/src/styles/components/_sidebar.scss
deleted file mode 100644
index 23018ee1f0..0000000000
--- a/ProductivitySuite/ui/react/src/styles/components/_sidebar.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-@import "../layout/flex";
-
-@mixin sidebar {
- @include flex(column, nowrap, flex-start, flex-start);
-}
diff --git a/ProductivitySuite/ui/react/src/styles/components/content.scss b/ProductivitySuite/ui/react/src/styles/components/content.scss
deleted file mode 100644
index 9a230f2493..0000000000
--- a/ProductivitySuite/ui/react/src/styles/components/content.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-@mixin textWrapEllipsis {
- text-overflow: ellipsis;
- white-space: nowrap;
- overflow: hidden;
-}
diff --git a/ProductivitySuite/ui/react/src/styles/components/context.module.scss b/ProductivitySuite/ui/react/src/styles/components/context.module.scss
deleted file mode 100644
index e2d3caafaa..0000000000
--- a/ProductivitySuite/ui/react/src/styles/components/context.module.scss
+++ /dev/null
@@ -1,88 +0,0 @@
-@import "../layout/flex";
-@import "../components/content.scss";
-
-.contextWrapper {
- background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
- border-right: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
- width: 13vw;
- overflow-y: hidden;
- overflow-x: hidden;
- display: grid;
- height: 100vh;
- grid-template-areas:
- "title"
- "history"
- "settings";
-
- grid-template-columns: auto;
- grid-template-rows: 70px 1fr 175px;
-
- .contextTitle {
- grid-area: title;
- top: 0;
- font-family:
- Greycliff CF,
- var(--mantine-font-family);
- margin-bottom: var(--mantine-spacing-sm);
- background-color: var(--mantine-color-body);
- padding: var(--mantine-spacing-md);
- padding-top: 18px;
- width: 100%;
- height: 60px;
- border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7));
- }
-
- .contextList {
- grid-area: history;
- // overflow-x: none;
- // overflow-y: auto;
-
- .contextListItem {
- display: flex;
- flex: 1 1 auto;
- justify-content: center;
- align-items: center;
- text-decoration: none;
- border-top-right-radius: var(--mantine-radius-md);
- border-bottom-right-radius: var(--mantine-radius-md);
- color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
- padding: 0 var(--mantine-spacing-md);
- font-size: var(--mantine-font-size-sm);
- margin-right: var(--mantine-spacing-md);
- font-weight: 500;
- height: 44px;
- width: 100%;
- line-height: 44px;
- cursor: pointer;
-
- .contextItemName {
- flex: 1 1 auto;
- width: 110px;
- @include textWrapEllipsis;
- }
-
- &:hover {
- background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
- color: light-dark(var(--mantine-color-dark), var(--mantine-color-light));
- }
-
- &[data-active] {
- &,
- &:hover {
- border-left-color: var(--mantine-color-blue-filled);
- background-color: var(--mantine-color-blue-filled);
- color: var(--mantine-color-white);
- }
- }
- }
- }
-
- .settings {
- grid-area: settings;
- border: black;
- padding: var(--mantine-spacing-sm);
- > * {
- padding: 5px 0;
- }
- }
-}
diff --git a/ProductivitySuite/ui/react/src/styles/layout/_basics.scss b/ProductivitySuite/ui/react/src/styles/layout/_basics.scss
deleted file mode 100644
index d11b1ef215..0000000000
--- a/ProductivitySuite/ui/react/src/styles/layout/_basics.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-@mixin absolutes {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
-}
diff --git a/ProductivitySuite/ui/react/src/styles/layout/_flex.scss b/ProductivitySuite/ui/react/src/styles/layout/_flex.scss
deleted file mode 100644
index 18d2ce8ecb..0000000000
--- a/ProductivitySuite/ui/react/src/styles/layout/_flex.scss
+++ /dev/null
@@ -1,6 +0,0 @@
-@mixin flex($direction: row, $wrap: nowrap, $alignItems: center, $justifyContent: center) {
- display: flex;
- flex-flow: $direction $wrap;
- align-items: $alignItems;
- justify-content: $justifyContent;
-}
diff --git a/ProductivitySuite/ui/react/src/styles/styles.scss b/ProductivitySuite/ui/react/src/styles/styles.scss
deleted file mode 100644
index 8028d8ad6d..0000000000
--- a/ProductivitySuite/ui/react/src/styles/styles.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-@import "layout/flex";
-@import "layout/basics";
diff --git a/ProductivitySuite/ui/react/src/theme/theme.tsx b/ProductivitySuite/ui/react/src/theme/theme.tsx
new file mode 100644
index 0000000000..d65e9609a5
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/theme/theme.tsx
@@ -0,0 +1,456 @@
+import { createTheme } from "@mui/material/styles";
+import moonIcon from "@assets/icons/moon.svg";
+import sunIcon from "@assets/icons/sun.svg";
+
+const lightBg = "#F2F3FF";
+
+const lightGrey = "#1f2133";
+
+const lightPurple = "#e3e5fd";
+const deepPurple = "#3D447F";
+const darkPurple = "#222647";
+const brightPurple = "#6b77db";
+const white60 = "#ffffff60";
+
+export const themeCreator = (mode: "light" | "dark") => {
+ return createTheme({
+ palette: {
+ mode: mode, // Default mode
+ primary: {
+ main: mode === "dark" ? "#ffffff" : "#ffffff",
+ contrastText: "#000000",
+ },
+ secondary: {
+ main: deepPurple,
+ contrastText: "#ffffff",
+ },
+ background: {
+ default: mode === "dark" ? "#090B1C" : lightBg,
+ paper: mode === "dark" ? "#161b22" : "#ffffff",
+ },
+ text: {
+ primary: mode === "dark" ? "#c9d1d9" : "#000000",
+ secondary: mode === "dark" ? "#ffffff" : deepPurple,
+ },
+ },
+ typography: {
+ fontFamily: "Roboto, Arial, sans-serif",
+ h1: {
+ fontWeight: 700,
+ fontSize: "2rem",
+ lineHeight: 1.5,
+ color: mode === "dark" ? "#ffffff" : deepPurple,
+ },
+ h2: {
+ fontWeight: 500,
+ fontSize: "1rem",
+ lineHeight: 1.4,
+ color: mode === "dark" ? "#ffffff" : deepPurple,
+ },
+ body1: {
+ fontSize: "1rem",
+ fontWeight: 300,
+ lineHeight: 1.5,
+ color: mode === "dark" ? "#ffffff" : deepPurple,
+ },
+ button: {
+ textTransform: "none",
+ fontWeight: 600,
+ },
+ },
+ components: {
+ MuiIconButton: {
+ styleOverrides: {
+ root: ({ theme }) => ({
+ svg: {
+ fill: theme.customStyles.icon?.main,
+ },
+ }),
+ },
+ },
+
+ MuiCheckbox: {
+ styleOverrides: {
+ root: ({ theme }) => ({
+ color: theme.customStyles.icon?.main,
+ "&.Mui-checked": {
+ color: theme.customStyles.icon?.main,
+ },
+ }),
+ },
+ },
+ MuiTooltip: {
+ styleOverrides: {
+ tooltip: {
+ backgroundColor: mode === "dark" ? lightGrey : darkPurple,
+ },
+ arrow: {
+ color: mode === "dark" ? lightGrey : darkPurple,
+ },
+ },
+ },
+ },
+ customStyles: {
+ header: {
+ backgroundColor: mode === "dark" ? "#090B1C" : lightBg,
+ boxShadow: mode === "dark" ? "none" : "0px 1px 24.1px 0px #4953D526",
+ borderBottom: mode === "dark" ? `1px solid ${deepPurple}7A` : "none",
+ },
+ aside: {
+ main: mode === "dark" ? lightGrey : "#E5E7FE",
+ },
+ customDivider: {
+ main: mode === "dark" ? white60 : deepPurple,
+ },
+ user: {
+ main: mode === "dark" ? "#161b22" : "#E3E5FD",
+ },
+ icon: {
+ main: mode === "dark" ? "#E5E7FE" : deepPurple,
+ },
+ input: {
+ main: mode === "dark" ? "#ffffff" : "#ffffff", // background color
+ primary: mode === "dark" ? "#c9d1d9" : "#000000",
+ secondary: mode === "dark" ? "#ffffff" : "#6b7280",
+ },
+ code: {
+ // title: mode === 'dark' ? '#2b2b2b' : '#2b2b2b',
+ primary: mode === "dark" ? "#5B5D74" : "#B6B9D4",
+ // text: mode === 'dark' ? '#ffffff' : '#ffffff',
+ // secondary: mode === 'dark' ? '#141415' : '#141415',
+ },
+ gradientShadow: {
+ border: `1px solid ${mode === "dark" ? "#ffffff20" : deepPurple + "10"}`,
+ boxShadow:
+ mode === "dark"
+ ? "0px 0px 10px rgba(0, 0, 0, 0.7)"
+ : "0px 0px 10px rgba(0, 0, 0, 0.1)",
+ },
+ gradientBlock: {
+ background:
+ mode === "dark"
+ ? `linear-gradient(180deg, ${lightGrey} 0%, rgba(61, 68, 127, 0.15)100%)`
+ : "linear-gradient(180deg, rgba(230, 232, 253, 0.50) 0%, rgba(61, 68, 127, 0.15) 100%)",
+ "&:hover": {
+ background:
+ mode === "dark"
+ ? `linear-gradient(180deg, rgba(61, 68, 127, 0.15) 0%, ${lightGrey} 100%)`
+ : "linear-gradient(180deg, rgba(61, 68, 127, 0.15) 0%, rgba(230, 232, 253, 0.50) 100%)",
+ },
+
+ ".MuiChip-root": {
+ backgroundColor: "#fff",
+ },
+ },
+ sources: {
+ iconWrap: {
+ background: "linear-gradient(90deg, #C398FA -56.85%, #7E6DBB 21.46%)",
+ svg: {
+ fill: "#ffffff !important",
+ color: "#ffffff",
+ },
+ },
+ sourceWrap: {
+ background: mode === "dark" ? "#1a1b27" : "#ffffff70",
+ border: `1px solid ${mode === "dark" ? "rgba(230, 232, 253, 0.30)" : lightPurple}`,
+ color: mode === "dark" ? "#fff" : deepPurple,
+ },
+ sourceChip: {
+ background: mode === "dark" ? "#1a1b27" : "#ffffff",
+ border: `1px solid ${mode === "dark" ? "#c398fa" : "rgba(73, 83, 213, 0.40)"}`,
+ color: mode === "dark" ? "#fff" : "#444",
+ },
+ },
+ audioProgress: {
+ stroke: mode === "dark" ? "#c9d1d9" : "#6b7280",
+ },
+ audioEditButton: {
+ boxShadow: "none",
+ border: "none",
+ backgroundColor: "transparent",
+ color: mode === "dark" ? "#fff" : deepPurple,
+ "&:hover": {
+ backgroundColor: mode === "dark" ? deepPurple : deepPurple + "40",
+ },
+ },
+ homeTitle: {
+ background:
+ mode === "dark"
+ ? "#fff"
+ : `linear-gradient(271deg, #C398FA -56.85%, #7E6DBB 21.46%, ${deepPurple} 99.77%)`,
+ },
+ homeButtons: {
+ borderRadius: "25px",
+ border: `1px solid ${mode === "dark" ? white60 : deepPurple + "60"}`, // take purple down some it down some
+ backgroundColor: mode === "dark" ? "#161b22" : lightBg,
+ color: mode === "dark" ? "#fff" : deepPurple,
+
+ boxShadow:
+ mode === "dark"
+ ? "0px 4px 10px rgba(0, 0, 0, 0.7)"
+ : "0px 4px 10px rgba(0, 0, 0, 0.1)",
+ "&:hover": {
+ backgroundColor: mode === "dark" ? darkPurple : lightPurple,
+ },
+ fontWeight: 300,
+ '&[aria-selected="true"]': {
+ fontWeight: 600,
+ backgroundColor: mode === "dark" ? darkPurple : lightPurple,
+ },
+ },
+ promptExpandButton: {
+ borderRadius: "25px",
+ border: `1px solid ${mode === "dark" ? white60 : deepPurple + "60"}`, // take purple down some it down some
+ backgroundColor: mode === "dark" ? "#161b22" : lightBg,
+ color: mode === "dark" ? "#fff" : deepPurple,
+
+ boxShadow:
+ mode === "dark"
+ ? "0px 4px 10px rgba(0, 0, 0, 0.7)"
+ : "0px 4px 10px rgba(0, 0, 0, 0.1)",
+ "&:hover": {
+ backgroundColor: mode === "dark" ? deepPurple : lightPurple,
+ },
+ },
+ promptButton: {
+ backgroundColor: mode === "dark" ? lightGrey : lightBg,
+ color: `${mode === "dark" ? "#fff" : deepPurple} !important`,
+ "&:hover": {
+ backgroundColor: mode === "dark" ? darkPurple : lightPurple,
+ color: mode === "dark" ? "#ffffff" : deepPurple,
+ },
+ },
+ promptListWrapper: {
+ backgroundColor: mode === "dark" ? lightGrey : lightBg,
+ boxShadow:
+ mode === "dark"
+ ? "0px 4px 10px rgba(0, 0, 0, 0.7)"
+ : "0px 4px 10px rgba(0, 0, 0, 0.1)",
+ },
+ primaryInput: {
+ inputWrapper: {
+ backgroundColor: mode === "dark" ? lightGrey : lightPurple,
+ border: `1px solid ${mode === "dark" ? "#ffffff20" : deepPurple + "10"}`,
+ boxShadow:
+ mode === "dark"
+ ? "0px 0px 10px rgba(0, 0, 0, 0.3)"
+ : "0px 0px 10px rgba(0, 0, 0, 0.1)",
+ "&:hover, &.active, &:focus": {
+ border: `1px solid ${mode === "dark" ? "#ffffff20" : deepPurple + "60"}`,
+ },
+ },
+ textInput: {
+ color: mode === "dark" ? "#fff" : "#3D447F",
+ "&::placeholder": {
+ color: mode === "dark" ? "#ffffff90" : "#6b7280",
+ },
+ },
+ circleButton: {
+ backgroundColor: mode === "dark" ? "transparent" : deepPurple + "80",
+ border: `1px solid ${mode === "dark" ? white60 : "transparent"}`,
+ "svg path": {
+ fill: mode === "dark" ? "#c9d1d9" : "#D9D9D9",
+ },
+ "&.active": {
+ backgroundColor: mode === "dark" ? deepPurple : lightGrey,
+ "svg path": {
+ fill: mode === "dark" ? "#c9d1d9" : "#D9D9D9",
+ },
+ },
+ "&:hover": {
+ backgroundColor: mode === "dark" ? "#646999" : "#003E71",
+ "svg path": {
+ fill: mode === "dark" ? "#c9d1d9" : "#D9D9D9",
+ },
+ },
+ },
+ },
+ tokensInput: {
+ color: mode === "dark" ? "#fff" : deepPurple,
+ backgroundColor: "transparent",
+ border: `1px solid ${mode === "dark" ? white60 : deepPurple + "70"}`,
+ boxShadow: "none",
+
+ "&:hover": {
+ borderColor: deepPurple,
+ },
+
+ "&:focus": {
+ borderColor: deepPurple,
+ },
+
+ "&[aria-invalid]": {
+ borderColor: "#cc0000 !important",
+ color: "#cc0000",
+ },
+ },
+ webInput: {
+ backgroundColor: mode === "dark" ? lightGrey : lightPurple,
+ ".Mui-focused": {
+ color: mode === "dark" ? "#ffffff" : deepPurple,
+ ".MuiOutlinedInput-notchedOutline": {
+ border: `1px solid ${mode === "dark" ? white60 : `${deepPurple}22`}`,
+ },
+ },
+ },
+ fileInputWrapper: {
+ backgroundColor: `${deepPurple}10`,
+ border: `1px dashed ${mode === "dark" ? white60 : `${deepPurple}22`}`,
+ },
+ fileInput: {
+ wrapper: {
+ backgroundColor: `${deepPurple}10`,
+ border: `1px dashed ${mode === "dark" ? white60 : `${deepPurple}22`}`,
+ },
+ file: {
+ backgroundColor:
+ mode === "dark" ? "rgba(255,255,255,0.1)" : "rgba(255,255,255,0.7)",
+ },
+ },
+ actionButtons: {
+ text: {
+ boxShadow: "none",
+ background: "none",
+ fontWeight: "400",
+ color: mode === "dark" ? "#ffffff" : "#007ce1",
+ "&:disabled": {
+ opacity: 0.5,
+ color: mode === "dark" ? "#ffffff" : "#007ce1",
+ },
+ "&:hover": {
+ background: mode === "dark" ? "#007ce1" : "#ffffff",
+ color: mode === "dark" ? "#ffffff" : "#007ce1",
+ },
+ },
+ delete: {
+ boxShadow: "none",
+ background: "#f15346",
+ fontWeight: "400",
+ color: "#fff",
+ "&:hover": {
+ background: "#cc0000",
+ },
+ "&:disabled": {
+ opacity: 0.5,
+ color: "#fff",
+ },
+ },
+ solid: {
+ boxShadow: "none",
+ background: deepPurple,
+ fontWeight: "400",
+ color: "#fff",
+ "&:hover": {
+ background: deepPurple,
+ },
+ "&:disabled": {
+ opacity: 0.5,
+ color: "#fff",
+ },
+ },
+ outline: {
+ boxShadow: "none",
+ background: "transparent",
+ fontWeight: "400",
+ color: mode === "dark" ? "#ffffff" : "#007ce1",
+ border: `1px solid ${mode === "dark" ? "#ffffff" : "#007ce1"}`,
+ "&:hover": {
+ background: mode === "dark" ? "#007ce1" : "#ffffff",
+ color: mode === "dark" ? "#ffffff" : "#007ce1",
+ },
+ "&.active": {
+ background: mode === "dark" ? "#ffffff" : "#007ce1",
+ color: mode === "dark" ? "#007ce1" : "#ffffff",
+ },
+ },
+ },
+ themeToggle: {
+ ".MuiSwitch-switchBase.Mui-checked": {
+ ".MuiSwitch-thumb:before": {
+ backgroundImage: `url(${moonIcon})`,
+ },
+ },
+ "& .MuiSwitch-thumb": {
+ backgroundColor: mode === "dark" ? "#fff" : "transparent",
+ border: `1px solid ${mode === "dark" ? "#090B1C" : deepPurple}`,
+ "svg path": {
+ fill: mode === "dark" ? "#E5E7FE" : deepPurple,
+ },
+ "&::before": {
+ backgroundImage: `url(${sunIcon})`,
+ },
+ },
+ "& .MuiSwitch-track": {
+ border: `1px solid ${mode === "dark" ? "#fff" : deepPurple}`,
+ backgroundColor: mode === "dark" ? "#8796A5" : "transparent",
+ },
+ },
+ dropDown: {
+ "&:hover, &:focus": {
+ backgroundColor:
+ mode === "dark" ? "rgba(0,0,0, 0.5)" : "rgba(230, 232, 253, 0.50)",
+ },
+ "&.Mui-selected": {
+ backgroundColor:
+ mode === "dark" ? "rgba(0,0,0, 1)" : "rgba(230, 232, 253, 0.75)",
+ },
+ "&.Mui-selected:hover, &.Mui-selected:focus": {
+ backgroundColor:
+ mode === "dark" ? "rgba(0,0,0, 1)" : "rgba(230, 232, 253, 0.75)",
+ },
+ wrapper: {
+ border: `1px solid ${mode === "dark" ? white60 : deepPurple + "70"}`,
+ },
+ },
+ settingsModal: {
+ boxShadow: " 0px 0px 20px rgba(0,0,0,0.5)",
+ border: "1px solid #000",
+ background: mode === "dark" ? lightGrey : lightBg,
+ "#modal-modal-title": {
+ backgroundColor: "#e5e7fe",
+ color: deepPurple,
+
+ svg: {
+ fill: deepPurple,
+ },
+ },
+ },
+ styledSlider: {
+ color: mode === "dark" ? brightPurple : deepPurple,
+
+ "&.disabled": {
+ color: mode === "dark" ? brightPurple : deepPurple,
+ },
+
+ ".MuiSlider-rail": {
+ backgroundColor: mode === "dark" ? brightPurple : deepPurple,
+ },
+
+ ".MuiSlider-track": {
+ backgroundColor: mode === "dark" ? brightPurple : deepPurple,
+ },
+
+ ".MuiSlider-thumb": {
+ backgroundColor: mode === "dark" ? brightPurple : deepPurple,
+
+ "&:hover": {
+ boxShadow: `0 0 0 6px rgba(61,68,127,0.3)`,
+ },
+
+ "&.focusVisible": {
+ boxShadow: `0 0 0 8px rgba(61,68,127,0.5)`,
+ },
+
+ "&.active": {
+ boxShadow: `0 0 0 8px rgba(61,68,127,0.5)`,
+ },
+
+ "&.disabled": {
+ backgroundColor: mode === "dark" ? brightPurple : deepPurple,
+ },
+ },
+ },
+ },
+ });
+};
+deepPurple;
diff --git a/ProductivitySuite/ui/react/src/types/common.ts b/ProductivitySuite/ui/react/src/types/common.ts
new file mode 100644
index 0000000000..eb65a08e53
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/types/common.ts
@@ -0,0 +1,13 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+export interface ErrorResponse {
+ response?: {
+ data?: {
+ error?: {
+ message?: string;
+ };
+ };
+ };
+ message: string;
+}
diff --git a/ProductivitySuite/ui/react/src/types/conversation.ts b/ProductivitySuite/ui/react/src/types/conversation.ts
new file mode 100644
index 0000000000..439d998799
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/types/conversation.ts
@@ -0,0 +1,57 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+// export interface Model {
+// model_type: string;
+// token_limit: number;
+// temperature: number;
+// display_name: string;
+// version: number;
+// vendor: string;
+// platform: string;
+// min_temperature: number;
+// max_temperature: number;
+// min_token_limit: number;
+// max_token_limit: number;
+// data_insights_input_token: number;
+// data_insights_output_token: number;
+// }
+
+export interface InferenceSettings {
+ model: string;
+ temperature: number;
+ token_limit: number;
+ input_token?: number;
+ output_token?: number;
+ tags?: null;
+ maxTokenLimit?: number;
+ minTokenLimit?: number;
+ maxTemperatureLimit?: number;
+ minTemperatureLimit?: number;
+}
+
+export interface Feedback {
+ comment: string;
+ rating: number;
+ is_thumbs_up: boolean;
+}
+
+export interface SuccessResponse {
+ message: string;
+}
+
+export interface PromptsResponse {
+ prompt_text: string;
+ tags: [];
+ tag_category: string;
+ author: string;
+}
+
+export interface StreamChatProps {
+ user_id: string;
+ conversation_id: string;
+ use_case: string;
+ query: string;
+ tags: string[];
+ settings: InferenceSettings;
+}
diff --git a/ProductivitySuite/ui/react/src/types/global.d.ts b/ProductivitySuite/ui/react/src/types/global.d.ts
new file mode 100644
index 0000000000..221d7c0154
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/types/global.d.ts
@@ -0,0 +1,7 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+declare module "*.svg" {
+ const content: string;
+ export default content;
+}
diff --git a/ProductivitySuite/ui/react/src/types/speech.d.ts b/ProductivitySuite/ui/react/src/types/speech.d.ts
new file mode 100644
index 0000000000..1d5eb60434
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/types/speech.d.ts
@@ -0,0 +1,27 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+type SpeechRecognitionErrorEvent = Event & {
+ error:
+ | "no-speech"
+ | "audio-capture"
+ | "not-allowed"
+ | "network"
+ | "aborted"
+ | "service-not-allowed"
+ | "bad-grammar"
+ | "language-not-supported";
+ message?: string; // Some browsers may provide an additional error message
+};
+
+type SpeechRecognitionEvent = Event & {
+ results: {
+ [index: number]: {
+ [index: number]: {
+ transcript: string;
+ confidence: number;
+ };
+ isFinal: boolean;
+ };
+ };
+};
diff --git a/ProductivitySuite/ui/react/src/types/styles.d.ts b/ProductivitySuite/ui/react/src/types/styles.d.ts
new file mode 100644
index 0000000000..7d3279f480
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/types/styles.d.ts
@@ -0,0 +1,7 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+declare module "*.module.scss" {
+ const classes: { [key: string]: string };
+ export default classes;
+}
diff --git a/ProductivitySuite/ui/react/src/types/theme.d.ts b/ProductivitySuite/ui/react/src/types/theme.d.ts
new file mode 100644
index 0000000000..a46a8afb85
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/types/theme.d.ts
@@ -0,0 +1,47 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import "@mui/material/styles";
+import { PaletteChip, PaletteColor } from "@mui/material/styles";
+
+declare module "@mui/material/styles" {
+ interface Theme {
+ customStyles: Record>;
+ }
+
+ interface ThemeOptions {
+ customStyles?: Record>;
+ }
+
+ interface Palette {
+ header?: PaletteColor;
+ aside?: PaletteColor;
+ customDivider?: PaletteColor;
+ input?: PaletteColor;
+ icon?: PaletteColor;
+ user?: PaletteColor;
+ code?: PaletteColor;
+ gradientBlock?: PaletteColor;
+ audioProgress?: PaletteColor;
+ primaryInput?: PaletteColor;
+ actionButtons?: PaletteColor;
+ themeToggle?: PaletteColor;
+ dropDown?: PaletteColor;
+ }
+
+ interface PaletteOptions {
+ header?: PaletteColorOptions;
+ aside?: PaletteColorOptions;
+ customDivider?: PaletteColorOptions;
+ input?: PaletteColorOptions;
+ icon?: PaletteColorOptions;
+ user?: PaletteColorOptions;
+ code?: PaletteColorOptions;
+ gradientBlock?: PaletteColorOptions;
+ audioProgress?: PaletteColorOptions;
+ primaryInput?: PaletteColorOptions;
+ actionButtons?: PaletteColorOptions;
+ themeToggle?: PaletteColorOptions;
+ dropDown?: PaletteColorOptions;
+ }
+}
diff --git a/ProductivitySuite/ui/react/src/utils/utils.js b/ProductivitySuite/ui/react/src/utils/utils.js
new file mode 100644
index 0000000000..59f40b5acf
--- /dev/null
+++ b/ProductivitySuite/ui/react/src/utils/utils.js
@@ -0,0 +1,96 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import React from "react";
+
+export const smartTrim = (string, maxLength) => {
+ if (!string) {
+ return string;
+ }
+ if (maxLength < 1) {
+ return string;
+ }
+ if (string.length <= maxLength) {
+ return string;
+ }
+ if (maxLength === 1) {
+ return string.substring(0, 1) + "...";
+ }
+ var midpoint = Math.ceil(string.length / 2);
+ var toremove = string.length - maxLength;
+ var lstrip = Math.ceil(toremove / 2);
+ var rstrip = toremove - lstrip;
+ return string.substring(0, midpoint - lstrip) + "..." + string.substring(midpoint + rstrip);
+};
+
+export const QueryStringFromArr = (paramsArr = []) => {
+ const queryString = [];
+
+ for (const param of paramsArr) {
+ queryString.push(`${param.name}=${param.value}`);
+ }
+
+ return queryString.join("&");
+};
+
+export const isAuthorized = (
+ allowedRoles = [],
+ userRole,
+ isPreviewOnlyFeature = false,
+ isPreviewUser = false,
+ isNotAllowed = false,
+) => {
+ return (
+ (allowedRoles.length === 0 || allowedRoles.includes(userRole)) &&
+ (!isPreviewOnlyFeature || isPreviewUser) &&
+ !isNotAllowed
+ );
+};
+
+function addPropsToReactElement(element, props, i) {
+ if (React.isValidElement(element)) {
+ return React.cloneElement(element, { key: i, ...props });
+ }
+ return element;
+}
+
+export function addPropsToChildren(children, props) {
+ if (!Array.isArray(children)) {
+ return addPropsToReactElement(children, props);
+ }
+ return children.map((childElement, i) => addPropsToReactElement(childElement, props, i));
+}
+
+export const getCurrentTimeStamp = () => {
+ return Math.floor(Date.now() / 1000);
+};
+
+export const uuidv4 = () => {
+ return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) =>
+ (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16),
+ );
+};
+
+export const readFilesAndSummarize = async (sourceFiles) => {
+ let summaryMessage = "";
+
+ if (sourceFiles.length) {
+ const readFilePromises = sourceFiles.map((fileWrapper) => {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ const text = reader.result?.toString() || "";
+ resolve(text);
+ };
+ reader.onerror = () => reject(new Error("Error reading file"));
+ reader.readAsText(fileWrapper.file);
+ });
+ });
+
+ const fileContents = await Promise.all(readFilePromises);
+
+ summaryMessage = fileContents.join("\n");
+ }
+
+ return summaryMessage;
+};
diff --git a/ProductivitySuite/ui/react/src/vite-env.d.ts b/ProductivitySuite/ui/react/src/vite-env.d.ts
index 4260915f75..0128e66e57 100644
--- a/ProductivitySuite/ui/react/src/vite-env.d.ts
+++ b/ProductivitySuite/ui/react/src/vite-env.d.ts
@@ -1,4 +1,5 @@
-// Copyright (C) 2024 Intel Corporation
+// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
///
+///
diff --git a/ProductivitySuite/ui/react/tsconfig.json b/ProductivitySuite/ui/react/tsconfig.json
index f50b75c5f0..d7149ff020 100644
--- a/ProductivitySuite/ui/react/tsconfig.json
+++ b/ProductivitySuite/ui/react/tsconfig.json
@@ -1,23 +1,34 @@
{
"compilerOptions": {
- "target": "ES2020",
- "useDefineForClassFields": true,
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
- "module": "ESNext",
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
"skipLibCheck": true,
-
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "esnext",
+ "moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
-
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true
+ "baseUrl": "src",
+ "paths": {
+ "@components/*": ["components/*"],
+ "@shared/*": ["shared/*"],
+ "@contexts/*": ["contexts/*"],
+ "@redux/*": ["redux/*"],
+ "@services/*": ["services/*"],
+ "@pages/*": ["pages/*"],
+ "@layouts/*": ["layouts/*"],
+ "@assets/*": ["assets/*"],
+ "@icons/*": ["icons/*"],
+ "@utils/*": ["utils/*"],
+ "@root/*": ["*"]
+ }
},
- "include": ["src"],
- "references": [{ "path": "./tsconfig.node.json" }]
+ "include": ["src", "src/theme/theme.tsx", "src/**/*.d.ts"]
}
diff --git a/ProductivitySuite/ui/react/vite.config.js b/ProductivitySuite/ui/react/vite.config.js
new file mode 100644
index 0000000000..16ef492fdf
--- /dev/null
+++ b/ProductivitySuite/ui/react/vite.config.js
@@ -0,0 +1,117 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import react from "@vitejs/plugin-react";
+import path from "path";
+
+import { defineConfig } from "vite";
+import { visualizer } from "rollup-plugin-visualizer";
+import compression from "vite-plugin-compression";
+import terser from "@rollup/plugin-terser";
+import sassDts from "vite-plugin-sass-dts";
+import svgr from "vite-plugin-svgr";
+
+export default defineConfig({
+ base: "/",
+ optimizeDeps: {
+ include: ["**/*.scss"], // Include all .scss files
+ },
+ modulePreload: {
+ polyfill: true, // Ensures compatibility
+ },
+ css: {
+ modules: {
+ // Enable CSS Modules for all .scss files
+ localsConvention: "camelCaseOnly",
+ },
+ },
+ commonjsOptions: {
+ esmExternals: true,
+ },
+ server: {
+ // https: true,
+ host: "0.0.0.0",
+ port: 5173,
+ },
+ build: {
+ sourcemap: false,
+ rollupOptions: {
+ // output: {
+ // manualChunks(id) {
+ // if (id.includes('node_modules')) {
+
+ // if (id.match(/react-dom|react-router|react-redux/)) {
+ // return 'react-vendor';
+ // }
+
+ // // // Code render files
+ // // if (id.match(/react-syntax-highlighter|react-markdown|gfm|remark|refractor|micromark|highlight|mdast/)) {
+ // // return 'code-vendor';
+ // // }
+
+ // if (id.match(/emotion|mui|styled-components/)) {
+ // return 'style-vendor';
+ // }
+
+ // if (id.match(/keycloak-js|axios|notistack|reduxjs|fetch-event-source|azure/)) {
+ // return 'utils-vendor';
+ // }
+
+ // const packages = id.toString().split('node_modules/')[1].split('/')[0];
+ // return `vendor-${packages}`;
+ // }
+ // }
+ // },
+ plugins: [
+ terser({
+ format: { comments: false },
+ compress: {
+ drop_console: true,
+ drop_debugger: true,
+ },
+ }),
+ ],
+ },
+ chunkSizeWarningLimit: 500,
+ assetsInlineLimit: 0,
+ },
+ plugins: [
+ svgr(),
+ react(),
+ // sassDts({
+ // enabledMode: []//['production'], // Generate type declarations on build
+ // }),
+ compression({
+ algorithm: "gzip",
+ ext: ".gz",
+ deleteOriginFile: false,
+ threshold: 10240,
+ }),
+ visualizer({
+ filename: "./dist/stats.html", // Output stats file
+ open: true, // Automatically open in the browser
+ gzipSize: true, // Show gzipped sizes
+ brotliSize: true, // Show Brotli sizes
+ }),
+ ],
+ resolve: {
+ alias: {
+ "@mui/styled-engine": "@mui/styled-engine-sc",
+ "@components": path.resolve(__dirname, "src/components/"),
+ "@shared": path.resolve(__dirname, "src/shared/"),
+ "@contexts": path.resolve(__dirname, "src/contexts/"),
+ "@redux": path.resolve(__dirname, "src/redux/"),
+ "@services": path.resolve(__dirname, "src/services/"),
+ "@pages": path.resolve(__dirname, "src/pages/"),
+ "@layouts": path.resolve(__dirname, "src/layouts/"),
+ "@assets": path.resolve(__dirname, "src/assets/"),
+ "@utils": path.resolve(__dirname, "src/utils/"),
+ "@icons": path.resolve(__dirname, "src/icons/"),
+ "@root": path.resolve(__dirname, "src/"),
+ },
+ },
+ assetsInclude: ["**/*.svg"], // Ensure Vite processes .svg files
+ // define: {
+ // "import.meta.env": process.env,
+ // },
+});
diff --git a/ProductivitySuite/ui/react/vite.config.ts b/ProductivitySuite/ui/react/vite.config.ts
deleted file mode 100644
index bb171b48d3..0000000000
--- a/ProductivitySuite/ui/react/vite.config.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (C) 2024 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-import { defineConfig } from "vitest/config";
-import react from "@vitejs/plugin-react";
-
-// https://vitejs.dev/config/
-export default defineConfig({
- css: {
- preprocessorOptions: {
- scss: {
- additionalData: `@import "./src/styles/styles.scss";`,
- },
- },
- },
- plugins: [react()],
- server: {
- port: 5174,
- },
- test: {
- globals: true,
- environment: "jsdom",
- },
- define: {
- "import.meta.env": process.env,
- },
-});