upd
This commit is contained in:
parent
d1e5ec019d
commit
7e35793ca1
70
.gitignore
vendored
70
.gitignore
vendored
@ -1,36 +1,36 @@
|
||||
|
||||
|
||||
|
||||
|
||||
.idea/
|
||||
.vs/
|
||||
|
||||
|
||||
*.log
|
||||
*.tmp
|
||||
*.swp
|
||||
|
||||
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
|
||||
|
||||
*.venv/
|
||||
Backend/venv/
|
||||
Backend/__pycache__/
|
||||
|
||||
|
||||
|
||||
|
||||
*.dockerignore
|
||||
*.env
|
||||
docker-compose.override.yml
|
||||
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
|
||||
*.lock
|
||||
package-lock.json
|
||||
|
||||
|
||||
|
||||
|
||||
.idea/
|
||||
.vs/
|
||||
|
||||
|
||||
*.log
|
||||
*.tmp
|
||||
*.swp
|
||||
|
||||
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
|
||||
|
||||
*.venv/
|
||||
Backend/venv/
|
||||
Backend/__pycache__/
|
||||
|
||||
|
||||
|
||||
|
||||
*.dockerignore
|
||||
*.env
|
||||
docker-compose.override.yml
|
||||
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
|
||||
*.lock
|
||||
package-lock.json
|
||||
yarn.lock
|
244
Backend/arch.py
244
Backend/arch.py
@ -1,122 +1,122 @@
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches
|
||||
from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE, MSO_CONNECTOR
|
||||
|
||||
# Vytvorenie novej prezentácie
|
||||
prs = Presentation()
|
||||
slide_layout = prs.slide_layouts[5] # Prázdny slide
|
||||
slide = prs.slides.add_slide(slide_layout)
|
||||
|
||||
# Definícia základných rozmerov a pozícií
|
||||
left_margin = Inches(0.5)
|
||||
top_margin = Inches(0.5)
|
||||
block_width = Inches(3)
|
||||
block_height = Inches(0.7)
|
||||
vertical_gap = Inches(0.3)
|
||||
horizontal_gap = Inches(0.5)
|
||||
|
||||
# Blok 1: Používateľský dotaz & Chat history
|
||||
box1 = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, left_margin, top_margin, block_width, block_height)
|
||||
box1.text = "Používateľský dotaz\n& Chat history"
|
||||
|
||||
# Blok 2: ConversationalAgent (pod box1)
|
||||
box2_top = top_margin + block_height + vertical_gap
|
||||
box2 = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, left_margin, box2_top, block_width, block_height)
|
||||
box2.text = "ConversationalAgent"
|
||||
|
||||
# Blok 3: Klasifikácia dotazu (pod box2)
|
||||
box3_top = box2_top + block_height + vertical_gap
|
||||
box3 = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, left_margin, box3_top, block_width, block_height)
|
||||
box3.text = "Klasifikácia dotazu"
|
||||
|
||||
# Vetvenie: Pozície pre dve vetvy
|
||||
branch_top = box3_top + block_height + vertical_gap
|
||||
|
||||
# Ľavá vetva ("Vyhladavanie")
|
||||
left_branch_left = left_margin - Inches(0.2)
|
||||
box4A = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, left_branch_left, branch_top, block_width, block_height)
|
||||
box4A.text = "ElasticsearchStore\nvyhľadávanie"
|
||||
|
||||
box5A_top = branch_top + block_height + vertical_gap
|
||||
box5A = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, left_branch_left, box5A_top, block_width, block_height)
|
||||
box5A.text = "Generovanie\ndynamického promptu"
|
||||
|
||||
box6A_top = box5A_top + block_height + vertical_gap
|
||||
box6A = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, left_branch_left, box6A_top, block_width, block_height)
|
||||
box6A.text = "Generovanie\nodpovede"
|
||||
|
||||
box7A_top = box6A_top + block_height + vertical_gap
|
||||
box7A = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, left_branch_left, box7A_top, block_width, block_height)
|
||||
box7A.text = "Finalizácia\nodpovede"
|
||||
|
||||
# Pravá vetva ("Upresnenie")
|
||||
right_branch_left = left_margin + block_width + horizontal_gap
|
||||
box4B = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, right_branch_left, branch_top, block_width, block_height)
|
||||
box4B.text = "Kombinovanie\ndotazov"
|
||||
|
||||
box5B_top = branch_top + block_height + vertical_gap
|
||||
box5B = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, right_branch_left, box5B_top, block_width, block_height)
|
||||
box5B.text = "ElasticsearchStore\nvyhľadávanie"
|
||||
|
||||
box6B_top = box5B_top + block_height + vertical_gap
|
||||
box6B = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, right_branch_left, box6B_top, block_width, block_height)
|
||||
box6B.text = "Generovanie\ndynamického promptu"
|
||||
|
||||
box7B_top = box6B_top + block_height + vertical_gap
|
||||
box7B = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, right_branch_left, box7B_top, block_width, block_height)
|
||||
box7B.text = "Generovanie\nodpovedí (2 modely)"
|
||||
|
||||
box8B_top = box7B_top + block_height + vertical_gap
|
||||
box8B = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, right_branch_left, box8B_top, block_width, block_height)
|
||||
box8B.text = "Validácia a\nhodnotenie odpovedí"
|
||||
|
||||
box9B_top = box8B_top + block_height + vertical_gap
|
||||
box9B = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, right_branch_left, box9B_top, block_width, block_height)
|
||||
box9B.text = "Finalizácia\nodpovede"
|
||||
|
||||
# Finálny blok: Výstup (zlúčenie vetiev)
|
||||
final_box_top = max(box7A_top, box9B_top) + block_height + vertical_gap
|
||||
final_box = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, left_margin, final_box_top, block_width, block_height)
|
||||
final_box.text = "Výstup"
|
||||
|
||||
# Funkcia na pridanie šípok medzi blokmi
|
||||
def add_connector(slide, start_shape, end_shape):
|
||||
start_x = start_shape.left + start_shape.width / 2
|
||||
start_y = start_shape.top + start_shape.height
|
||||
end_x = end_shape.left + end_shape.width / 2
|
||||
end_y = end_shape.top
|
||||
connector = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, start_x, start_y, end_x, end_y)
|
||||
# Aktuálna verzia python-pptx nepodporuje nastavenie šípky, preto tento riadok odstraňte alebo zakomentujte:
|
||||
# connector.line.end_arrowhead.style = 1
|
||||
return connector
|
||||
|
||||
# Prepojenie blokov
|
||||
add_connector(slide, box1, box2)
|
||||
add_connector(slide, box2, box3)
|
||||
|
||||
# Vetvenie z Box3 do oboch vetiev
|
||||
mid_point = box3.left + box3.width / 2
|
||||
branch_mid_y = box3.top + box3.height + vertical_gap/2
|
||||
# Do ľavej vetvy:
|
||||
connector_left = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, mid_point, box3.top + box3.height, left_branch_left + block_width/2, branch_mid_y)
|
||||
# Do pravej vetvy:
|
||||
connector_right = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, mid_point, box3.top + box3.height, right_branch_left + block_width/2, branch_mid_y)
|
||||
|
||||
# Prepojenie blokov v ľavej vetve
|
||||
add_connector(slide, box4A, box5A)
|
||||
add_connector(slide, box5A, box6A)
|
||||
add_connector(slide, box6A, box7A)
|
||||
|
||||
# Prepojenie blokov v pravej vetve
|
||||
add_connector(slide, box4B, box5B)
|
||||
add_connector(slide, box5B, box6B)
|
||||
add_connector(slide, box6B, box7B)
|
||||
add_connector(slide, box7B, box8B)
|
||||
add_connector(slide, box8B, box9B)
|
||||
|
||||
# Spojenie oboch vetiev s finálnym blokom "Výstup"
|
||||
add_connector(slide, box7A, final_box)
|
||||
add_connector(slide, box9B, final_box)
|
||||
|
||||
# Uloženie prezentácie
|
||||
prs.save("architecture_diagram.pptx")
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches
|
||||
from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE, MSO_CONNECTOR
|
||||
|
||||
# Vytvorenie novej prezentácie
|
||||
prs = Presentation()
|
||||
slide_layout = prs.slide_layouts[5] # Prázdny slide
|
||||
slide = prs.slides.add_slide(slide_layout)
|
||||
|
||||
# Definícia základných rozmerov a pozícií
|
||||
left_margin = Inches(0.5)
|
||||
top_margin = Inches(0.5)
|
||||
block_width = Inches(3)
|
||||
block_height = Inches(0.7)
|
||||
vertical_gap = Inches(0.3)
|
||||
horizontal_gap = Inches(0.5)
|
||||
|
||||
# Blok 1: Používateľský dotaz & Chat history
|
||||
box1 = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, left_margin, top_margin, block_width, block_height)
|
||||
box1.text = "Používateľský dotaz\n& Chat history"
|
||||
|
||||
# Blok 2: ConversationalAgent (pod box1)
|
||||
box2_top = top_margin + block_height + vertical_gap
|
||||
box2 = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, left_margin, box2_top, block_width, block_height)
|
||||
box2.text = "ConversationalAgent"
|
||||
|
||||
# Blok 3: Klasifikácia dotazu (pod box2)
|
||||
box3_top = box2_top + block_height + vertical_gap
|
||||
box3 = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, left_margin, box3_top, block_width, block_height)
|
||||
box3.text = "Klasifikácia dotazu"
|
||||
|
||||
# Vetvenie: Pozície pre dve vetvy
|
||||
branch_top = box3_top + block_height + vertical_gap
|
||||
|
||||
# Ľavá vetva ("Vyhladavanie")
|
||||
left_branch_left = left_margin - Inches(0.2)
|
||||
box4A = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, left_branch_left, branch_top, block_width, block_height)
|
||||
box4A.text = "ElasticsearchStore\nvyhľadávanie"
|
||||
|
||||
box5A_top = branch_top + block_height + vertical_gap
|
||||
box5A = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, left_branch_left, box5A_top, block_width, block_height)
|
||||
box5A.text = "Generovanie\ndynamického promptu"
|
||||
|
||||
box6A_top = box5A_top + block_height + vertical_gap
|
||||
box6A = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, left_branch_left, box6A_top, block_width, block_height)
|
||||
box6A.text = "Generovanie\nodpovede"
|
||||
|
||||
box7A_top = box6A_top + block_height + vertical_gap
|
||||
box7A = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, left_branch_left, box7A_top, block_width, block_height)
|
||||
box7A.text = "Finalizácia\nodpovede"
|
||||
|
||||
# Pravá vetva ("Upresnenie")
|
||||
right_branch_left = left_margin + block_width + horizontal_gap
|
||||
box4B = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, right_branch_left, branch_top, block_width, block_height)
|
||||
box4B.text = "Kombinovanie\ndotazov"
|
||||
|
||||
box5B_top = branch_top + block_height + vertical_gap
|
||||
box5B = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, right_branch_left, box5B_top, block_width, block_height)
|
||||
box5B.text = "ElasticsearchStore\nvyhľadávanie"
|
||||
|
||||
box6B_top = box5B_top + block_height + vertical_gap
|
||||
box6B = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, right_branch_left, box6B_top, block_width, block_height)
|
||||
box6B.text = "Generovanie\ndynamického promptu"
|
||||
|
||||
box7B_top = box6B_top + block_height + vertical_gap
|
||||
box7B = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, right_branch_left, box7B_top, block_width, block_height)
|
||||
box7B.text = "Generovanie\nodpovedí (2 modely)"
|
||||
|
||||
box8B_top = box7B_top + block_height + vertical_gap
|
||||
box8B = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, right_branch_left, box8B_top, block_width, block_height)
|
||||
box8B.text = "Validácia a\nhodnotenie odpovedí"
|
||||
|
||||
box9B_top = box8B_top + block_height + vertical_gap
|
||||
box9B = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, right_branch_left, box9B_top, block_width, block_height)
|
||||
box9B.text = "Finalizácia\nodpovede"
|
||||
|
||||
# Finálny blok: Výstup (zlúčenie vetiev)
|
||||
final_box_top = max(box7A_top, box9B_top) + block_height + vertical_gap
|
||||
final_box = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, left_margin, final_box_top, block_width, block_height)
|
||||
final_box.text = "Výstup"
|
||||
|
||||
# Funkcia na pridanie šípok medzi blokmi
|
||||
def add_connector(slide, start_shape, end_shape):
|
||||
start_x = start_shape.left + start_shape.width / 2
|
||||
start_y = start_shape.top + start_shape.height
|
||||
end_x = end_shape.left + end_shape.width / 2
|
||||
end_y = end_shape.top
|
||||
connector = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, start_x, start_y, end_x, end_y)
|
||||
# Aktuálna verzia python-pptx nepodporuje nastavenie šípky, preto tento riadok odstraňte alebo zakomentujte:
|
||||
# connector.line.end_arrowhead.style = 1
|
||||
return connector
|
||||
|
||||
# Prepojenie blokov
|
||||
add_connector(slide, box1, box2)
|
||||
add_connector(slide, box2, box3)
|
||||
|
||||
# Vetvenie z Box3 do oboch vetiev
|
||||
mid_point = box3.left + box3.width / 2
|
||||
branch_mid_y = box3.top + box3.height + vertical_gap/2
|
||||
# Do ľavej vetvy:
|
||||
connector_left = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, mid_point, box3.top + box3.height, left_branch_left + block_width/2, branch_mid_y)
|
||||
# Do pravej vetvy:
|
||||
connector_right = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, mid_point, box3.top + box3.height, right_branch_left + block_width/2, branch_mid_y)
|
||||
|
||||
# Prepojenie blokov v ľavej vetve
|
||||
add_connector(slide, box4A, box5A)
|
||||
add_connector(slide, box5A, box6A)
|
||||
add_connector(slide, box6A, box7A)
|
||||
|
||||
# Prepojenie blokov v pravej vetve
|
||||
add_connector(slide, box4B, box5B)
|
||||
add_connector(slide, box5B, box6B)
|
||||
add_connector(slide, box6B, box7B)
|
||||
add_connector(slide, box7B, box8B)
|
||||
add_connector(slide, box8B, box9B)
|
||||
|
||||
# Spojenie oboch vetiev s finálnym blokom "Výstup"
|
||||
add_connector(slide, box7A, final_box)
|
||||
add_connector(slide, box9B, final_box)
|
||||
|
||||
# Uloženie prezentácie
|
||||
prs.save("architecture_diagram.pptx")
|
||||
|
@ -1,3 +1,3 @@
|
||||
{
|
||||
"useCloud" : false
|
||||
{
|
||||
"useCloud" : false
|
||||
}
|
@ -1,80 +1,80 @@
|
||||
from elasticsearch import Elasticsearch
|
||||
from langchain.embeddings import HuggingFaceEmbeddings
|
||||
from elasticsearch.helpers import bulk
|
||||
import json
|
||||
|
||||
# Настройка подключения к Elasticsearch с аутентификацией и HTTPS
|
||||
es = Elasticsearch(
|
||||
[{'host': 'localhost', 'port': 9200, 'scheme': 'https'}],
|
||||
http_auth=('elastic', '3lvFhvVYrazLsj=M-R_g'), # замените на ваш пароль
|
||||
verify_certs=False # Отключить проверку SSL-сертификата, если используется самоподписанный сертификат
|
||||
)
|
||||
|
||||
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
||||
|
||||
def create_index():
|
||||
# Определяем маппинг для индекса
|
||||
mapping = {
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "text",
|
||||
"analyzer": "standard"
|
||||
},
|
||||
"vector": {
|
||||
"type": "dense_vector",
|
||||
"dims": 384 # Размерность векторного представления
|
||||
},
|
||||
"full_data": {
|
||||
"type": "object",
|
||||
"enabled": False # Отключаем индексацию вложенных данных
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
es.indices.create(index='drug_docs', body=mapping, ignore=400)
|
||||
|
||||
def load_drug_data(json_path):
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
|
||||
def index_documents(data):
|
||||
actions = []
|
||||
total_docs = len(data)
|
||||
for i, item in enumerate(data, start=1):
|
||||
doc_text = f"{item['link']} {item.get('pribalovy_letak', '')} {item.get('spc', '')}"
|
||||
|
||||
vector = embeddings.embed_query(doc_text)
|
||||
|
||||
action = {
|
||||
"_index": "drug_docs",
|
||||
"_id": i,
|
||||
"_source": {
|
||||
'text': doc_text,
|
||||
'vector': vector,
|
||||
'full_data': item
|
||||
}
|
||||
}
|
||||
actions.append(action)
|
||||
|
||||
# Отображение прогресса
|
||||
print(f"Индексируется документ {i}/{total_docs}", end='\r')
|
||||
|
||||
# Опционально: индексируем пакетами по N документов
|
||||
if i % 100 == 0 or i == total_docs:
|
||||
bulk(es, actions)
|
||||
actions = []
|
||||
|
||||
# Если остались неиндексированные документы
|
||||
if actions:
|
||||
bulk(es, actions)
|
||||
|
||||
print("\nИндексирование завершено.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_index()
|
||||
data_path = "../../esDB/cleaned_general_info_additional.json"
|
||||
drug_data = load_drug_data(data_path)
|
||||
index_documents(drug_data)
|
||||
|
||||
from elasticsearch import Elasticsearch
|
||||
from langchain.embeddings import HuggingFaceEmbeddings
|
||||
from elasticsearch.helpers import bulk
|
||||
import json
|
||||
|
||||
# Настройка подключения к Elasticsearch с аутентификацией и HTTPS
|
||||
es = Elasticsearch(
|
||||
[{'host': 'localhost', 'port': 9200, 'scheme': 'https'}],
|
||||
http_auth=('elastic', '3lvFhvVYrazLsj=M-R_g'), # замените на ваш пароль
|
||||
verify_certs=False # Отключить проверку SSL-сертификата, если используется самоподписанный сертификат
|
||||
)
|
||||
|
||||
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
||||
|
||||
def create_index():
|
||||
# Определяем маппинг для индекса
|
||||
mapping = {
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "text",
|
||||
"analyzer": "standard"
|
||||
},
|
||||
"vector": {
|
||||
"type": "dense_vector",
|
||||
"dims": 384 # Размерность векторного представления
|
||||
},
|
||||
"full_data": {
|
||||
"type": "object",
|
||||
"enabled": False # Отключаем индексацию вложенных данных
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
es.indices.create(index='drug_docs', body=mapping, ignore=400)
|
||||
|
||||
def load_drug_data(json_path):
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
|
||||
def index_documents(data):
|
||||
actions = []
|
||||
total_docs = len(data)
|
||||
for i, item in enumerate(data, start=1):
|
||||
doc_text = f"{item['link']} {item.get('pribalovy_letak', '')} {item.get('spc', '')}"
|
||||
|
||||
vector = embeddings.embed_query(doc_text)
|
||||
|
||||
action = {
|
||||
"_index": "drug_docs",
|
||||
"_id": i,
|
||||
"_source": {
|
||||
'text': doc_text,
|
||||
'vector': vector,
|
||||
'full_data': item
|
||||
}
|
||||
}
|
||||
actions.append(action)
|
||||
|
||||
# Отображение прогресса
|
||||
print(f"Индексируется документ {i}/{total_docs}", end='\r')
|
||||
|
||||
# Опционально: индексируем пакетами по N документов
|
||||
if i % 100 == 0 or i == total_docs:
|
||||
bulk(es, actions)
|
||||
actions = []
|
||||
|
||||
# Если остались неиндексированные документы
|
||||
if actions:
|
||||
bulk(es, actions)
|
||||
|
||||
print("\nИндексирование завершено.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_index()
|
||||
data_path = "../../esDB/cleaned_general_info_additional.json"
|
||||
drug_data = load_drug_data(data_path)
|
||||
index_documents(drug_data)
|
||||
|
||||
|
@ -1,80 +1,80 @@
|
||||
from elasticsearch import Elasticsearch
|
||||
from langchain.embeddings import HuggingFaceEmbeddings
|
||||
from elasticsearch.helpers import bulk
|
||||
import json
|
||||
|
||||
# Настройка подключения к Elasticsearch с аутентификацией и HTTPS
|
||||
es = Elasticsearch(
|
||||
[{'host': 'localhost', 'port': 9200, 'scheme': 'https'}],
|
||||
http_auth=('elastic', 'S7DoO3ma=G=9USBPbqq3'), # замените на ваш пароль
|
||||
verify_certs=False # Отключить проверку SSL-сертификата, если используется самоподписанный сертификат
|
||||
)
|
||||
|
||||
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
||||
|
||||
def create_index():
|
||||
# Определяем маппинг для индекса
|
||||
mapping = {
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "text",
|
||||
"analyzer": "standard"
|
||||
},
|
||||
"vector": {
|
||||
"type": "dense_vector",
|
||||
"dims": 384 # Размерность векторного представления
|
||||
},
|
||||
"full_data": {
|
||||
"type": "object",
|
||||
"enabled": False # Отключаем индексацию вложенных данных
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
es.indices.create(index='drug_docs', body=mapping, ignore=400)
|
||||
|
||||
def load_drug_data(json_path):
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
|
||||
def index_documents(data):
|
||||
actions = []
|
||||
total_docs = len(data)
|
||||
for i, item in enumerate(data, start=1):
|
||||
doc_text = f"{item['link']} {item.get('pribalovy_letak', '')} {item.get('spc', '')}"
|
||||
|
||||
vector = embeddings.embed_query(doc_text)
|
||||
|
||||
action = {
|
||||
"_index": "drug_docs",
|
||||
"_id": i,
|
||||
"_source": {
|
||||
'text': doc_text,
|
||||
'vector': vector,
|
||||
'full_data': item
|
||||
}
|
||||
}
|
||||
actions.append(action)
|
||||
|
||||
# Отображение прогресса
|
||||
print(f"Индексируется документ {i}/{total_docs}", end='\r')
|
||||
|
||||
# Опционально: индексируем пакетами по N документов
|
||||
if i % 100 == 0 or i == total_docs:
|
||||
bulk(es, actions)
|
||||
actions = []
|
||||
|
||||
# Если остались неиндексированные документы
|
||||
if actions:
|
||||
bulk(es, actions)
|
||||
|
||||
print("\nИндексирование завершено.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_index()
|
||||
data_path = "/home/poiasnik/esDB/cleaned_general_info_additional.json"
|
||||
drug_data = load_drug_data(data_path)
|
||||
index_documents(drug_data)
|
||||
|
||||
from elasticsearch import Elasticsearch
|
||||
from langchain.embeddings import HuggingFaceEmbeddings
|
||||
from elasticsearch.helpers import bulk
|
||||
import json
|
||||
|
||||
# Настройка подключения к Elasticsearch с аутентификацией и HTTPS
|
||||
es = Elasticsearch(
|
||||
[{'host': 'localhost', 'port': 9200, 'scheme': 'https'}],
|
||||
http_auth=('elastic', 'S7DoO3ma=G=9USBPbqq3'), # замените на ваш пароль
|
||||
verify_certs=False # Отключить проверку SSL-сертификата, если используется самоподписанный сертификат
|
||||
)
|
||||
|
||||
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
||||
|
||||
def create_index():
|
||||
# Определяем маппинг для индекса
|
||||
mapping = {
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "text",
|
||||
"analyzer": "standard"
|
||||
},
|
||||
"vector": {
|
||||
"type": "dense_vector",
|
||||
"dims": 384 # Размерность векторного представления
|
||||
},
|
||||
"full_data": {
|
||||
"type": "object",
|
||||
"enabled": False # Отключаем индексацию вложенных данных
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
es.indices.create(index='drug_docs', body=mapping, ignore=400)
|
||||
|
||||
def load_drug_data(json_path):
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
|
||||
def index_documents(data):
|
||||
actions = []
|
||||
total_docs = len(data)
|
||||
for i, item in enumerate(data, start=1):
|
||||
doc_text = f"{item['link']} {item.get('pribalovy_letak', '')} {item.get('spc', '')}"
|
||||
|
||||
vector = embeddings.embed_query(doc_text)
|
||||
|
||||
action = {
|
||||
"_index": "drug_docs",
|
||||
"_id": i,
|
||||
"_source": {
|
||||
'text': doc_text,
|
||||
'vector': vector,
|
||||
'full_data': item
|
||||
}
|
||||
}
|
||||
actions.append(action)
|
||||
|
||||
# Отображение прогресса
|
||||
print(f"Индексируется документ {i}/{total_docs}", end='\r')
|
||||
|
||||
# Опционально: индексируем пакетами по N документов
|
||||
if i % 100 == 0 or i == total_docs:
|
||||
bulk(es, actions)
|
||||
actions = []
|
||||
|
||||
# Если остались неиндексированные документы
|
||||
if actions:
|
||||
bulk(es, actions)
|
||||
|
||||
print("\nИндексирование завершено.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_index()
|
||||
data_path = "/home/poiasnik/esDB/cleaned_general_info_additional.json"
|
||||
drug_data = load_drug_data(data_path)
|
||||
index_documents(drug_data)
|
||||
|
||||
|
@ -1,41 +1,41 @@
|
||||
from elasticsearch import Elasticsearch
|
||||
from langchain_huggingface import HuggingFaceEmbeddings
|
||||
import json
|
||||
import sys
|
||||
|
||||
es = Elasticsearch(
|
||||
cloud_id="tt:dXMtZWFzdC0yLmF3cy5lbGFzdGljLWNsb3VkLmNvbTo0NDMkOGM3ODQ0ZWVhZTEyNGY3NmFjNjQyNDFhNjI4NmVhYzMkZTI3YjlkNTQ0ODdhNGViNmEyMTcxMjMxNmJhMWI0ZGU=",
|
||||
basic_auth=("elastic", "sSz2BEGv56JRNjGFwoQ191RJ")
|
||||
)
|
||||
|
||||
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
||||
|
||||
|
||||
def load_drug_data(json_path):
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
|
||||
|
||||
def index_documents(data):
|
||||
total_documents = len(data)
|
||||
for i, item in enumerate(data, start=1):
|
||||
doc_text = f"{item['link']} {item.get('pribalovy_letak', '')} {item.get('spc', '')}"
|
||||
|
||||
vector = embeddings.embed_query(doc_text)
|
||||
|
||||
es.index(index='drug_docs', id=i, body={
|
||||
'text': doc_text,
|
||||
'vector': vector,
|
||||
'full_data': item
|
||||
})
|
||||
|
||||
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
|
||||
|
||||
data_path = "../../data_adc_databaza/cleaned_general_info_additional.json"
|
||||
drug_data = load_drug_data(data_path)
|
||||
index_documents(drug_data)
|
||||
from elasticsearch import Elasticsearch
|
||||
from langchain_huggingface import HuggingFaceEmbeddings
|
||||
import json
|
||||
import sys
|
||||
|
||||
es = Elasticsearch(
|
||||
cloud_id="tt:dXMtZWFzdC0yLmF3cy5lbGFzdGljLWNsb3VkLmNvbTo0NDMkOGM3ODQ0ZWVhZTEyNGY3NmFjNjQyNDFhNjI4NmVhYzMkZTI3YjlkNTQ0ODdhNGViNmEyMTcxMjMxNmJhMWI0ZGU=",
|
||||
basic_auth=("elastic", "sSz2BEGv56JRNjGFwoQ191RJ")
|
||||
)
|
||||
|
||||
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
||||
|
||||
|
||||
def load_drug_data(json_path):
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
|
||||
|
||||
def index_documents(data):
|
||||
total_documents = len(data)
|
||||
for i, item in enumerate(data, start=1):
|
||||
doc_text = f"{item['link']} {item.get('pribalovy_letak', '')} {item.get('spc', '')}"
|
||||
|
||||
vector = embeddings.embed_query(doc_text)
|
||||
|
||||
es.index(index='drug_docs', id=i, body={
|
||||
'text': doc_text,
|
||||
'vector': vector,
|
||||
'full_data': item
|
||||
})
|
||||
|
||||
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
|
||||
|
||||
data_path = "../../data_adc_databaza/cleaned_general_info_additional.json"
|
||||
drug_data = load_drug_data(data_path)
|
||||
index_documents(drug_data)
|
||||
|
@ -1,70 +1,70 @@
|
||||
from elasticsearch import Elasticsearch
|
||||
from langchain_huggingface import HuggingFaceEmbeddings
|
||||
from elasticsearch.helpers import bulk
|
||||
import json
|
||||
|
||||
es = Elasticsearch([{'host': 'localhost', 'port': 9200, 'scheme': 'http'}])
|
||||
|
||||
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
||||
|
||||
def create_index():
|
||||
|
||||
mapping = {
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "text",
|
||||
"analyzer": "standard"
|
||||
},
|
||||
"vector": {
|
||||
"type": "dense_vector",
|
||||
"dims": 384
|
||||
},
|
||||
"full_data": {
|
||||
"type": "object",
|
||||
"enabled": False
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
es.indices.create(index='drug_docs', body=mapping, ignore=400)
|
||||
|
||||
def load_drug_data(json_path):
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
|
||||
def index_documents(data):
|
||||
actions = []
|
||||
total_docs = len(data)
|
||||
for i, item in enumerate(data, start=1):
|
||||
doc_text = f"{item['link']} {item.get('pribalovy_letak', '')} {item.get('spc', '')}"
|
||||
|
||||
vector = embeddings.embed_query(doc_text)
|
||||
|
||||
action = {
|
||||
"_index": "drug_docs",
|
||||
"_id": i,
|
||||
"_source": {
|
||||
'text': doc_text,
|
||||
'vector': vector,
|
||||
'full_data': item
|
||||
}
|
||||
}
|
||||
actions.append(action)
|
||||
|
||||
|
||||
|
||||
if i % 100 == 0 or i == total_docs:
|
||||
bulk(es, actions)
|
||||
actions = []
|
||||
|
||||
if actions:
|
||||
bulk(es, actions)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_index()
|
||||
data_path = "../../data_adc_databaza/cleaned_general_info_additional.json"
|
||||
drug_data = load_drug_data(data_path)
|
||||
index_documents(drug_data)
|
||||
from elasticsearch import Elasticsearch
|
||||
from langchain_huggingface import HuggingFaceEmbeddings
|
||||
from elasticsearch.helpers import bulk
|
||||
import json
|
||||
|
||||
es = Elasticsearch([{'host': 'localhost', 'port': 9200, 'scheme': 'http'}])
|
||||
|
||||
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
||||
|
||||
def create_index():
|
||||
|
||||
mapping = {
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "text",
|
||||
"analyzer": "standard"
|
||||
},
|
||||
"vector": {
|
||||
"type": "dense_vector",
|
||||
"dims": 384
|
||||
},
|
||||
"full_data": {
|
||||
"type": "object",
|
||||
"enabled": False
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
es.indices.create(index='drug_docs', body=mapping, ignore=400)
|
||||
|
||||
def load_drug_data(json_path):
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
|
||||
def index_documents(data):
|
||||
actions = []
|
||||
total_docs = len(data)
|
||||
for i, item in enumerate(data, start=1):
|
||||
doc_text = f"{item['link']} {item.get('pribalovy_letak', '')} {item.get('spc', '')}"
|
||||
|
||||
vector = embeddings.embed_query(doc_text)
|
||||
|
||||
action = {
|
||||
"_index": "drug_docs",
|
||||
"_id": i,
|
||||
"_source": {
|
||||
'text': doc_text,
|
||||
'vector': vector,
|
||||
'full_data': item
|
||||
}
|
||||
}
|
||||
actions.append(action)
|
||||
|
||||
|
||||
|
||||
if i % 100 == 0 or i == total_docs:
|
||||
bulk(es, actions)
|
||||
actions = []
|
||||
|
||||
if actions:
|
||||
bulk(es, actions)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_index()
|
||||
data_path = "../../data_adc_databaza/cleaned_general_info_additional.json"
|
||||
drug_data = load_drug_data(data_path)
|
||||
index_documents(drug_data)
|
||||
|
1082
Backend/model.py
1082
Backend/model.py
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,12 @@
|
||||
Flask
|
||||
flask-cors
|
||||
requests
|
||||
elasticsearch
|
||||
langchain
|
||||
langchain-huggingface
|
||||
langchain-elasticsearch
|
||||
psycopg2-binary
|
||||
google-auth
|
||||
transformers
|
||||
sentence-transformers>=2.2.2
|
||||
accelerate
|
||||
Flask
|
||||
flask-cors
|
||||
requests
|
||||
elasticsearch
|
||||
langchain
|
||||
langchain-huggingface
|
||||
langchain-elasticsearch
|
||||
psycopg2-binary
|
||||
google-auth
|
||||
transformers
|
||||
sentence-transformers>=2.2.2
|
||||
accelerate
|
||||
|
@ -1,361 +1,366 @@
|
||||
import time
|
||||
import re
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_cors import CORS
|
||||
from google.oauth2 import id_token
|
||||
from google.auth.transport import requests as google_requests
|
||||
import logging
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from model import process_query_with_mistral
|
||||
|
||||
_real_time = time.time
|
||||
time.time = lambda: _real_time() - 1
|
||||
|
||||
# Database connection parameters
|
||||
DATABASE_CONFIG = {
|
||||
"dbname": "HealthAIDB",
|
||||
"user": "postgres",
|
||||
"password": "Oleg2005",
|
||||
"host": "postgres",
|
||||
"port": 5432,
|
||||
}
|
||||
|
||||
import logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
conn = psycopg2.connect(**DATABASE_CONFIG)
|
||||
logger.info("Database connection established successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error connecting to database: {e}", exc_info=True)
|
||||
conn = None
|
||||
|
||||
def init_db():
|
||||
create_users_query = """
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
google_id TEXT,
|
||||
password TEXT
|
||||
);
|
||||
"""
|
||||
create_chat_history_query = """
|
||||
CREATE TABLE IF NOT EXISTS chat_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_email TEXT NOT NULL,
|
||||
chat TEXT NOT NULL,
|
||||
user_data TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
"""
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(create_users_query)
|
||||
cur.execute(create_chat_history_query)
|
||||
conn.commit()
|
||||
logger.info("Database tables initialized successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error initializing database tables: {e}", exc_info=True)
|
||||
conn.rollback()
|
||||
|
||||
if conn:
|
||||
init_db()
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app, resources={r"/api/*": {"origins": "*"}})
|
||||
|
||||
CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com"
|
||||
|
||||
def save_user_to_db(name, email, google_id=None, password=None):
|
||||
logger.info(f"Saving user {name} with email: {email} to the database")
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO users (name, email, google_id, password)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
ON CONFLICT (email) DO NOTHING
|
||||
""",
|
||||
(name, email, google_id, password)
|
||||
)
|
||||
conn.commit()
|
||||
logger.info(f"User {name} ({email}) saved successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving user {name} ({email}) to database: {e}", exc_info=True)
|
||||
|
||||
@app.route('/api/verify', methods=['POST'])
|
||||
def verify_token():
|
||||
logger.info("Received token verification request")
|
||||
data = request.get_json()
|
||||
token = data.get('token')
|
||||
if not token:
|
||||
logger.warning("Token not provided in request")
|
||||
return jsonify({'error': 'No token provided'}), 400
|
||||
try:
|
||||
id_info = id_token.verify_oauth2_token(token, google_requests.Request(), CLIENT_ID)
|
||||
user_email = id_info.get('email')
|
||||
user_name = id_info.get('name')
|
||||
google_id = id_info.get('sub')
|
||||
logger.info(f"Token verified for user: {user_name} ({user_email})")
|
||||
save_user_to_db(name=user_name, email=user_email, google_id=google_id)
|
||||
return jsonify({'message': 'Authentication successful', 'user': {'email': user_email, 'name': user_name}}), 200
|
||||
except ValueError as e:
|
||||
logger.error(f"Token verification error: {e}", exc_info=True)
|
||||
return jsonify({'error': 'Invalid token'}), 400
|
||||
|
||||
@app.route('/api/register', methods=['POST'])
|
||||
def register():
|
||||
logger.info("Received new user registration request")
|
||||
data = request.get_json()
|
||||
name = data.get('name')
|
||||
email = data.get('email')
|
||||
password = data.get('password')
|
||||
if not all([name, email, password]):
|
||||
logger.warning("Not all required fields provided for registration")
|
||||
return jsonify({'error': 'All fields are required'}), 400
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
|
||||
existing_user = cur.fetchone()
|
||||
if existing_user:
|
||||
logger.warning(f"User with email {email} already exists")
|
||||
return jsonify({'error': 'User already exists'}), 409
|
||||
save_user_to_db(name=name, email=email, password=password)
|
||||
logger.info(f"User {name} ({email}) registered successfully")
|
||||
return jsonify({'message': 'User registered successfully'}), 201
|
||||
except Exception as e:
|
||||
logger.error(f"Error during user registration: {e}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/login', methods=['POST'])
|
||||
def login():
|
||||
logger.info("Received login request")
|
||||
data = request.get_json()
|
||||
email = data.get('email')
|
||||
password = data.get('password')
|
||||
if not all([email, password]):
|
||||
logger.warning("Email or password not provided")
|
||||
return jsonify({'error': 'Email and password are required'}), 400
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
|
||||
user = cur.fetchone()
|
||||
if not user or user.get('password') != password:
|
||||
logger.warning(f"Invalid credentials for email: {email}")
|
||||
return jsonify({'error': 'Invalid credentials'}), 401
|
||||
logger.info(f"User {user.get('name')} ({email}) logged in successfully")
|
||||
return jsonify({'message': 'Login successful', 'user': {'name': user.get('name'), 'email': user.get('email')}}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error during user login: {e}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/update_profile', methods=['PUT'])
|
||||
def update_profile():
|
||||
data = request.get_json()
|
||||
email = data.get('email')
|
||||
if not email:
|
||||
return jsonify({'error': 'Email is required'}), 400
|
||||
|
||||
# Fields to update; if not provided, the current value remains.
|
||||
name = data.get('name')
|
||||
phone = data.get('phone')
|
||||
role = data.get('role')
|
||||
bio = data.get('bio')
|
||||
picture = data.get('picture')
|
||||
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE users
|
||||
SET name = COALESCE(%s, name),
|
||||
phone = COALESCE(%s, phone),
|
||||
role = COALESCE(%s, role),
|
||||
bio = COALESCE(%s, bio),
|
||||
picture = COALESCE(%s, picture)
|
||||
WHERE email = %s
|
||||
""",
|
||||
(name, phone, role, bio, picture, email)
|
||||
)
|
||||
conn.commit()
|
||||
logger.info(f"Profile updated for {email}")
|
||||
return jsonify({'message': 'Profile updated successfully'}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating profile: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/chat', methods=['POST'])
|
||||
def chat():
|
||||
logger.info("Received chat request")
|
||||
data = request.get_json()
|
||||
query = data.get('query', '')
|
||||
user_email = data.get('email')
|
||||
chat_id = data.get('chatId')
|
||||
|
||||
if not query:
|
||||
logger.warning("No query provided")
|
||||
return jsonify({'error': 'No query provided'}), 400
|
||||
|
||||
logger.info(f"Processing request for chatId: {chat_id if chat_id else 'new chat'} | Query: {query}")
|
||||
|
||||
# Retrieve chat context from the database
|
||||
chat_context = ""
|
||||
if chat_id:
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("SELECT chat, user_data FROM chat_history WHERE id = %s", (chat_id,))
|
||||
result = cur.fetchone()
|
||||
if result:
|
||||
chat_context = result.get("chat", "")
|
||||
logger.info(f"Loaded chat context for chatId {chat_id}: {chat_context}")
|
||||
else:
|
||||
logger.info(f"No chat context found for chatId {chat_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading chat context from DB: {e}", exc_info=True)
|
||||
|
||||
logger.info("Calling process_query_with_mistral function")
|
||||
response_obj = process_query_with_mistral(query, chat_id=chat_id, chat_context=chat_context)
|
||||
best_answer = response_obj.get("best_answer", "") if isinstance(response_obj, dict) else str(response_obj)
|
||||
logger.info(f"Response from process_query_with_mistral: {best_answer}")
|
||||
|
||||
best_answer = re.sub(r'[*#]', '', best_answer)
|
||||
best_answer = re.sub(r'(\d\.\s)', r'\n\n\1', best_answer)
|
||||
best_answer = re.sub(r':\s-', r':\n-', best_answer)
|
||||
|
||||
# Update or create chat_history record including user_data if available
|
||||
if chat_id:
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("SELECT chat FROM chat_history WHERE id = %s", (chat_id,))
|
||||
existing_chat = cur.fetchone()
|
||||
if existing_chat:
|
||||
updated_chat = existing_chat['chat'] + f"\nUser: {query}\nBot: {best_answer}"
|
||||
if "patient_data" in response_obj:
|
||||
cur.execute("UPDATE chat_history SET chat = %s, user_data = %s WHERE id = %s",
|
||||
(updated_chat, response_obj["patient_data"], chat_id))
|
||||
else:
|
||||
cur.execute("UPDATE chat_history SET chat = %s WHERE id = %s", (updated_chat, chat_id))
|
||||
conn.commit()
|
||||
logger.info(f"Chat history for chatId {chat_id} updated successfully")
|
||||
else:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur2:
|
||||
cur2.execute(
|
||||
"INSERT INTO chat_history (user_email, chat) VALUES (%s, %s) RETURNING id",
|
||||
(user_email, f"User: {query}\nBot: {best_answer}")
|
||||
)
|
||||
new_chat_id = cur2.fetchone()['id']
|
||||
conn.commit()
|
||||
chat_id = new_chat_id
|
||||
logger.info(f"New chat created with chatId: {chat_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating/creating chat history: {e}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
else:
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO chat_history (user_email, chat) VALUES (%s, %s) RETURNING id",
|
||||
(user_email, f"User: {query}\nBot: {best_answer}")
|
||||
)
|
||||
new_chat_id = cur.fetchone()['id']
|
||||
conn.commit()
|
||||
chat_id = new_chat_id
|
||||
logger.info(f"New chat created with chatId: {chat_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating new chat: {e}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
return jsonify({'response': {'best_answer': best_answer, 'model': response_obj.get("model", ""), 'chatId': chat_id}}), 200
|
||||
|
||||
@app.route('/api/save_user_data', methods=['POST'])
|
||||
def save_user_data():
|
||||
logger.info("Received request to save user data")
|
||||
data = request.get_json()
|
||||
chat_id = data.get('chatId')
|
||||
user_data = data.get('userData')
|
||||
if not chat_id or not user_data:
|
||||
return jsonify({'error': 'chatId and userData are required'}), 400
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("UPDATE chat_history SET user_data = %s WHERE id = %s", (user_data, chat_id))
|
||||
conn.commit()
|
||||
logger.info(f"User data for chatId {chat_id} updated successfully")
|
||||
return jsonify({'message': 'User data updated successfully'}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating user data: {e}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/chat_history', methods=['GET'])
|
||||
def get_chat_history():
|
||||
logger.info("Received request to get chat history")
|
||||
user_email = request.args.get('email')
|
||||
if not user_email:
|
||||
return jsonify({'error': 'User email is required'}), 400
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"SELECT id, chat, user_data, created_at FROM chat_history WHERE user_email = %s ORDER BY created_at DESC",
|
||||
(user_email,)
|
||||
)
|
||||
history = cur.fetchall()
|
||||
return jsonify({'history': history}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting chat history for {user_email}: {e}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/chat_history', methods=['DELETE'])
|
||||
def delete_chat():
|
||||
logger.info("Received request to delete chat")
|
||||
chat_id = request.args.get('id')
|
||||
if not chat_id:
|
||||
return jsonify({'error': 'Chat id is required'}), 400
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("DELETE FROM chat_history WHERE id = %s", (chat_id,))
|
||||
conn.commit()
|
||||
return jsonify({'message': 'Chat deleted successfully'}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting chat with chatId {chat_id}: {e}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/get_user_data', methods=['GET'])
|
||||
def get_user_data():
|
||||
chat_id = request.args.get('chatId')
|
||||
if not chat_id:
|
||||
return jsonify({'error': 'Chat id is required'}), 400
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("SELECT user_data FROM chat_history WHERE id = %s", (chat_id,))
|
||||
result = cur.fetchone()
|
||||
if result and result.get("user_data"):
|
||||
return jsonify({'user_data': result.get("user_data")}), 200
|
||||
else:
|
||||
return jsonify({'user_data': None}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving user data: {e}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/chat_history_detail', methods=['GET'])
|
||||
def chat_history_detail():
|
||||
logger.info("Received request to get chat details")
|
||||
chat_id = request.args.get('id')
|
||||
if not chat_id:
|
||||
return jsonify({'error': 'Chat id is required'}), 400
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("SELECT id, chat, user_data, created_at FROM chat_history WHERE id = %s", (chat_id,))
|
||||
chat = cur.fetchone()
|
||||
if not chat:
|
||||
return jsonify({'error': 'Chat not found'}), 404
|
||||
return jsonify({'chat': chat}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting chat details for chatId {chat_id}: {e}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
logger.info("Starting Flask application")
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
import time
|
||||
import re
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_cors import CORS
|
||||
from google.oauth2 import id_token
|
||||
from google.auth.transport import requests as google_requests
|
||||
import logging
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from model import process_query_with_mistral
|
||||
|
||||
_real_time = time.time
|
||||
time.time = lambda: _real_time() - 1
|
||||
|
||||
# Database connection parameters
|
||||
DATABASE_CONFIG = {
|
||||
"dbname": "HealthAIDB",
|
||||
"user": "postgres",
|
||||
"password": "Oleg2005",
|
||||
"host": "postgres",
|
||||
"port": 5432,
|
||||
}
|
||||
|
||||
import logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
conn = psycopg2.connect(**DATABASE_CONFIG)
|
||||
logger.info("Database connection established successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error connecting to database: {e}", exc_info=True)
|
||||
conn = None
|
||||
|
||||
def init_db():
|
||||
create_users_query = """
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
google_id TEXT,
|
||||
password TEXT,
|
||||
phone TEXT,
|
||||
role TEXT,
|
||||
bio TEXT,
|
||||
picture TEXT
|
||||
);
|
||||
|
||||
"""
|
||||
create_chat_history_query = """
|
||||
CREATE TABLE IF NOT EXISTS chat_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_email TEXT NOT NULL,
|
||||
chat TEXT NOT NULL,
|
||||
user_data TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
"""
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(create_users_query)
|
||||
cur.execute(create_chat_history_query)
|
||||
conn.commit()
|
||||
logger.info("Database tables initialized successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error initializing database tables: {e}", exc_info=True)
|
||||
conn.rollback()
|
||||
|
||||
if conn:
|
||||
init_db()
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app, resources={r"/api/*": {"origins": "*"}})
|
||||
|
||||
CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com"
|
||||
|
||||
def save_user_to_db(name, email, google_id=None, password=None):
|
||||
logger.info(f"Saving user {name} with email: {email} to the database")
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO users (name, email, google_id, password)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
ON CONFLICT (email) DO NOTHING
|
||||
""",
|
||||
(name, email, google_id, password)
|
||||
)
|
||||
conn.commit()
|
||||
logger.info(f"User {name} ({email}) saved successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving user {name} ({email}) to database: {e}", exc_info=True)
|
||||
|
||||
@app.route('/api/verify', methods=['POST'])
|
||||
def verify_token():
|
||||
logger.info("Received token verification request")
|
||||
data = request.get_json()
|
||||
token = data.get('token')
|
||||
if not token:
|
||||
logger.warning("Token not provided in request")
|
||||
return jsonify({'error': 'No token provided'}), 400
|
||||
try:
|
||||
id_info = id_token.verify_oauth2_token(token, google_requests.Request(), CLIENT_ID)
|
||||
user_email = id_info.get('email')
|
||||
user_name = id_info.get('name')
|
||||
google_id = id_info.get('sub')
|
||||
logger.info(f"Token verified for user: {user_name} ({user_email})")
|
||||
save_user_to_db(name=user_name, email=user_email, google_id=google_id)
|
||||
return jsonify({'message': 'Authentication successful', 'user': {'email': user_email, 'name': user_name}}), 200
|
||||
except ValueError as e:
|
||||
logger.error(f"Token verification error: {e}", exc_info=True)
|
||||
return jsonify({'error': 'Invalid token'}), 400
|
||||
|
||||
@app.route('/api/register', methods=['POST'])
|
||||
def register():
|
||||
logger.info("Received new user registration request")
|
||||
data = request.get_json()
|
||||
name = data.get('name')
|
||||
email = data.get('email')
|
||||
password = data.get('password')
|
||||
if not all([name, email, password]):
|
||||
logger.warning("Not all required fields provided for registration")
|
||||
return jsonify({'error': 'All fields are required'}), 400
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
|
||||
existing_user = cur.fetchone()
|
||||
if existing_user:
|
||||
logger.warning(f"User with email {email} already exists")
|
||||
return jsonify({'error': 'User already exists'}), 409
|
||||
save_user_to_db(name=name, email=email, password=password)
|
||||
logger.info(f"User {name} ({email}) registered successfully")
|
||||
return jsonify({'message': 'User registered successfully'}), 201
|
||||
except Exception as e:
|
||||
logger.error(f"Error during user registration: {e}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/login', methods=['POST'])
|
||||
def login():
|
||||
logger.info("Received login request")
|
||||
data = request.get_json()
|
||||
email = data.get('email')
|
||||
password = data.get('password')
|
||||
if not all([email, password]):
|
||||
logger.warning("Email or password not provided")
|
||||
return jsonify({'error': 'Email and password are required'}), 400
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
|
||||
user = cur.fetchone()
|
||||
if not user or user.get('password') != password:
|
||||
logger.warning(f"Invalid credentials for email: {email}")
|
||||
return jsonify({'error': 'Invalid credentials'}), 401
|
||||
logger.info(f"User {user.get('name')} ({email}) logged in successfully")
|
||||
return jsonify({'message': 'Login successful', 'user': {'name': user.get('name'), 'email': user.get('email')}}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error during user login: {e}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/update_profile', methods=['PUT'])
|
||||
def update_profile():
|
||||
data = request.get_json()
|
||||
email = data.get('email')
|
||||
if not email:
|
||||
return jsonify({'error': 'Email is required'}), 400
|
||||
|
||||
# Fields to update; if not provided, the current value remains.
|
||||
name = data.get('name')
|
||||
phone = data.get('phone')
|
||||
role = data.get('role')
|
||||
bio = data.get('bio')
|
||||
picture = data.get('picture')
|
||||
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE users
|
||||
SET name = COALESCE(%s, name),
|
||||
phone = COALESCE(%s, phone),
|
||||
role = COALESCE(%s, role),
|
||||
bio = COALESCE(%s, bio),
|
||||
picture = COALESCE(%s, picture)
|
||||
WHERE email = %s
|
||||
""",
|
||||
(name, phone, role, bio, picture, email)
|
||||
)
|
||||
conn.commit()
|
||||
logger.info(f"Profile updated for {email}")
|
||||
return jsonify({'message': 'Profile updated successfully'}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating profile: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/chat', methods=['POST'])
|
||||
def chat():
|
||||
logger.info("Received chat request")
|
||||
data = request.get_json()
|
||||
query = data.get('query', '')
|
||||
user_email = data.get('email')
|
||||
chat_id = data.get('chatId')
|
||||
|
||||
if not query:
|
||||
logger.warning("No query provided")
|
||||
return jsonify({'error': 'No query provided'}), 400
|
||||
|
||||
logger.info(f"Processing request for chatId: {chat_id if chat_id else 'new chat'} | Query: {query}")
|
||||
|
||||
# Retrieve chat context from the database
|
||||
chat_context = ""
|
||||
if chat_id:
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("SELECT chat, user_data FROM chat_history WHERE id = %s", (chat_id,))
|
||||
result = cur.fetchone()
|
||||
if result:
|
||||
chat_context = result.get("chat", "")
|
||||
logger.info(f"Loaded chat context for chatId {chat_id}: {chat_context}")
|
||||
else:
|
||||
logger.info(f"No chat context found for chatId {chat_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading chat context from DB: {e}", exc_info=True)
|
||||
|
||||
logger.info("Calling process_query_with_mistral function")
|
||||
response_obj = process_query_with_mistral(query, chat_id=chat_id, chat_context=chat_context)
|
||||
best_answer = response_obj.get("best_answer", "") if isinstance(response_obj, dict) else str(response_obj)
|
||||
logger.info(f"Response from process_query_with_mistral: {best_answer}")
|
||||
|
||||
best_answer = re.sub(r'[*#]', '', best_answer)
|
||||
best_answer = re.sub(r'(\d\.\s)', r'\n\n\1', best_answer)
|
||||
best_answer = re.sub(r':\s-', r':\n-', best_answer)
|
||||
|
||||
# Update or create chat_history record including user_data if available
|
||||
if chat_id:
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("SELECT chat FROM chat_history WHERE id = %s", (chat_id,))
|
||||
existing_chat = cur.fetchone()
|
||||
if existing_chat:
|
||||
updated_chat = existing_chat['chat'] + f"\nUser: {query}\nBot: {best_answer}"
|
||||
if "patient_data" in response_obj:
|
||||
cur.execute("UPDATE chat_history SET chat = %s, user_data = %s WHERE id = %s",
|
||||
(updated_chat, response_obj["patient_data"], chat_id))
|
||||
else:
|
||||
cur.execute("UPDATE chat_history SET chat = %s WHERE id = %s", (updated_chat, chat_id))
|
||||
conn.commit()
|
||||
logger.info(f"Chat history for chatId {chat_id} updated successfully")
|
||||
else:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur2:
|
||||
cur2.execute(
|
||||
"INSERT INTO chat_history (user_email, chat) VALUES (%s, %s) RETURNING id",
|
||||
(user_email, f"User: {query}\nBot: {best_answer}")
|
||||
)
|
||||
new_chat_id = cur2.fetchone()['id']
|
||||
conn.commit()
|
||||
chat_id = new_chat_id
|
||||
logger.info(f"New chat created with chatId: {chat_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating/creating chat history: {e}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
else:
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO chat_history (user_email, chat) VALUES (%s, %s) RETURNING id",
|
||||
(user_email, f"User: {query}\nBot: {best_answer}")
|
||||
)
|
||||
new_chat_id = cur.fetchone()['id']
|
||||
conn.commit()
|
||||
chat_id = new_chat_id
|
||||
logger.info(f"New chat created with chatId: {chat_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating new chat: {e}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
return jsonify({'response': {'best_answer': best_answer, 'model': response_obj.get("model", ""), 'chatId': chat_id}}), 200
|
||||
|
||||
@app.route('/api/save_user_data', methods=['POST'])
|
||||
def save_user_data():
|
||||
logger.info("Received request to save user data")
|
||||
data = request.get_json()
|
||||
chat_id = data.get('chatId')
|
||||
user_data = data.get('userData')
|
||||
if not chat_id or not user_data:
|
||||
return jsonify({'error': 'chatId and userData are required'}), 400
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("UPDATE chat_history SET user_data = %s WHERE id = %s", (user_data, chat_id))
|
||||
conn.commit()
|
||||
logger.info(f"User data for chatId {chat_id} updated successfully")
|
||||
return jsonify({'message': 'User data updated successfully'}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating user data: {e}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/chat_history', methods=['GET'])
|
||||
def get_chat_history():
|
||||
logger.info("Received request to get chat history")
|
||||
user_email = request.args.get('email')
|
||||
if not user_email:
|
||||
return jsonify({'error': 'User email is required'}), 400
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"SELECT id, chat, user_data, created_at FROM chat_history WHERE user_email = %s ORDER BY created_at DESC",
|
||||
(user_email,)
|
||||
)
|
||||
history = cur.fetchall()
|
||||
return jsonify({'history': history}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting chat history for {user_email}: {e}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/chat_history', methods=['DELETE'])
|
||||
def delete_chat():
|
||||
logger.info("Received request to delete chat")
|
||||
chat_id = request.args.get('id')
|
||||
if not chat_id:
|
||||
return jsonify({'error': 'Chat id is required'}), 400
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("DELETE FROM chat_history WHERE id = %s", (chat_id,))
|
||||
conn.commit()
|
||||
return jsonify({'message': 'Chat deleted successfully'}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting chat with chatId {chat_id}: {e}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/get_user_data', methods=['GET'])
|
||||
def get_user_data():
|
||||
chat_id = request.args.get('chatId')
|
||||
if not chat_id:
|
||||
return jsonify({'error': 'Chat id is required'}), 400
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("SELECT user_data FROM chat_history WHERE id = %s", (chat_id,))
|
||||
result = cur.fetchone()
|
||||
if result and result.get("user_data"):
|
||||
return jsonify({'user_data': result.get("user_data")}), 200
|
||||
else:
|
||||
return jsonify({'user_data': None}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving user data: {e}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/chat_history_detail', methods=['GET'])
|
||||
def chat_history_detail():
|
||||
logger.info("Received request to get chat details")
|
||||
chat_id = request.args.get('id')
|
||||
if not chat_id:
|
||||
return jsonify({'error': 'Chat id is required'}), 400
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("SELECT id, chat, user_data, created_at FROM chat_history WHERE id = %s", (chat_id,))
|
||||
chat = cur.fetchone()
|
||||
if not chat:
|
||||
return jsonify({'error': 'Chat not found'}), 404
|
||||
return jsonify({'chat': chat}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting chat details for chatId {chat_id}: {e}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
logger.info("Starting Flask application")
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
|
@ -1,56 +1,56 @@
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches
|
||||
|
||||
# Создание новой презентации
|
||||
prs = Presentation()
|
||||
|
||||
# Добавляем пустой слайд (layout с индексом 5 обычно является пустым)
|
||||
slide_layout = prs.slide_layouts[5]
|
||||
slide = prs.slides.add_slide(slide_layout)
|
||||
|
||||
# Определяем позицию и размер таблицы
|
||||
left = Inches(0.5)
|
||||
top = Inches(1.5)
|
||||
width = Inches(9)
|
||||
height = Inches(3)
|
||||
|
||||
# Количество строк: 1 заголовок + 2 строки с данными
|
||||
rows = 3
|
||||
cols = 4
|
||||
|
||||
# Добавляем таблицу на слайд
|
||||
table = slide.shapes.add_table(rows, cols, left, top, width, height).table
|
||||
|
||||
# Устанавливаем ширину столбцов (при необходимости можно настроить отдельно)
|
||||
table.columns[0].width = Inches(1.5) # Модель
|
||||
table.columns[1].width = Inches(1) # Оценка
|
||||
table.columns[2].width = Inches(4) # Текст
|
||||
table.columns[3].width = Inches(2.5) # Описание
|
||||
|
||||
# Заполняем заголовки
|
||||
table.cell(0, 0).text = "Модель"
|
||||
table.cell(0, 1).text = "Оценка"
|
||||
table.cell(0, 2).text = "Текст"
|
||||
table.cell(0, 3).text = "Описание"
|
||||
|
||||
# Данные для первого кандидата
|
||||
table.cell(1, 0).text = "Mistral Small"
|
||||
table.cell(1, 1).text = "9.0"
|
||||
table.cell(1, 2).text = (
|
||||
"Nevolnosť môže byť spôsobená rôznymi príčinami, ako sú napríklad gastrointestinálne problémy, infekcie, alebo vedľajšie účinky liekov. "
|
||||
"Pre ľudí, ktorí hľadajú voľnopredajný liek na nevolnosť, sú dostupné niekoľko možností: 1. Dimedrol (Dramin) – Antihistaminikum; "
|
||||
"2. Bismut subsalicylát (Pepto-Bismol); 3. Ginger (Zázvor); 4. Meclizin (Bonine). Pred použitím lieku je dôležité konzultovať s lekárom."
|
||||
)
|
||||
table.cell(1, 3).text = "Evaluation based on required criteria."
|
||||
|
||||
# Данные для второго кандидата
|
||||
table.cell(2, 0).text = "Mistral Large"
|
||||
table.cell(2, 1).text = "8.0"
|
||||
table.cell(2, 2).text = (
|
||||
"Pre nevolnosť sa dajú použiť niektoré voľne predávané lieky, ale dôležité je poradiť sa s lekárom, najmä ak má pacient 20 rokov. "
|
||||
"Medzi bežné voľne predávané lieky patria: 1. Dimenhydrinát; 2. Meclozín. Tieto lieky môžu spôsobiť spánkovosť a dávkovanie je nutné konzultovať."
|
||||
)
|
||||
table.cell(2, 3).text = "Evaluation based on required criteria."
|
||||
|
||||
# Сохраняем презентацию в файл
|
||||
prs.save("evaluation_table.pptx")
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches
|
||||
|
||||
# Создание новой презентации
|
||||
prs = Presentation()
|
||||
|
||||
# Добавляем пустой слайд (layout с индексом 5 обычно является пустым)
|
||||
slide_layout = prs.slide_layouts[5]
|
||||
slide = prs.slides.add_slide(slide_layout)
|
||||
|
||||
# Определяем позицию и размер таблицы
|
||||
left = Inches(0.5)
|
||||
top = Inches(1.5)
|
||||
width = Inches(9)
|
||||
height = Inches(3)
|
||||
|
||||
# Количество строк: 1 заголовок + 2 строки с данными
|
||||
rows = 3
|
||||
cols = 4
|
||||
|
||||
# Добавляем таблицу на слайд
|
||||
table = slide.shapes.add_table(rows, cols, left, top, width, height).table
|
||||
|
||||
# Устанавливаем ширину столбцов (при необходимости можно настроить отдельно)
|
||||
table.columns[0].width = Inches(1.5) # Модель
|
||||
table.columns[1].width = Inches(1) # Оценка
|
||||
table.columns[2].width = Inches(4) # Текст
|
||||
table.columns[3].width = Inches(2.5) # Описание
|
||||
|
||||
# Заполняем заголовки
|
||||
table.cell(0, 0).text = "Модель"
|
||||
table.cell(0, 1).text = "Оценка"
|
||||
table.cell(0, 2).text = "Текст"
|
||||
table.cell(0, 3).text = "Описание"
|
||||
|
||||
# Данные для первого кандидата
|
||||
table.cell(1, 0).text = "Mistral Small"
|
||||
table.cell(1, 1).text = "9.0"
|
||||
table.cell(1, 2).text = (
|
||||
"Nevolnosť môže byť spôsobená rôznymi príčinami, ako sú napríklad gastrointestinálne problémy, infekcie, alebo vedľajšie účinky liekov. "
|
||||
"Pre ľudí, ktorí hľadajú voľnopredajný liek na nevolnosť, sú dostupné niekoľko možností: 1. Dimedrol (Dramin) – Antihistaminikum; "
|
||||
"2. Bismut subsalicylát (Pepto-Bismol); 3. Ginger (Zázvor); 4. Meclizin (Bonine). Pred použitím lieku je dôležité konzultovať s lekárom."
|
||||
)
|
||||
table.cell(1, 3).text = "Evaluation based on required criteria."
|
||||
|
||||
# Данные для второго кандидата
|
||||
table.cell(2, 0).text = "Mistral Large"
|
||||
table.cell(2, 1).text = "8.0"
|
||||
table.cell(2, 2).text = (
|
||||
"Pre nevolnosť sa dajú použiť niektoré voľne predávané lieky, ale dôležité je poradiť sa s lekárom, najmä ak má pacient 20 rokov. "
|
||||
"Medzi bežné voľne predávané lieky patria: 1. Dimenhydrinát; 2. Meclozín. Tieto lieky môžu spôsobiť spánkovosť a dávkovanie je nutné konzultovať."
|
||||
)
|
||||
table.cell(2, 3).text = "Evaluation based on required criteria."
|
||||
|
||||
# Сохраняем презентацию в файл
|
||||
prs.save("evaluation_table.pptx")
|
||||
|
@ -1,34 +1,34 @@
|
||||
import requests
|
||||
|
||||
|
||||
API_TOKEN = "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"
|
||||
API_URL = "https://api-inference.huggingface.co/models/google/flan-t5-large"
|
||||
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {API_TOKEN}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
def query_flan_t5(prompt):
|
||||
payload = {
|
||||
"inputs": prompt,
|
||||
"parameters": {
|
||||
"max_length": 250,
|
||||
"do_sample": True,
|
||||
"temperature": 0.9,
|
||||
"top_p": 0.95,
|
||||
"top_k": 50
|
||||
}
|
||||
}
|
||||
response = requests.post(API_URL, headers=headers, json=payload)
|
||||
return response.json()
|
||||
|
||||
|
||||
prompt = "Ako sa máš? Daj odpoved v slovencine"
|
||||
result = query_flan_t5(prompt)
|
||||
|
||||
if isinstance(result, list) and len(result) > 0:
|
||||
print("Ответ от Flan-T5:", result[0]['generated_text'])
|
||||
else:
|
||||
import requests
|
||||
|
||||
|
||||
API_TOKEN = "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"
|
||||
API_URL = "https://api-inference.huggingface.co/models/google/flan-t5-large"
|
||||
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {API_TOKEN}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
def query_flan_t5(prompt):
|
||||
payload = {
|
||||
"inputs": prompt,
|
||||
"parameters": {
|
||||
"max_length": 250,
|
||||
"do_sample": True,
|
||||
"temperature": 0.9,
|
||||
"top_p": 0.95,
|
||||
"top_k": 50
|
||||
}
|
||||
}
|
||||
response = requests.post(API_URL, headers=headers, json=payload)
|
||||
return response.json()
|
||||
|
||||
|
||||
prompt = "Ako sa máš? Daj odpoved v slovencine"
|
||||
result = query_flan_t5(prompt)
|
||||
|
||||
if isinstance(result, list) and len(result) > 0:
|
||||
print("Ответ от Flan-T5:", result[0]['generated_text'])
|
||||
else:
|
||||
print("Ошибка при получении ответа:", result)
|
@ -1,47 +1,47 @@
|
||||
# import requests
|
||||
#
|
||||
# API_TOKEN = "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"
|
||||
# API_URL = "https://api-inference.huggingface.co/models/google/mt5-base"
|
||||
#
|
||||
# headers = {
|
||||
# "Authorization": f"Bearer {API_TOKEN}",
|
||||
# "Content-Type": "application/json"
|
||||
# }
|
||||
#
|
||||
# def query_mT5(prompt):
|
||||
# payload = {
|
||||
# "inputs": prompt,
|
||||
# "parameters": {
|
||||
# "max_length": 100,
|
||||
# "do_sample": True,
|
||||
# "temperature": 0.7
|
||||
# }
|
||||
# }
|
||||
# response = requests.post(API_URL, headers=headers, json=payload)
|
||||
# return response.json()
|
||||
#
|
||||
# # Пример использования
|
||||
# result = query_mT5("Aké sú účinné lieky na horúčku?")
|
||||
# print("Ответ от mT5:", result)
|
||||
|
||||
from transformers import AutoTokenizer, MT5ForConditionalGeneration
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained("google/mt5-small")
|
||||
model = MT5ForConditionalGeneration.from_pretrained("google/mt5-small")
|
||||
|
||||
# training
|
||||
input_ids = tokenizer("The <extra_id_0> walks in <extra_id_1> park", return_tensors="pt").input_ids
|
||||
labels = tokenizer("<extra_id_0> cute dog <extra_id_1> the <extra_id_2>", return_tensors="pt").input_ids
|
||||
outputs = model(input_ids=input_ids, labels=labels)
|
||||
loss = outputs.loss
|
||||
logits = outputs.logits
|
||||
|
||||
# inference
|
||||
|
||||
input_ids = tokenizer(
|
||||
"summarize: studies have shown that owning a dog is good for you", return_tensors="pt"
|
||||
).input_ids # Batch size 1
|
||||
outputs = model.generate(input_ids, max_new_tokens=50)
|
||||
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
|
||||
|
||||
# studies have shown that owning a dog is good for you.
|
||||
# import requests
|
||||
#
|
||||
# API_TOKEN = "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"
|
||||
# API_URL = "https://api-inference.huggingface.co/models/google/mt5-base"
|
||||
#
|
||||
# headers = {
|
||||
# "Authorization": f"Bearer {API_TOKEN}",
|
||||
# "Content-Type": "application/json"
|
||||
# }
|
||||
#
|
||||
# def query_mT5(prompt):
|
||||
# payload = {
|
||||
# "inputs": prompt,
|
||||
# "parameters": {
|
||||
# "max_length": 100,
|
||||
# "do_sample": True,
|
||||
# "temperature": 0.7
|
||||
# }
|
||||
# }
|
||||
# response = requests.post(API_URL, headers=headers, json=payload)
|
||||
# return response.json()
|
||||
#
|
||||
# # Пример использования
|
||||
# result = query_mT5("Aké sú účinné lieky na horúčku?")
|
||||
# print("Ответ от mT5:", result)
|
||||
|
||||
from transformers import AutoTokenizer, MT5ForConditionalGeneration
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained("google/mt5-small")
|
||||
model = MT5ForConditionalGeneration.from_pretrained("google/mt5-small")
|
||||
|
||||
# training
|
||||
input_ids = tokenizer("The <extra_id_0> walks in <extra_id_1> park", return_tensors="pt").input_ids
|
||||
labels = tokenizer("<extra_id_0> cute dog <extra_id_1> the <extra_id_2>", return_tensors="pt").input_ids
|
||||
outputs = model(input_ids=input_ids, labels=labels)
|
||||
loss = outputs.loss
|
||||
logits = outputs.logits
|
||||
|
||||
# inference
|
||||
|
||||
input_ids = tokenizer(
|
||||
"summarize: studies have shown that owning a dog is good for you", return_tensors="pt"
|
||||
).input_ids # Batch size 1
|
||||
outputs = model.generate(input_ids, max_new_tokens=50)
|
||||
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
|
||||
|
||||
# studies have shown that owning a dog is good for you.
|
||||
|
@ -1,18 +1,18 @@
|
||||
from sentence_transformers import SentenceTransformer, util
|
||||
|
||||
|
||||
model = SentenceTransformer("TUKE-DeutscheTelekom/slovakbert-skquad-mnlr")
|
||||
|
||||
|
||||
sentences = [
|
||||
"Prvý most cez Zlatý roh nechal vybudovať cisár Justinián I. V roku 1502 vypísal sultán Bajezid II. súťaž na nový most.",
|
||||
"V ktorom roku vznikol druhý drevený most cez záliv Zlatý roh?",
|
||||
"Aká je priemerná dĺžka života v Eritrei?"
|
||||
]
|
||||
|
||||
embeddings = model.encode(sentences)
|
||||
print("Shape of embeddings:", embeddings.shape)
|
||||
|
||||
|
||||
similarities = util.cos_sim(embeddings, embeddings)
|
||||
print("Similarity matrix:\n", similarities)
|
||||
from sentence_transformers import SentenceTransformer, util
|
||||
|
||||
|
||||
model = SentenceTransformer("TUKE-DeutscheTelekom/slovakbert-skquad-mnlr")
|
||||
|
||||
|
||||
sentences = [
|
||||
"Prvý most cez Zlatý roh nechal vybudovať cisár Justinián I. V roku 1502 vypísal sultán Bajezid II. súťaž na nový most.",
|
||||
"V ktorom roku vznikol druhý drevený most cez záliv Zlatý roh?",
|
||||
"Aká je priemerná dĺžka života v Eritrei?"
|
||||
]
|
||||
|
||||
embeddings = model.encode(sentences)
|
||||
print("Shape of embeddings:", embeddings.shape)
|
||||
|
||||
|
||||
similarities = util.cos_sim(embeddings, embeddings)
|
||||
print("Similarity matrix:\n", similarities)
|
||||
|
@ -1,73 +1,73 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
elasticsearch:
|
||||
build:
|
||||
context: ./elasticsearch
|
||||
dockerfile: Dockerfile
|
||||
container_name: elasticsearch
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- xpack.security.enabled=false
|
||||
ports:
|
||||
- "9200:9200"
|
||||
- "9300:9300"
|
||||
networks:
|
||||
- app-network
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-f", "http://localhost:9200/_cluster/health" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 40s
|
||||
|
||||
postgres:
|
||||
image: postgres:14
|
||||
container_name: postgres_db
|
||||
environment:
|
||||
- POSTGRES_DB=HealthAIDB
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=Oleg2005
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
backend:
|
||||
container_name: backend_container
|
||||
build:
|
||||
context: ./Backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- ELASTICSEARCH_HOST=http://elasticsearch:9200
|
||||
- DATABASE_HOST=postgres
|
||||
depends_on:
|
||||
elasticsearch:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_started
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
frontend:
|
||||
container_name: frontend_container
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "5173:5173"
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
elasticsearch:
|
||||
build:
|
||||
context: ./elasticsearch
|
||||
dockerfile: Dockerfile
|
||||
container_name: elasticsearch
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- xpack.security.enabled=false
|
||||
ports:
|
||||
- "9200:9200"
|
||||
- "9300:9300"
|
||||
networks:
|
||||
- app-network
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-f", "http://localhost:9200/_cluster/health" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 40s
|
||||
|
||||
postgres:
|
||||
image: postgres:14
|
||||
container_name: postgres_db
|
||||
environment:
|
||||
- POSTGRES_DB=HealthAIDB
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=Oleg2005
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
backend:
|
||||
container_name: backend_container
|
||||
build:
|
||||
context: ./Backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- ELASTICSEARCH_HOST=http://elasticsearch:9200
|
||||
- DATABASE_HOST=postgres
|
||||
depends_on:
|
||||
elasticsearch:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_started
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
frontend:
|
||||
container_name: frontend_container
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "5173:5173"
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
|
@ -1,15 +1,15 @@
|
||||
FROM docker.elastic.co/elasticsearch/elasticsearch:8.14.3
|
||||
|
||||
|
||||
ENV discovery.type=single-node
|
||||
ENV xpack.security.enabled=false
|
||||
|
||||
|
||||
COPY --chown=elasticsearch:elasticsearch data/ /usr/share/elasticsearch/data
|
||||
|
||||
|
||||
RUN chmod -R 0775 /usr/share/elasticsearch/data
|
||||
|
||||
|
||||
RUN find /usr/share/elasticsearch/data -type f -name "*.lock" -delete
|
||||
RUN rm -f /usr/share/elasticsearch/data/nodes/0/node.lock
|
||||
FROM docker.elastic.co/elasticsearch/elasticsearch:8.14.3
|
||||
|
||||
|
||||
ENV discovery.type=single-node
|
||||
ENV xpack.security.enabled=false
|
||||
|
||||
|
||||
COPY --chown=elasticsearch:elasticsearch data/ /usr/share/elasticsearch/data
|
||||
|
||||
|
||||
RUN chmod -R 0775 /usr/share/elasticsearch/data
|
||||
|
||||
|
||||
RUN find /usr/share/elasticsearch/data -type f -name "*.lock" -delete
|
||||
RUN rm -f /usr/share/elasticsearch/data/nodes/0/node.lock
|
||||
|
48
frontend/.gitignore
vendored
48
frontend/.gitignore
vendored
@ -1,24 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"hash": "c4d2d06d",
|
||||
"configHash": "9db50785",
|
||||
"lockfileHash": "e3b0c442",
|
||||
"browserHash": "efbdb5f3",
|
||||
"optimized": {},
|
||||
"chunks": {}
|
||||
{
|
||||
"hash": "c4d2d06d",
|
||||
"configHash": "9db50785",
|
||||
"lockfileHash": "e3b0c442",
|
||||
"browserHash": "efbdb5f3",
|
||||
"optimized": {},
|
||||
"chunks": {}
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
|
@ -1,26 +1,26 @@
|
||||
|
||||
FROM node:18-alpine
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
|
||||
RUN npm install
|
||||
|
||||
|
||||
COPY . .
|
||||
|
||||
|
||||
RUN npm run build
|
||||
|
||||
|
||||
RUN npm install -g serve
|
||||
|
||||
|
||||
EXPOSE 5173
|
||||
|
||||
|
||||
CMD ["serve", "-s", "dist", "-l", "5173"]
|
||||
|
||||
FROM node:18-alpine
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
|
||||
RUN npm install
|
||||
|
||||
|
||||
COPY . .
|
||||
|
||||
|
||||
RUN npm run build
|
||||
|
||||
|
||||
RUN npm install -g serve
|
||||
|
||||
|
||||
EXPOSE 5173
|
||||
|
||||
|
||||
CMD ["serve", "-s", "dist", "-l", "5173"]
|
||||
|
@ -1,28 +1,28 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
@ -1,12 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Health AI</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Health AI</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
9952
frontend/package-lock.json
generated
9952
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,46 +1,46 @@
|
||||
{
|
||||
"name": "coconuts",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@gsap/react": "^2.1.1",
|
||||
"@mui/icons-material": "^6.4.3",
|
||||
"@mui/material": "^6.1.5",
|
||||
"@react-oauth/google": "^0.12.1",
|
||||
"@reduxjs/toolkit": "^2.3.0",
|
||||
"appwrite": "^16.0.2",
|
||||
"final-form": "^4.20.10",
|
||||
"gsap": "^3.12.5",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-final-form": "^6.5.9",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-router-dom": "^6.27.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.13.0",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.13.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.13",
|
||||
"globals": "^15.11.0",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "^8.10.0",
|
||||
"vite": "^5.4.9"
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "coconuts",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@gsap/react": "^2.1.1",
|
||||
"@mui/icons-material": "^6.4.3",
|
||||
"@mui/material": "^6.1.5",
|
||||
"@react-oauth/google": "^0.12.1",
|
||||
"@reduxjs/toolkit": "^2.3.0",
|
||||
"appwrite": "^16.0.2",
|
||||
"final-form": "^4.20.10",
|
||||
"gsap": "^3.12.5",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-final-form": "^6.5.9",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-router-dom": "^6.27.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.13.0",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.13.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.13",
|
||||
"globals": "^15.11.0",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "^8.10.0",
|
||||
"vite": "^5.4.9"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
@ -1,45 +1,45 @@
|
||||
import { BrowserRouter as Router, Route, Routes, Outlet } from 'react-router-dom';
|
||||
import Navigation from './Components/Navigation';
|
||||
import {Home} from './pages/LandingPage';
|
||||
import RegistrationForm from "./Components/RegistrationForm";
|
||||
import LoginForm from "./Components/LoginForm";
|
||||
import ChatHistory from "./Components/ChatHistory";
|
||||
import HomePage from './pages/HomePage';
|
||||
import NewChatPage from "./Components/NewChatPage";
|
||||
import About from "./Components/About.tsx";
|
||||
import Contact from "./Components/Contact.tsx";
|
||||
import Profile from "./Components/Profile.tsx";
|
||||
|
||||
const Layout = () => (
|
||||
<div className="flex w-full h-screen dark:bg-slate-200">
|
||||
<Navigation isExpanded={false} />
|
||||
<div className="flex-grow p-3 h-full">
|
||||
<main className="h-full w-full border rounded-xl dark:bg-slate-100 shadow-xl">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/register" element={<RegistrationForm />} />
|
||||
<Route path="/login" element={<LoginForm />} />
|
||||
<Route path="/contact" element={<Contact />} />
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/dashboard" element={<Layout />}>
|
||||
<Route path="new-chat" element={<NewChatPage />} />
|
||||
<Route path="chat/:id" element={<HomePage />} />
|
||||
<Route path="history" element={<ChatHistory />} />
|
||||
<Route index element={<HomePage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
import { BrowserRouter as Router, Route, Routes, Outlet } from 'react-router-dom';
|
||||
import Navigation from './Components/Navigation';
|
||||
import {Home} from './pages/LandingPage';
|
||||
import RegistrationForm from "./Components/RegistrationForm";
|
||||
import LoginForm from "./Components/LoginForm";
|
||||
import ChatHistory from "./Components/ChatHistory";
|
||||
import HomePage from './pages/HomePage';
|
||||
import NewChatPage from "./Components/NewChatPage";
|
||||
import About from "./Components/About.tsx";
|
||||
import Contact from "./Components/Contact.tsx";
|
||||
import Profile from "./Components/Profile.tsx";
|
||||
|
||||
const Layout = () => (
|
||||
<div className="flex w-full h-screen dark:bg-slate-200">
|
||||
<Navigation isExpanded={false} />
|
||||
<div className="flex-grow p-3 h-full">
|
||||
<main className="h-full w-full border rounded-xl dark:bg-slate-100 shadow-xl">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/register" element={<RegistrationForm />} />
|
||||
<Route path="/login" element={<LoginForm />} />
|
||||
<Route path="/contact" element={<Contact />} />
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/dashboard" element={<Layout />}>
|
||||
<Route path="new-chat" element={<NewChatPage />} />
|
||||
<Route path="chat/:id" element={<HomePage />} />
|
||||
<Route path="history" element={<ChatHistory />} />
|
||||
<Route index element={<HomePage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
@ -1,109 +1,109 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Typography, Grid, Paper } from '@mui/material';
|
||||
import {Navbar} from '../pages/LandingPage';
|
||||
import MedicalServicesIcon from '@mui/icons-material/MedicalServices';
|
||||
import LocalHospitalIcon from '@mui/icons-material/LocalHospital';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
|
||||
const About: React.FC = () => {
|
||||
const [user, setUser] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
setUser(JSON.parse(storedUser));
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ background: 'linear-gradient(to right, #d0e7ff, #f0f8ff)', minHeight: '100vh', p: 4 }}>
|
||||
{/* Navigation bar */}
|
||||
<Navbar user={user} setUser={setUser} />
|
||||
|
||||
{/* Main content with top padding to account for fixed Navbar */}
|
||||
<Box sx={{ pt: '80px' }}>
|
||||
<Typography
|
||||
variant="h3"
|
||||
align="center"
|
||||
gutterBottom
|
||||
sx={{ fontWeight: 'bold', color: '#0d47a1' }}
|
||||
>
|
||||
About Health AI
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
align="center"
|
||||
gutterBottom
|
||||
sx={{ color: '#0d47a1', mb: 4 }}
|
||||
>
|
||||
Your Personal AI Assistant for Tailored Drug Recommendations
|
||||
</Typography>
|
||||
<Grid container spacing={4} justifyContent="center">
|
||||
{/* Project Information Card */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper elevation={3} sx={{ p: 3, backgroundColor: '#ffffff', borderRadius: '12px' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<MedicalServicesIcon sx={{ fontSize: 40, color: '#0d47a1', mr: 1 }} />
|
||||
<Typography variant="h5" sx={{ fontWeight: 'bold', color: '#0d47a1' }}>
|
||||
About the Project
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ color: '#424242', mb: 2 }}>
|
||||
Health AI is a cutting-edge application specializing in providing personalized drug recommendations and medication advice.
|
||||
Leveraging advanced AI models like Mistral and powerful search technologies such as Elasticsearch, our platform delivers accurate,
|
||||
context-aware suggestions for both over-the-counter and prescription medications.
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: '#424242' }}>
|
||||
Our backend utilizes modern technologies including Flask, PostgreSQL, and Google OAuth, ensuring robust security and reliable performance.
|
||||
We also use long-term conversational memory to continuously enhance our responses.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
{/* How It Works Card */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper elevation={3} sx={{ p: 3, backgroundColor: '#ffffff', borderRadius: '12px' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<LocalHospitalIcon sx={{ fontSize: 40, color: '#0d47a1', mr: 1 }} />
|
||||
<Typography variant="h5" sx={{ fontWeight: 'bold', color: '#0d47a1' }}>
|
||||
How It Works
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ color: '#424242', mb: 2 }}>
|
||||
Our system uses natural language processing to understand user queries and extract key details such as age, medical history,
|
||||
and medication type. It then employs vector search techniques to fetch the most relevant information from a comprehensive drug database,
|
||||
ensuring precise recommendations.
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: '#424242' }}>
|
||||
Health AI validates its responses to guarantee consistency and reliability, making it an innovative solution for personalized healthcare guidance.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
{/* Future Enhancements Card */}
|
||||
<Grid item xs={12}>
|
||||
<Paper elevation={3} sx={{ p: 3, backgroundColor: '#ffffff', borderRadius: '12px' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<CodeIcon sx={{ fontSize: 40, color: '#0d47a1', mr: 1 }} />
|
||||
<Typography variant="h5" sx={{ fontWeight: 'bold', color: '#0d47a1' }}>
|
||||
What's Next?
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ color: '#424242' }}>
|
||||
We are continuously improving Health AI by integrating additional data sources and refining our AI algorithms.
|
||||
Future enhancements include real-time drug interaction checks, comprehensive patient monitoring,
|
||||
and seamless integration with healthcare providers. Stay tuned for more exciting updates and features as we strive to make healthcare more accessible and efficient.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{/* Footer */}
|
||||
<Box sx={{ textAlign: 'center', mt: 6 }}>
|
||||
<Typography variant="body2" sx={{ color: '#424242' }}>
|
||||
© {new Date().getFullYear()} Health AI. All rights reserved.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Typography, Grid, Paper } from '@mui/material';
|
||||
import {Navbar} from '../pages/LandingPage';
|
||||
import MedicalServicesIcon from '@mui/icons-material/MedicalServices';
|
||||
import LocalHospitalIcon from '@mui/icons-material/LocalHospital';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
|
||||
const About: React.FC = () => {
|
||||
const [user, setUser] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
setUser(JSON.parse(storedUser));
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ background: 'linear-gradient(to right, #d0e7ff, #f0f8ff)', minHeight: '100vh', p: 4 }}>
|
||||
{/* Navigation bar */}
|
||||
<Navbar user={user} setUser={setUser} />
|
||||
|
||||
{/* Main content with top padding to account for fixed Navbar */}
|
||||
<Box sx={{ pt: '80px' }}>
|
||||
<Typography
|
||||
variant="h3"
|
||||
align="center"
|
||||
gutterBottom
|
||||
sx={{ fontWeight: 'bold', color: '#0d47a1' }}
|
||||
>
|
||||
About Health AI
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
align="center"
|
||||
gutterBottom
|
||||
sx={{ color: '#0d47a1', mb: 4 }}
|
||||
>
|
||||
Your Personal AI Assistant for Tailored Drug Recommendations
|
||||
</Typography>
|
||||
<Grid container spacing={4} justifyContent="center">
|
||||
{/* Project Information Card */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper elevation={3} sx={{ p: 3, backgroundColor: '#ffffff', borderRadius: '12px' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<MedicalServicesIcon sx={{ fontSize: 40, color: '#0d47a1', mr: 1 }} />
|
||||
<Typography variant="h5" sx={{ fontWeight: 'bold', color: '#0d47a1' }}>
|
||||
About the Project
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ color: '#424242', mb: 2 }}>
|
||||
Health AI is a cutting-edge application specializing in providing personalized drug recommendations and medication advice.
|
||||
Leveraging advanced AI models like Mistral and powerful search technologies such as Elasticsearch, our platform delivers accurate,
|
||||
context-aware suggestions for both over-the-counter and prescription medications.
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: '#424242' }}>
|
||||
Our backend utilizes modern technologies including Flask, PostgreSQL, and Google OAuth, ensuring robust security and reliable performance.
|
||||
We also use long-term conversational memory to continuously enhance our responses.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
{/* How It Works Card */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper elevation={3} sx={{ p: 3, backgroundColor: '#ffffff', borderRadius: '12px' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<LocalHospitalIcon sx={{ fontSize: 40, color: '#0d47a1', mr: 1 }} />
|
||||
<Typography variant="h5" sx={{ fontWeight: 'bold', color: '#0d47a1' }}>
|
||||
How It Works
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ color: '#424242', mb: 2 }}>
|
||||
Our system uses natural language processing to understand user queries and extract key details such as age, medical history,
|
||||
and medication type. It then employs vector search techniques to fetch the most relevant information from a comprehensive drug database,
|
||||
ensuring precise recommendations.
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: '#424242' }}>
|
||||
Health AI validates its responses to guarantee consistency and reliability, making it an innovative solution for personalized healthcare guidance.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
{/* Future Enhancements Card */}
|
||||
<Grid item xs={12}>
|
||||
<Paper elevation={3} sx={{ p: 3, backgroundColor: '#ffffff', borderRadius: '12px' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<CodeIcon sx={{ fontSize: 40, color: '#0d47a1', mr: 1 }} />
|
||||
<Typography variant="h5" sx={{ fontWeight: 'bold', color: '#0d47a1' }}>
|
||||
What's Next?
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ color: '#424242' }}>
|
||||
We are continuously improving Health AI by integrating additional data sources and refining our AI algorithms.
|
||||
Future enhancements include real-time drug interaction checks, comprehensive patient monitoring,
|
||||
and seamless integration with healthcare providers. Stay tuned for more exciting updates and features as we strive to make healthcare more accessible and efficient.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{/* Footer */}
|
||||
<Box sx={{ textAlign: 'center', mt: 6 }}>
|
||||
<Typography variant="body2" sx={{ color: '#424242' }}>
|
||||
© {new Date().getFullYear()} Health AI. All rights reserved.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
|
@ -1,57 +1,57 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
|
||||
interface ChatHistoryItem {
|
||||
id: number;
|
||||
chat: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const ChatDetails: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const location = useLocation();
|
||||
const [chat, setChat] = useState<ChatHistoryItem | null>(location.state?.chat || null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chat && id) {
|
||||
fetch(`http://localhost:5000/api/chat_history_detail?id=${encodeURIComponent(id)}`)
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error('Chat not found');
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
setError(data.error);
|
||||
} else {
|
||||
setChat(data.chat);
|
||||
}
|
||||
})
|
||||
.catch((err) => setError(err.message));
|
||||
}
|
||||
}, [id, chat]);
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error}</div>;
|
||||
}
|
||||
|
||||
if (!chat) {
|
||||
return <div>Loading chat details...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h1>Chat Details</h1>
|
||||
<div style={{ border: '1px solid #ccc', padding: '10px' }}>
|
||||
{chat.chat.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
<small>{new Date(chat.created_at).toLocaleString()}</small>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatDetails;
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
|
||||
interface ChatHistoryItem {
|
||||
id: number;
|
||||
chat: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const ChatDetails: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const location = useLocation();
|
||||
const [chat, setChat] = useState<ChatHistoryItem | null>(location.state?.chat || null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chat && id) {
|
||||
fetch(`http://localhost:5000/api/chat_history_detail?id=${encodeURIComponent(id)}`)
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error('Chat not found');
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
setError(data.error);
|
||||
} else {
|
||||
setChat(data.chat);
|
||||
}
|
||||
})
|
||||
.catch((err) => setError(err.message));
|
||||
}
|
||||
}, [id, chat]);
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error}</div>;
|
||||
}
|
||||
|
||||
if (!chat) {
|
||||
return <div>Loading chat details...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h1>Chat Details</h1>
|
||||
<div style={{ border: '1px solid #ccc', padding: '10px' }}>
|
||||
{chat.chat.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
<small>{new Date(chat.created_at).toLocaleString()}</small>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatDetails;
|
||||
|
@ -1,158 +1,158 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Box, Typography, Paper, IconButton } from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
|
||||
interface ChatHistoryItem {
|
||||
id: number;
|
||||
chat: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const ChatHistory: React.FC = () => {
|
||||
const [history, setHistory] = useState<ChatHistoryItem[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
const user = JSON.parse(storedUser);
|
||||
const email = user.email;
|
||||
fetch(`http://localhost:5000/api/chat_history?email=${encodeURIComponent(email)}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
setError(data.error);
|
||||
} else {
|
||||
setHistory(data.history);
|
||||
}
|
||||
})
|
||||
.catch(() => setError('Error fetching chat history'));
|
||||
} else {
|
||||
setError('User not logged in');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleClick = (item: ChatHistoryItem) => {
|
||||
navigate(`/dashboard/chat/${item.id}`, { state: { selectedChat: item } });
|
||||
};
|
||||
|
||||
const handleDelete = (chatId: number) => {
|
||||
if (window.confirm('Are you sure that you want to delete that chat?')) {
|
||||
fetch(`http://localhost:5000/api/chat_history?id=${chatId}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
setError(data.error);
|
||||
} else {
|
||||
setHistory(history.filter((chat) => chat.id !== chatId));
|
||||
}
|
||||
})
|
||||
.catch(() => setError('Error deleting chat'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100vh',
|
||||
overflowY: 'auto',
|
||||
background: '#f5f5f5',
|
||||
boxSizing: 'border-box',
|
||||
p: 3,
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
'-ms-overflow-style': 'none',
|
||||
'scrollbarWidth': 'none',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
mb: 3,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
color: '#0d47a1',
|
||||
}}
|
||||
>
|
||||
Chat History
|
||||
</Typography>
|
||||
{error ? (
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ color: 'error.main', textAlign: 'center' }}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
) : (
|
||||
<Box sx={{ maxWidth: '800px', mx: 'auto' }}>
|
||||
{history.length === 0 ? (
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ textAlign: 'center', color: '#424242' }}
|
||||
>
|
||||
No chat history found.
|
||||
</Typography>
|
||||
) : (
|
||||
history.map((item) => {
|
||||
const lines = item.chat.split("\n");
|
||||
let firstUserMessage = lines[0];
|
||||
if (firstUserMessage.startsWith("User:")) {
|
||||
firstUserMessage = firstUserMessage.replace("User:", "").trim();
|
||||
}
|
||||
return (
|
||||
<Paper
|
||||
key={item.id}
|
||||
sx={{
|
||||
p: 2,
|
||||
mb: 2,
|
||||
cursor: 'pointer',
|
||||
transition: 'box-shadow 0.3s ease',
|
||||
'&:hover': { boxShadow: 6 },
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{ flex: 1, display: 'flex', flexDirection: 'column' }}
|
||||
onClick={() => handleClick(item)}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ fontWeight: 'bold', color: '#0d47a1' }}
|
||||
>
|
||||
{firstUserMessage}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ color: '#757575' }}
|
||||
>
|
||||
{new Date(item.created_at).toLocaleString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(item.id);
|
||||
}}
|
||||
sx={{ color: '#d32f2f' }}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Paper>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatHistory;
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Box, Typography, Paper, IconButton } from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
|
||||
interface ChatHistoryItem {
|
||||
id: number;
|
||||
chat: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const ChatHistory: React.FC = () => {
|
||||
const [history, setHistory] = useState<ChatHistoryItem[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
const user = JSON.parse(storedUser);
|
||||
const email = user.email;
|
||||
fetch(`http://localhost:5000/api/chat_history?email=${encodeURIComponent(email)}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
setError(data.error);
|
||||
} else {
|
||||
setHistory(data.history);
|
||||
}
|
||||
})
|
||||
.catch(() => setError('Error fetching chat history'));
|
||||
} else {
|
||||
setError('User not logged in');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleClick = (item: ChatHistoryItem) => {
|
||||
navigate(`/dashboard/chat/${item.id}`, { state: { selectedChat: item } });
|
||||
};
|
||||
|
||||
const handleDelete = (chatId: number) => {
|
||||
if (window.confirm('Are you sure that you want to delete that chat?')) {
|
||||
fetch(`http://localhost:5000/api/chat_history?id=${chatId}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
setError(data.error);
|
||||
} else {
|
||||
setHistory(history.filter((chat) => chat.id !== chatId));
|
||||
}
|
||||
})
|
||||
.catch(() => setError('Error deleting chat'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100vh',
|
||||
overflowY: 'auto',
|
||||
background: '#f5f5f5',
|
||||
boxSizing: 'border-box',
|
||||
p: 3,
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
'-ms-overflow-style': 'none',
|
||||
'scrollbarWidth': 'none',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
mb: 3,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
color: '#0d47a1',
|
||||
}}
|
||||
>
|
||||
Chat History
|
||||
</Typography>
|
||||
{error ? (
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ color: 'error.main', textAlign: 'center' }}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
) : (
|
||||
<Box sx={{ maxWidth: '800px', mx: 'auto' }}>
|
||||
{history.length === 0 ? (
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ textAlign: 'center', color: '#424242' }}
|
||||
>
|
||||
No chat history found.
|
||||
</Typography>
|
||||
) : (
|
||||
history.map((item) => {
|
||||
const lines = item.chat.split("\n");
|
||||
let firstUserMessage = lines[0];
|
||||
if (firstUserMessage.startsWith("User:")) {
|
||||
firstUserMessage = firstUserMessage.replace("User:", "").trim();
|
||||
}
|
||||
return (
|
||||
<Paper
|
||||
key={item.id}
|
||||
sx={{
|
||||
p: 2,
|
||||
mb: 2,
|
||||
cursor: 'pointer',
|
||||
transition: 'box-shadow 0.3s ease',
|
||||
'&:hover': { boxShadow: 6 },
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{ flex: 1, display: 'flex', flexDirection: 'column' }}
|
||||
onClick={() => handleClick(item)}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ fontWeight: 'bold', color: '#0d47a1' }}
|
||||
>
|
||||
{firstUserMessage}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ color: '#757575' }}
|
||||
>
|
||||
{new Date(item.created_at).toLocaleString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(item.id);
|
||||
}}
|
||||
sx={{ color: '#d32f2f' }}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Paper>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatHistory;
|
||||
|
@ -1,129 +1,129 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Typography, Paper, Grid } from '@mui/material';
|
||||
import {Navbar} from '../pages/LandingPage';
|
||||
import SchoolIcon from '@mui/icons-material/School';
|
||||
import DeveloperModeIcon from '@mui/icons-material/DeveloperMode';
|
||||
import EmailIcon from '@mui/icons-material/Email';
|
||||
|
||||
const Contact: React.FC = () => {
|
||||
const [user, setUser] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
setUser(JSON.parse(storedUser));
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
background: 'linear-gradient(to right, #d0e7ff, #f0f8ff)',
|
||||
minHeight: '100vh',
|
||||
p: 4,
|
||||
}}
|
||||
>
|
||||
{/* Navbar with navigation links */}
|
||||
<Navbar user={user} setUser={setUser} />
|
||||
|
||||
{/* Main content with spacing for fixed Navbar */}
|
||||
<Box sx={{ pt: '80px', maxWidth: '800px', mx: 'auto' }}>
|
||||
<Paper
|
||||
elevation={4}
|
||||
sx={{
|
||||
p: 4,
|
||||
borderRadius: '16px',
|
||||
backgroundColor: '#ffffff',
|
||||
boxShadow: '0 4px 20px rgba(0,0,0,0.1)',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h4"
|
||||
align="center"
|
||||
sx={{ fontWeight: 'bold', color: '#0d47a1', mb: 4 }}
|
||||
>
|
||||
Contact
|
||||
</Typography>
|
||||
<Grid container spacing={4}>
|
||||
{/* University Info */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: '#e3f2fd',
|
||||
}}
|
||||
>
|
||||
<SchoolIcon sx={{ fontSize: 40, color: '#0d47a1', mr: 2 }} />
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#0d47a1' }}>
|
||||
Technical University of Košice
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#424242' }}>
|
||||
KEMT Department
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Grid>
|
||||
{/* Developer Info */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: '#e8f5e9',
|
||||
}}
|
||||
>
|
||||
<DeveloperModeIcon sx={{ fontSize: 40, color: '#0d47a1', mr: 2 }} />
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#0d47a1' }}>
|
||||
Developer
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#424242' }}>
|
||||
oleh.poiasnik@student.tuke.sk
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Grid>
|
||||
{/* Additional Contact Option */}
|
||||
<Grid item xs={12}>
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: '#fff3e0',
|
||||
}}
|
||||
>
|
||||
<EmailIcon sx={{ fontSize: 40, color: '#0d47a1', mr: 2 }} />
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#0d47a1' }}>
|
||||
Email Us
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#424242' }}>
|
||||
For any inquiries or further information about Health AI, please get in touch!
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Box sx={{ textAlign: 'center', mt: 4 }}>
|
||||
<Typography variant="body2" sx={{ color: '#424242' }}>
|
||||
© {new Date().getFullYear()} Health AI. All rights reserved.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Contact;
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Typography, Paper, Grid } from '@mui/material';
|
||||
import {Navbar} from '../pages/LandingPage';
|
||||
import SchoolIcon from '@mui/icons-material/School';
|
||||
import DeveloperModeIcon from '@mui/icons-material/DeveloperMode';
|
||||
import EmailIcon from '@mui/icons-material/Email';
|
||||
|
||||
const Contact: React.FC = () => {
|
||||
const [user, setUser] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
setUser(JSON.parse(storedUser));
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
background: 'linear-gradient(to right, #d0e7ff, #f0f8ff)',
|
||||
minHeight: '100vh',
|
||||
p: 4,
|
||||
}}
|
||||
>
|
||||
{/* Navbar with navigation links */}
|
||||
<Navbar user={user} setUser={setUser} />
|
||||
|
||||
{/* Main content with spacing for fixed Navbar */}
|
||||
<Box sx={{ pt: '80px', maxWidth: '800px', mx: 'auto' }}>
|
||||
<Paper
|
||||
elevation={4}
|
||||
sx={{
|
||||
p: 4,
|
||||
borderRadius: '16px',
|
||||
backgroundColor: '#ffffff',
|
||||
boxShadow: '0 4px 20px rgba(0,0,0,0.1)',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h4"
|
||||
align="center"
|
||||
sx={{ fontWeight: 'bold', color: '#0d47a1', mb: 4 }}
|
||||
>
|
||||
Contact
|
||||
</Typography>
|
||||
<Grid container spacing={4}>
|
||||
{/* University Info */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: '#e3f2fd',
|
||||
}}
|
||||
>
|
||||
<SchoolIcon sx={{ fontSize: 40, color: '#0d47a1', mr: 2 }} />
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#0d47a1' }}>
|
||||
Technical University of Košice
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#424242' }}>
|
||||
KEMT Department
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Grid>
|
||||
{/* Developer Info */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: '#e8f5e9',
|
||||
}}
|
||||
>
|
||||
<DeveloperModeIcon sx={{ fontSize: 40, color: '#0d47a1', mr: 2 }} />
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#0d47a1' }}>
|
||||
Developer
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#424242' }}>
|
||||
oleh.poiasnik@student.tuke.sk
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Grid>
|
||||
{/* Additional Contact Option */}
|
||||
<Grid item xs={12}>
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: '#fff3e0',
|
||||
}}
|
||||
>
|
||||
<EmailIcon sx={{ fontSize: 40, color: '#0d47a1', mr: 2 }} />
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#0d47a1' }}>
|
||||
Email Us
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#424242' }}>
|
||||
For any inquiries or further information about Health AI, please get in touch!
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Box sx={{ textAlign: 'center', mt: 4 }}>
|
||||
<Typography variant="body2" sx={{ color: '#424242' }}>
|
||||
© {new Date().getFullYear()} Health AI. All rights reserved.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Contact;
|
||||
|
@ -1,190 +1,190 @@
|
||||
import React from 'react';
|
||||
import { GoogleLogin, GoogleOAuthProvider } from '@react-oauth/google';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
const CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com";
|
||||
|
||||
const LoginFormContent: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const emailElement = document.getElementById('email') as HTMLInputElement | null;
|
||||
const passwordElement = document.getElementById('password') as HTMLInputElement | null;
|
||||
|
||||
if (!emailElement || !passwordElement) {
|
||||
console.error('One or many inputs are missing');
|
||||
return;
|
||||
}
|
||||
|
||||
const email = emailElement.value;
|
||||
const password = passwordElement.value;
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:5000/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Login successful:', data.message);
|
||||
const loggedInUser = {
|
||||
name: data.user.name,
|
||||
email: data.user.email,
|
||||
picture: data.user.picture || 'https://via.placeholder.com/150',
|
||||
};
|
||||
localStorage.setItem('user', JSON.stringify(loggedInUser));
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
console.error('Error:', data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loginning:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleLoginSuccess = async (response: any) => {
|
||||
try {
|
||||
const res = await fetch('http://localhost:5000/api/verify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: response.credential }),
|
||||
});
|
||||
const data = await res.json();
|
||||
const loggedInUser = {
|
||||
name: data.user.name,
|
||||
email: data.user.email,
|
||||
picture: data.user.picture || 'https://via.placeholder.com/150',
|
||||
};
|
||||
localStorage.setItem('user', JSON.stringify(loggedInUser));
|
||||
navigate('/dashboard');
|
||||
} catch (error) {
|
||||
console.error('Error token verification:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleLoginError = () => {
|
||||
console.error('Error auth through Google');
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '16px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="login-card"
|
||||
style={{
|
||||
maxWidth: '400px',
|
||||
width: '100%',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||
backgroundColor: '#fff',
|
||||
padding: '24px',
|
||||
}}
|
||||
>
|
||||
<h2 style={{ textAlign: 'center', fontWeight: 'bold', color: '#333', marginBottom: '16px' }}>
|
||||
Sign In
|
||||
</h2>
|
||||
<form noValidate autoComplete="off" onSubmit={handleSubmit}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label htmlFor="email" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label htmlFor="password" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
backgroundColor: '#007bff',
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
marginBottom: '16px',
|
||||
color: '#555',
|
||||
fontSize: '14px',
|
||||
borderBottom: '1px solid #ccc',
|
||||
lineHeight: '0.1em',
|
||||
margin: '10px 0 20px',
|
||||
}}
|
||||
>
|
||||
<span style={{ background: '#fff', padding: '0 10px' }}>OR</span>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', marginBottom: '16px' }}>
|
||||
<GoogleLogin
|
||||
onSuccess={handleGoogleLoginSuccess}
|
||||
onError={handleGoogleLoginError}
|
||||
size="large"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
<p style={{ textAlign: 'center', color: '#555', marginTop: '16px' }}>
|
||||
Don't have an account?{' '}
|
||||
<Link to="/register" style={{ color: '#007bff', textDecoration: 'none' }}>
|
||||
Sign Up
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LoginForm: React.FC = () => {
|
||||
return (
|
||||
<GoogleOAuthProvider clientId={CLIENT_ID}>
|
||||
<LoginFormContent />
|
||||
</GoogleOAuthProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
import React from 'react';
|
||||
import { GoogleLogin, GoogleOAuthProvider } from '@react-oauth/google';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
const CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com";
|
||||
|
||||
const LoginFormContent: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const emailElement = document.getElementById('email') as HTMLInputElement | null;
|
||||
const passwordElement = document.getElementById('password') as HTMLInputElement | null;
|
||||
|
||||
if (!emailElement || !passwordElement) {
|
||||
console.error('One or many inputs are missing');
|
||||
return;
|
||||
}
|
||||
|
||||
const email = emailElement.value;
|
||||
const password = passwordElement.value;
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:5000/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Login successful:', data.message);
|
||||
const loggedInUser = {
|
||||
name: data.user.name,
|
||||
email: data.user.email,
|
||||
picture: data.user.picture || 'https://via.placeholder.com/150',
|
||||
};
|
||||
localStorage.setItem('user', JSON.stringify(loggedInUser));
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
console.error('Error:', data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loginning:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleLoginSuccess = async (response: any) => {
|
||||
try {
|
||||
const res = await fetch('http://localhost:5000/api/verify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: response.credential }),
|
||||
});
|
||||
const data = await res.json();
|
||||
const loggedInUser = {
|
||||
name: data.user.name,
|
||||
email: data.user.email,
|
||||
picture: data.user.picture || 'https://via.placeholder.com/150',
|
||||
};
|
||||
localStorage.setItem('user', JSON.stringify(loggedInUser));
|
||||
navigate('/dashboard');
|
||||
} catch (error) {
|
||||
console.error('Error token verification:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleLoginError = () => {
|
||||
console.error('Error auth through Google');
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '16px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="login-card"
|
||||
style={{
|
||||
maxWidth: '400px',
|
||||
width: '100%',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||
backgroundColor: '#fff',
|
||||
padding: '24px',
|
||||
}}
|
||||
>
|
||||
<h2 style={{ textAlign: 'center', fontWeight: 'bold', color: '#333', marginBottom: '16px' }}>
|
||||
Sign In
|
||||
</h2>
|
||||
<form noValidate autoComplete="off" onSubmit={handleSubmit}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label htmlFor="email" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label htmlFor="password" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
backgroundColor: '#007bff',
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
marginBottom: '16px',
|
||||
color: '#555',
|
||||
fontSize: '14px',
|
||||
borderBottom: '1px solid #ccc',
|
||||
lineHeight: '0.1em',
|
||||
margin: '10px 0 20px',
|
||||
}}
|
||||
>
|
||||
<span style={{ background: '#fff', padding: '0 10px' }}>OR</span>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', marginBottom: '16px' }}>
|
||||
<GoogleLogin
|
||||
onSuccess={handleGoogleLoginSuccess}
|
||||
onError={handleGoogleLoginError}
|
||||
size="large"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
<p style={{ textAlign: 'center', color: '#555', marginTop: '16px' }}>
|
||||
Don't have an account?{' '}
|
||||
<Link to="/register" style={{ color: '#007bff', textDecoration: 'none' }}>
|
||||
Sign Up
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LoginForm: React.FC = () => {
|
||||
return (
|
||||
<GoogleOAuthProvider clientId={CLIENT_ID}>
|
||||
<LoginFormContent />
|
||||
</GoogleOAuthProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
|
@ -1,146 +1,146 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import { MdAddCircleOutline } from "react-icons/md";
|
||||
import { GoHistory } from "react-icons/go";
|
||||
import { CgLogIn } from "react-icons/cg";
|
||||
import BackImage from '../assets/smallheadicon.png';
|
||||
|
||||
export interface NavigationItem {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
const NavigationItems: NavigationItem[] = [
|
||||
{
|
||||
title: 'New Chat',
|
||||
link: '/dashboard/new-chat',
|
||||
icon: <MdAddCircleOutline size={30} />
|
||||
},
|
||||
{
|
||||
title: 'History',
|
||||
link: '/dashboard/history',
|
||||
icon: <GoHistory size={25} />
|
||||
}
|
||||
];
|
||||
|
||||
interface NavigationProps {
|
||||
isExpanded: boolean;
|
||||
}
|
||||
|
||||
const Navigation = ({ isExpanded = false }: NavigationProps) => {
|
||||
const [theme, setTheme] = useState<'dark' | 'light'>('light');
|
||||
|
||||
useEffect(() => {
|
||||
if (window.matchMedia('(prefers-color-scheme:dark)').matches) {
|
||||
setTheme('dark');
|
||||
} else {
|
||||
setTheme('light');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (theme === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
|
||||
|
||||
const [user, setUser] = useState<any>(null);
|
||||
useEffect(() => {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
setUser(JSON.parse(storedUser));
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='h-full p-3 w-fit'>
|
||||
<div className='h-full rounded-xl border flex flex-col px-1 justify-between py-2 items-center dark:bg-slate-300 shadow-xl'>
|
||||
<div className='flex flex-col items-start gap-12'>
|
||||
<Link to='/' className='w-full flex items-center justify-center'>
|
||||
<IconButton sx={{ width: 40, height: 40 }}>
|
||||
<img src={BackImage} width={25} alt="Back" />
|
||||
</IconButton>
|
||||
{isExpanded && (
|
||||
<p className='text-2xl font-semibold text-dark-blue flex items-center'>
|
||||
Health AI
|
||||
</p>
|
||||
)}
|
||||
</Link>
|
||||
<div className='flex flex-col p-1 gap-5 items-center'>
|
||||
{NavigationItems.map((item) => (
|
||||
<Link
|
||||
key={item.link}
|
||||
to={item.link}
|
||||
className='flex gap-2 items-center w-full'
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 2,
|
||||
backgroundColor: '#FFFFFF',
|
||||
...(theme === 'dark' && {
|
||||
'&:hover': {
|
||||
backgroundColor: '#eef3f4',
|
||||
borderColor: '#0062cc',
|
||||
boxShadow: 'none',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
</IconButton>
|
||||
{isExpanded && item.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Link to={user ? '/profile' : '/login'} className="flex items-center">
|
||||
<IconButton
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 2,
|
||||
backgroundColor: '#FFFFFF',
|
||||
}}
|
||||
>
|
||||
{user ? (
|
||||
<Avatar alt={user.name} src={user.picture} />
|
||||
) : (
|
||||
<CgLogIn size={24} />
|
||||
)}
|
||||
</IconButton>
|
||||
</Link>
|
||||
{/*<button onClick={handleThemeSwitch} className='flex items-center gap-2'>*/}
|
||||
{/* <IconButton*/}
|
||||
{/* sx={{*/}
|
||||
{/* width: 40,*/}
|
||||
{/* height: 40,*/}
|
||||
{/* borderRadius: 2,*/}
|
||||
{/* background: theme === 'dark' ? 'white' : 'initial',*/}
|
||||
{/* '&:focus-visible': {*/}
|
||||
{/* outline: '2px solid blue',*/}
|
||||
{/* outlineOffset: '0px',*/}
|
||||
{/* borderRadius: '4px',*/}
|
||||
{/* },*/}
|
||||
{/* }}*/}
|
||||
{/* >*/}
|
||||
{/* {theme === 'light' ? <CiLight size={30} /> : <MdOutlineDarkMode size={30} />}*/}
|
||||
{/* </IconButton>*/}
|
||||
{/* {isExpanded && (theme === 'light' ? 'Light mode' : 'Dark mode')}*/}
|
||||
{/*</button>*/}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navigation;
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import { MdAddCircleOutline } from "react-icons/md";
|
||||
import { GoHistory } from "react-icons/go";
|
||||
import { CgLogIn } from "react-icons/cg";
|
||||
import BackImage from '../assets/smallheadicon.png';
|
||||
|
||||
export interface NavigationItem {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
const NavigationItems: NavigationItem[] = [
|
||||
{
|
||||
title: 'New Chat',
|
||||
link: '/dashboard/new-chat',
|
||||
icon: <MdAddCircleOutline size={30} />
|
||||
},
|
||||
{
|
||||
title: 'History',
|
||||
link: '/dashboard/history',
|
||||
icon: <GoHistory size={25} />
|
||||
}
|
||||
];
|
||||
|
||||
interface NavigationProps {
|
||||
isExpanded: boolean;
|
||||
}
|
||||
|
||||
const Navigation = ({ isExpanded = false }: NavigationProps) => {
|
||||
const [theme, setTheme] = useState<'dark' | 'light'>('light');
|
||||
|
||||
useEffect(() => {
|
||||
if (window.matchMedia('(prefers-color-scheme:dark)').matches) {
|
||||
setTheme('dark');
|
||||
} else {
|
||||
setTheme('light');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (theme === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
|
||||
|
||||
const [user, setUser] = useState<any>(null);
|
||||
useEffect(() => {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
setUser(JSON.parse(storedUser));
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='h-full p-3 w-fit'>
|
||||
<div className='h-full rounded-xl border flex flex-col px-1 justify-between py-2 items-center dark:bg-slate-300 shadow-xl'>
|
||||
<div className='flex flex-col items-start gap-12'>
|
||||
<Link to='/' className='w-full flex items-center justify-center'>
|
||||
<IconButton sx={{ width: 40, height: 40 }}>
|
||||
<img src={BackImage} width={25} alt="Back" />
|
||||
</IconButton>
|
||||
{isExpanded && (
|
||||
<p className='text-2xl font-semibold text-dark-blue flex items-center'>
|
||||
Health AI
|
||||
</p>
|
||||
)}
|
||||
</Link>
|
||||
<div className='flex flex-col p-1 gap-5 items-center'>
|
||||
{NavigationItems.map((item) => (
|
||||
<Link
|
||||
key={item.link}
|
||||
to={item.link}
|
||||
className='flex gap-2 items-center w-full'
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 2,
|
||||
backgroundColor: '#FFFFFF',
|
||||
...(theme === 'dark' && {
|
||||
'&:hover': {
|
||||
backgroundColor: '#eef3f4',
|
||||
borderColor: '#0062cc',
|
||||
boxShadow: 'none',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
</IconButton>
|
||||
{isExpanded && item.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Link to={user ? '/profile' : '/login'} className="flex items-center">
|
||||
<IconButton
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 2,
|
||||
backgroundColor: '#FFFFFF',
|
||||
}}
|
||||
>
|
||||
{user ? (
|
||||
<Avatar alt={user.name} src={user.picture} />
|
||||
) : (
|
||||
<CgLogIn size={24} />
|
||||
)}
|
||||
</IconButton>
|
||||
</Link>
|
||||
{/*<button onClick={handleThemeSwitch} className='flex items-center gap-2'>*/}
|
||||
{/* <IconButton*/}
|
||||
{/* sx={{*/}
|
||||
{/* width: 40,*/}
|
||||
{/* height: 40,*/}
|
||||
{/* borderRadius: 2,*/}
|
||||
{/* background: theme === 'dark' ? 'white' : 'initial',*/}
|
||||
{/* '&:focus-visible': {*/}
|
||||
{/* outline: '2px solid blue',*/}
|
||||
{/* outlineOffset: '0px',*/}
|
||||
{/* borderRadius: '4px',*/}
|
||||
{/* },*/}
|
||||
{/* }}*/}
|
||||
{/* >*/}
|
||||
{/* {theme === 'light' ? <CiLight size={30} /> : <MdOutlineDarkMode size={30} />}*/}
|
||||
{/* </IconButton>*/}
|
||||
{/* {isExpanded && (theme === 'light' ? 'Light mode' : 'Dark mode')}*/}
|
||||
{/*</button>*/}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navigation;
|
||||
|
@ -1,186 +1,186 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import gsap from 'gsap';
|
||||
import { useGSAP } from '@gsap/react';
|
||||
import { useLazySendChatQuestionQuery } from '../store/api/chatApi';
|
||||
import callCenterIcon from '../assets/call-center.png';
|
||||
|
||||
interface ChatMessage {
|
||||
sender: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const NewChatPage: React.FC = () => {
|
||||
const [sendChatQuestion, { isLoading, isFetching }] = useLazySendChatQuestionQuery();
|
||||
const [message, setMessage] = useState<string>('');
|
||||
const [chatHistory, setChatHistory] = useState<ChatMessage[]>([]);
|
||||
const navigate = useNavigate();
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [chatHistory, isLoading, isFetching]);
|
||||
|
||||
useEffect(() => {
|
||||
setChatHistory([]);
|
||||
}, []);
|
||||
|
||||
async function onSubmit() {
|
||||
if (!message.trim()) return;
|
||||
|
||||
const newUserMessage: ChatMessage = { sender: 'User', text: message };
|
||||
const updatedHistory = [...chatHistory, newUserMessage];
|
||||
setChatHistory(updatedHistory);
|
||||
const userMessage = message;
|
||||
setMessage('');
|
||||
|
||||
const storedUser = localStorage.getItem('user');
|
||||
const email = storedUser ? JSON.parse(storedUser).email : '';
|
||||
|
||||
const question = { query: userMessage, email };
|
||||
|
||||
try {
|
||||
const res = await sendChatQuestion(question).unwrap();
|
||||
console.log('Response from server:', res);
|
||||
|
||||
let bestAnswer = res.response.best_answer;
|
||||
if (typeof bestAnswer !== 'string') {
|
||||
bestAnswer = String(bestAnswer);
|
||||
}
|
||||
bestAnswer = bestAnswer.trim();
|
||||
|
||||
const newAssistantMessage: ChatMessage = { sender: 'Assistant', text: bestAnswer };
|
||||
const newUpdatedHistory = [...updatedHistory, newAssistantMessage];
|
||||
setChatHistory(newUpdatedHistory);
|
||||
|
||||
if (res.response.chatId) {
|
||||
const chatString = newUpdatedHistory
|
||||
.map(msg => (msg.sender === 'User' ? 'User: ' : 'Bot: ') + msg.text)
|
||||
.join('\n');
|
||||
navigate(`/dashboard/chat/${res.response.chatId}`, {
|
||||
replace: true,
|
||||
state: {
|
||||
selectedChat: {
|
||||
id: res.response.chatId,
|
||||
chat: chatString,
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setChatHistory(prev => [
|
||||
...prev,
|
||||
{ sender: 'Assistant', text: 'Something went wrong' }
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
useGSAP(() => {
|
||||
gsap.from('#input', { opacity: 0, y: 5, ease: 'power2.inOut', duration: 0.5 });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-end items-center p-4 gap-8 h-full w-full">
|
||||
<div className="w-full p-2 rounded overflow-y-auto h-full mb-4">
|
||||
{chatHistory.length > 0 ? (
|
||||
<>
|
||||
{chatHistory.map((msg, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex mb-2 ${msg.sender === 'User' ? 'justify-end' : 'justify-start items-start'}`}
|
||||
>
|
||||
{msg.sender === 'Assistant' && (
|
||||
<img
|
||||
src={callCenterIcon}
|
||||
alt="Call Center Icon"
|
||||
className="w-6 h-6 mr-2"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`p-3 rounded-lg max-w-md flex ${
|
||||
msg.sender === 'User'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 text-gray-800'
|
||||
}`}
|
||||
style={{ whiteSpace: 'normal' }}
|
||||
>
|
||||
{msg.text.split('\n').map((line, i) => (
|
||||
<p key={i}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{(isLoading || isFetching) && (
|
||||
<div className="flex mb-2 justify-start items-start">
|
||||
<img
|
||||
src={callCenterIcon}
|
||||
alt="Call Center Icon"
|
||||
className="w-6 h-6 mr-2"
|
||||
/>
|
||||
<div
|
||||
className="p-3 rounded-lg max-w-md flex bg-gray-200 text-gray-800"
|
||||
style={{ whiteSpace: 'normal' }}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="animate-spin h-5 w-5 mr-3 text-gray-500"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Assistant is typing...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col gap-2 items-center justify-center">
|
||||
<h1 className="text-xl" id="firstheading">
|
||||
Start a New Chat
|
||||
</h1>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
<div id="input" className="w-2/3 mb-20">
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Type your message..."
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="w-full px-5 py-2 rounded-l-xl outline-none border border-gray-300"
|
||||
/>
|
||||
<button
|
||||
disabled={isLoading || isFetching}
|
||||
onClick={onSubmit}
|
||||
className="bg-black text-white font-semibold px-4 py-2 rounded-r-xl hover:bg-slate-700"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewChatPage;
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import gsap from 'gsap';
|
||||
import { useGSAP } from '@gsap/react';
|
||||
import { useLazySendChatQuestionQuery } from '../store/api/chatApi';
|
||||
import callCenterIcon from '../assets/call-center.png';
|
||||
|
||||
interface ChatMessage {
|
||||
sender: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const NewChatPage: React.FC = () => {
|
||||
const [sendChatQuestion, { isLoading, isFetching }] = useLazySendChatQuestionQuery();
|
||||
const [message, setMessage] = useState<string>('');
|
||||
const [chatHistory, setChatHistory] = useState<ChatMessage[]>([]);
|
||||
const navigate = useNavigate();
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [chatHistory, isLoading, isFetching]);
|
||||
|
||||
useEffect(() => {
|
||||
setChatHistory([]);
|
||||
}, []);
|
||||
|
||||
async function onSubmit() {
|
||||
if (!message.trim()) return;
|
||||
|
||||
const newUserMessage: ChatMessage = { sender: 'User', text: message };
|
||||
const updatedHistory = [...chatHistory, newUserMessage];
|
||||
setChatHistory(updatedHistory);
|
||||
const userMessage = message;
|
||||
setMessage('');
|
||||
|
||||
const storedUser = localStorage.getItem('user');
|
||||
const email = storedUser ? JSON.parse(storedUser).email : '';
|
||||
|
||||
const question = { query: userMessage, email };
|
||||
|
||||
try {
|
||||
const res = await sendChatQuestion(question).unwrap();
|
||||
console.log('Response from server:', res);
|
||||
|
||||
let bestAnswer = res.response.best_answer;
|
||||
if (typeof bestAnswer !== 'string') {
|
||||
bestAnswer = String(bestAnswer);
|
||||
}
|
||||
bestAnswer = bestAnswer.trim();
|
||||
|
||||
const newAssistantMessage: ChatMessage = { sender: 'Assistant', text: bestAnswer };
|
||||
const newUpdatedHistory = [...updatedHistory, newAssistantMessage];
|
||||
setChatHistory(newUpdatedHistory);
|
||||
|
||||
if (res.response.chatId) {
|
||||
const chatString = newUpdatedHistory
|
||||
.map(msg => (msg.sender === 'User' ? 'User: ' : 'Bot: ') + msg.text)
|
||||
.join('\n');
|
||||
navigate(`/dashboard/chat/${res.response.chatId}`, {
|
||||
replace: true,
|
||||
state: {
|
||||
selectedChat: {
|
||||
id: res.response.chatId,
|
||||
chat: chatString,
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setChatHistory(prev => [
|
||||
...prev,
|
||||
{ sender: 'Assistant', text: 'Something went wrong' }
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
useGSAP(() => {
|
||||
gsap.from('#input', { opacity: 0, y: 5, ease: 'power2.inOut', duration: 0.5 });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-end items-center p-4 gap-8 h-full w-full">
|
||||
<div className="w-full p-2 rounded overflow-y-auto h-full mb-4">
|
||||
{chatHistory.length > 0 ? (
|
||||
<>
|
||||
{chatHistory.map((msg, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex mb-2 ${msg.sender === 'User' ? 'justify-end' : 'justify-start items-start'}`}
|
||||
>
|
||||
{msg.sender === 'Assistant' && (
|
||||
<img
|
||||
src={callCenterIcon}
|
||||
alt="Call Center Icon"
|
||||
className="w-6 h-6 mr-2"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`p-3 rounded-lg max-w-md flex ${
|
||||
msg.sender === 'User'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 text-gray-800'
|
||||
}`}
|
||||
style={{ whiteSpace: 'normal' }}
|
||||
>
|
||||
{msg.text.split('\n').map((line, i) => (
|
||||
<p key={i}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{(isLoading || isFetching) && (
|
||||
<div className="flex mb-2 justify-start items-start">
|
||||
<img
|
||||
src={callCenterIcon}
|
||||
alt="Call Center Icon"
|
||||
className="w-6 h-6 mr-2"
|
||||
/>
|
||||
<div
|
||||
className="p-3 rounded-lg max-w-md flex bg-gray-200 text-gray-800"
|
||||
style={{ whiteSpace: 'normal' }}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="animate-spin h-5 w-5 mr-3 text-gray-500"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Assistant is typing...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col gap-2 items-center justify-center">
|
||||
<h1 className="text-xl" id="firstheading">
|
||||
Start a New Chat
|
||||
</h1>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
<div id="input" className="w-2/3 mb-20">
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Type your message..."
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="w-full px-5 py-2 rounded-l-xl outline-none border border-gray-300"
|
||||
/>
|
||||
<button
|
||||
disabled={isLoading || isFetching}
|
||||
onClick={onSubmit}
|
||||
className="bg-black text-white font-semibold px-4 py-2 rounded-r-xl hover:bg-slate-700"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewChatPage;
|
||||
|
@ -1,233 +1,233 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Avatar,
|
||||
Paper,
|
||||
Button,
|
||||
TextField,
|
||||
IconButton,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Navbar } from '../pages/LandingPage';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
|
||||
const Profile: React.FC = () => {
|
||||
const [user, setUser] = useState<any>(null);
|
||||
const [editing, setEditing] = useState<boolean>(false);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
role: '',
|
||||
bio: '',
|
||||
picture: '',
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
const parsedUser = JSON.parse(storedUser);
|
||||
setUser(parsedUser);
|
||||
setFormData({
|
||||
name: parsedUser.name || '',
|
||||
email: parsedUser.email || '',
|
||||
phone: parsedUser.phone || '',
|
||||
role: parsedUser.role || '',
|
||||
bio: parsedUser.bio || '',
|
||||
picture: parsedUser.picture || '',
|
||||
});
|
||||
} else {
|
||||
navigate('/login');
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[e.target.name]: e.target.value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setFormData({
|
||||
name: user.name || '',
|
||||
email: user.email || '',
|
||||
phone: user.phone || '',
|
||||
role: user.role || '',
|
||||
bio: user.bio || '',
|
||||
picture: user.picture || '',
|
||||
});
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
const handleSaveEdit = async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:5000/api/update_profile', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
const updatedUser = { ...user, ...formData };
|
||||
setUser(updatedUser);
|
||||
localStorage.setItem('user', JSON.stringify(updatedUser));
|
||||
setEditing(false);
|
||||
} else {
|
||||
alert(data.error || 'Error updating profile');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Error updating profile');
|
||||
}
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
background: 'linear-gradient(to right, #d0e7ff, #f0f8ff)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Navbar user={user} setUser={setUser} />
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
p: 4,
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
p: 4,
|
||||
borderRadius: '12px',
|
||||
maxWidth: '500px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
src={user.picture}
|
||||
alt={user.name}
|
||||
sx={{ width: 100, height: 100, mb: 2 }}
|
||||
/>
|
||||
{editing ? (
|
||||
<>
|
||||
<TextField
|
||||
label="Name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
disabled
|
||||
/>
|
||||
<TextField
|
||||
label="Phone"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Role"
|
||||
name="role"
|
||||
value={formData.role}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Bio"
|
||||
name="bio"
|
||||
value={formData.bio}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Picture URL"
|
||||
name="picture"
|
||||
value={formData.picture}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
width: '100%',
|
||||
mt: 2,
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={handleSaveEdit} color="primary">
|
||||
<CheckIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={handleCancelEdit} color="error">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ fontWeight: 'bold', color: '#0d47a1', mb: 1 }}
|
||||
>
|
||||
{user.name}
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: '#424242', mb: 2 }}>
|
||||
{user.email}
|
||||
</Typography>
|
||||
<Divider sx={{ width: '100%', mb: 2 }} />
|
||||
<Typography variant="body1" sx={{ color: '#424242', mb: 1 }}>
|
||||
<strong>Phone:</strong> {user.phone || 'Not provided'}
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: '#424242', mb: 1 }}>
|
||||
<strong>Role:</strong> {user.role || 'User'}
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: '#424242', mb: 1 }}>
|
||||
<strong>Bio:</strong> {user.bio || 'No bio available'}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ mt: 3, backgroundColor: '#0d47a1' }}
|
||||
onClick={() => setEditing(true)}
|
||||
>
|
||||
Edit Profile
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Avatar,
|
||||
Paper,
|
||||
Button,
|
||||
TextField,
|
||||
IconButton,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Navbar } from '../pages/LandingPage';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
|
||||
const Profile: React.FC = () => {
|
||||
const [user, setUser] = useState<any>(null);
|
||||
const [editing, setEditing] = useState<boolean>(false);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
role: '',
|
||||
bio: '',
|
||||
picture: '',
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
const parsedUser = JSON.parse(storedUser);
|
||||
setUser(parsedUser);
|
||||
setFormData({
|
||||
name: parsedUser.name || '',
|
||||
email: parsedUser.email || '',
|
||||
phone: parsedUser.phone || '',
|
||||
role: parsedUser.role || '',
|
||||
bio: parsedUser.bio || '',
|
||||
picture: parsedUser.picture || '',
|
||||
});
|
||||
} else {
|
||||
navigate('/login');
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[e.target.name]: e.target.value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setFormData({
|
||||
name: user.name || '',
|
||||
email: user.email || '',
|
||||
phone: user.phone || '',
|
||||
role: user.role || '',
|
||||
bio: user.bio || '',
|
||||
picture: user.picture || '',
|
||||
});
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
const handleSaveEdit = async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:5000/api/update_profile', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
const updatedUser = { ...user, ...formData };
|
||||
setUser(updatedUser);
|
||||
localStorage.setItem('user', JSON.stringify(updatedUser));
|
||||
setEditing(false);
|
||||
} else {
|
||||
alert(data.error || 'Error updating profile');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Error updating profile');
|
||||
}
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
background: 'linear-gradient(to right, #d0e7ff, #f0f8ff)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Navbar user={user} setUser={setUser} />
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
p: 4,
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
p: 4,
|
||||
borderRadius: '12px',
|
||||
maxWidth: '500px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
src={user.picture}
|
||||
alt={user.name}
|
||||
sx={{ width: 100, height: 100, mb: 2 }}
|
||||
/>
|
||||
{editing ? (
|
||||
<>
|
||||
<TextField
|
||||
label="Name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
disabled
|
||||
/>
|
||||
<TextField
|
||||
label="Phone"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Role"
|
||||
name="role"
|
||||
value={formData.role}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Bio"
|
||||
name="bio"
|
||||
value={formData.bio}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Picture URL"
|
||||
name="picture"
|
||||
value={formData.picture}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
width: '100%',
|
||||
mt: 2,
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={handleSaveEdit} color="primary">
|
||||
<CheckIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={handleCancelEdit} color="error">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ fontWeight: 'bold', color: '#0d47a1', mb: 1 }}
|
||||
>
|
||||
{user.name}
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: '#424242', mb: 2 }}>
|
||||
{user.email}
|
||||
</Typography>
|
||||
<Divider sx={{ width: '100%', mb: 2 }} />
|
||||
<Typography variant="body1" sx={{ color: '#424242', mb: 1 }}>
|
||||
<strong>Phone:</strong> {user.phone || 'Not provided'}
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: '#424242', mb: 1 }}>
|
||||
<strong>Role:</strong> {user.role || 'User'}
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: '#424242', mb: 1 }}>
|
||||
<strong>Bio:</strong> {user.bio || 'No bio available'}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ mt: 3, backgroundColor: '#0d47a1' }}
|
||||
onClick={() => setEditing(true)}
|
||||
>
|
||||
Edit Profile
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
|
@ -1,240 +1,240 @@
|
||||
import React from 'react';
|
||||
import { GoogleLogin, GoogleOAuthProvider } from '@react-oauth/google';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
const CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com";
|
||||
|
||||
const RegistrationFormContent: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const nameElement = document.getElementById('name') as HTMLInputElement | null;
|
||||
const emailElement = document.getElementById('email') as HTMLInputElement | null;
|
||||
const passwordElement = document.getElementById('password') as HTMLInputElement | null;
|
||||
const confirmPasswordElement = document.getElementById('confirm-password') as HTMLInputElement | null;
|
||||
|
||||
if (!nameElement || !emailElement || !passwordElement || !confirmPasswordElement) {
|
||||
console.error('One or more input fields are missing');
|
||||
return;
|
||||
}
|
||||
|
||||
const name = nameElement.value;
|
||||
const email = emailElement.value;
|
||||
const password = passwordElement.value;
|
||||
const confirmPassword = confirmPasswordElement.value;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
console.error('Passwords do not match');
|
||||
alert('Passwords do not match');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:5000/api/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, email, password }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
console.log('User registered successfully:', data.message);
|
||||
const loggedInUser = {
|
||||
name,
|
||||
email,
|
||||
picture: 'https://via.placeholder.com/150',
|
||||
};
|
||||
localStorage.setItem('user', JSON.stringify(loggedInUser));
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
console.error('Error:', data.error);
|
||||
alert(data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error registering user:', error);
|
||||
alert('Error registering user');
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleLoginSuccess = async (response: any) => {
|
||||
try {
|
||||
const res = await fetch('http://localhost:5000/api/verify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: response.credential }),
|
||||
});
|
||||
const data = await res.json();
|
||||
const loggedInUser = {
|
||||
name: data.user.name,
|
||||
email: data.user.email,
|
||||
picture: data.user.picture || 'https://via.placeholder.com/150',
|
||||
};
|
||||
localStorage.setItem('user', JSON.stringify(loggedInUser));
|
||||
navigate('/dashboard');
|
||||
} catch (error) {
|
||||
console.error('Error tiken verification:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleLoginError = () => {
|
||||
console.error('Error auth: Google login failed');
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '16px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="registration-card"
|
||||
style={{
|
||||
maxWidth: '400px',
|
||||
width: '100%',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||
backgroundColor: '#fff',
|
||||
padding: '24px',
|
||||
}}
|
||||
>
|
||||
<h2 style={{ textAlign: 'center', fontWeight: 'bold', color: '#333', marginBottom: '16px' }}>
|
||||
Create Your Account
|
||||
</h2>
|
||||
<p style={{ textAlign: 'center', color: '#555', marginBottom: '24px' }}>
|
||||
Join us to explore personalized health solutions.
|
||||
</p>
|
||||
<form noValidate autoComplete="off" onSubmit={handleSubmit}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label htmlFor="name" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label htmlFor="email" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label htmlFor="password" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<label
|
||||
htmlFor="confirm-password"
|
||||
style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}
|
||||
>
|
||||
Confirm Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirm-password"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
backgroundColor: '#007bff',
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
Register
|
||||
</button>
|
||||
</form>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
marginBottom: '16px',
|
||||
color: '#555',
|
||||
fontSize: '14px',
|
||||
borderBottom: '1px solid #ccc',
|
||||
lineHeight: '0.1em',
|
||||
margin: '10px 0 20px',
|
||||
}}
|
||||
>
|
||||
<span style={{ background: '#fff', padding: '0 10px' }}>OR</span>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', marginBottom: '16px' }}>
|
||||
<GoogleLogin
|
||||
onSuccess={handleGoogleLoginSuccess}
|
||||
onError={handleGoogleLoginError}
|
||||
size="large"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
<p style={{ textAlign: 'center', color: '#555', marginTop: '16px' }}>
|
||||
Already have an account?{' '}
|
||||
<Link to="/login" style={{ color: '#007bff', textDecoration: 'none' }}>
|
||||
Sign In
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RegistrationForm: React.FC = () => {
|
||||
return (
|
||||
<GoogleOAuthProvider clientId={CLIENT_ID}>
|
||||
<RegistrationFormContent />
|
||||
</GoogleOAuthProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegistrationForm;
|
||||
import React from 'react';
|
||||
import { GoogleLogin, GoogleOAuthProvider } from '@react-oauth/google';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
const CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com";
|
||||
|
||||
const RegistrationFormContent: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const nameElement = document.getElementById('name') as HTMLInputElement | null;
|
||||
const emailElement = document.getElementById('email') as HTMLInputElement | null;
|
||||
const passwordElement = document.getElementById('password') as HTMLInputElement | null;
|
||||
const confirmPasswordElement = document.getElementById('confirm-password') as HTMLInputElement | null;
|
||||
|
||||
if (!nameElement || !emailElement || !passwordElement || !confirmPasswordElement) {
|
||||
console.error('One or more input fields are missing');
|
||||
return;
|
||||
}
|
||||
|
||||
const name = nameElement.value;
|
||||
const email = emailElement.value;
|
||||
const password = passwordElement.value;
|
||||
const confirmPassword = confirmPasswordElement.value;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
console.error('Passwords do not match');
|
||||
alert('Passwords do not match');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:5000/api/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, email, password }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
console.log('User registered successfully:', data.message);
|
||||
const loggedInUser = {
|
||||
name,
|
||||
email,
|
||||
picture: 'https://via.placeholder.com/150',
|
||||
};
|
||||
localStorage.setItem('user', JSON.stringify(loggedInUser));
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
console.error('Error:', data.error);
|
||||
alert(data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error registering user:', error);
|
||||
alert('Error registering user');
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleLoginSuccess = async (response: any) => {
|
||||
try {
|
||||
const res = await fetch('http://localhost:5000/api/verify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: response.credential }),
|
||||
});
|
||||
const data = await res.json();
|
||||
const loggedInUser = {
|
||||
name: data.user.name,
|
||||
email: data.user.email,
|
||||
picture: data.user.picture || 'https://via.placeholder.com/150',
|
||||
};
|
||||
localStorage.setItem('user', JSON.stringify(loggedInUser));
|
||||
navigate('/dashboard');
|
||||
} catch (error) {
|
||||
console.error('Error tiken verification:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleLoginError = () => {
|
||||
console.error('Error auth: Google login failed');
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '16px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="registration-card"
|
||||
style={{
|
||||
maxWidth: '400px',
|
||||
width: '100%',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||
backgroundColor: '#fff',
|
||||
padding: '24px',
|
||||
}}
|
||||
>
|
||||
<h2 style={{ textAlign: 'center', fontWeight: 'bold', color: '#333', marginBottom: '16px' }}>
|
||||
Create Your Account
|
||||
</h2>
|
||||
<p style={{ textAlign: 'center', color: '#555', marginBottom: '24px' }}>
|
||||
Join us to explore personalized health solutions.
|
||||
</p>
|
||||
<form noValidate autoComplete="off" onSubmit={handleSubmit}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label htmlFor="name" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label htmlFor="email" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label htmlFor="password" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<label
|
||||
htmlFor="confirm-password"
|
||||
style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}
|
||||
>
|
||||
Confirm Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirm-password"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
backgroundColor: '#007bff',
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
Register
|
||||
</button>
|
||||
</form>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
marginBottom: '16px',
|
||||
color: '#555',
|
||||
fontSize: '14px',
|
||||
borderBottom: '1px solid #ccc',
|
||||
lineHeight: '0.1em',
|
||||
margin: '10px 0 20px',
|
||||
}}
|
||||
>
|
||||
<span style={{ background: '#fff', padding: '0 10px' }}>OR</span>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', marginBottom: '16px' }}>
|
||||
<GoogleLogin
|
||||
onSuccess={handleGoogleLoginSuccess}
|
||||
onError={handleGoogleLoginError}
|
||||
size="large"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
<p style={{ textAlign: 'center', color: '#555', marginTop: '16px' }}>
|
||||
Already have an account?{' '}
|
||||
<Link to="/login" style={{ color: '#007bff', textDecoration: 'none' }}>
|
||||
Sign In
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RegistrationForm: React.FC = () => {
|
||||
return (
|
||||
<GoogleOAuthProvider clientId={CLIENT_ID}>
|
||||
<RegistrationFormContent />
|
||||
</GoogleOAuthProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegistrationForm;
|
||||
|
@ -1,37 +1,37 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Приховує скроллбар у браузерах на основі WebKit (Chrome, Safari) */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Приховує скроллбар у Firefox */
|
||||
.no-scrollbar {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* Приховує скроллбар в Internet Explorer та Edge */
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
/* HTML: <div class="loader"></div> */
|
||||
.loader {
|
||||
width: 20px;
|
||||
aspect-ratio: 2;
|
||||
--_g: no-repeat radial-gradient(circle closest-side,#000 90%,#0000);
|
||||
background:
|
||||
var(--_g) 0% 50%,
|
||||
var(--_g) 50% 50%,
|
||||
var(--_g) 100% 50%;
|
||||
background-size: calc(100%/3) 50%;
|
||||
animation: l3 1s infinite linear;
|
||||
}
|
||||
@keyframes l3 {
|
||||
20%{background-position:0% 0%, 50% 50%,100% 50%}
|
||||
40%{background-position:0% 100%, 50% 0%,100% 50%}
|
||||
60%{background-position:0% 50%, 50% 100%,100% 0%}
|
||||
80%{background-position:0% 50%, 50% 50%,100% 100%}
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Приховує скроллбар у браузерах на основі WebKit (Chrome, Safari) */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Приховує скроллбар у Firefox */
|
||||
.no-scrollbar {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* Приховує скроллбар в Internet Explorer та Edge */
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
/* HTML: <div class="loader"></div> */
|
||||
.loader {
|
||||
width: 20px;
|
||||
aspect-ratio: 2;
|
||||
--_g: no-repeat radial-gradient(circle closest-side,#000 90%,#0000);
|
||||
background:
|
||||
var(--_g) 0% 50%,
|
||||
var(--_g) 50% 50%,
|
||||
var(--_g) 100% 50%;
|
||||
background-size: calc(100%/3) 50%;
|
||||
animation: l3 1s infinite linear;
|
||||
}
|
||||
@keyframes l3 {
|
||||
20%{background-position:0% 0%, 50% 50%,100% 50%}
|
||||
40%{background-position:0% 100%, 50% 0%,100% 50%}
|
||||
60%{background-position:0% 50%, 50% 100%,100% 0%}
|
||||
80%{background-position:0% 50%, 50% 50%,100% 100%}
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
import { Provider } from 'react-redux'
|
||||
import store from './store/index.ts'
|
||||
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
</StrictMode>,
|
||||
)
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
import { Provider } from 'react-redux'
|
||||
import store from './store/index.ts'
|
||||
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
@ -1,227 +1,227 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useLazySendChatQuestionQuery } from '../store/api/chatApi';
|
||||
import callCenterIcon from '../assets/call-center.png';
|
||||
|
||||
interface ChatMessage {
|
||||
sender: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const HomePage: React.FC = () => {
|
||||
const [sendChatQuestion, { isLoading, isFetching }] = useLazySendChatQuestionQuery();
|
||||
const [message, setMessage] = useState('');
|
||||
const [chatHistory, setChatHistory] = useState<ChatMessage[]>([]);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const state = (location.state as any) || {};
|
||||
|
||||
const isNewChat = state.newChat === true;
|
||||
const selectedChat = state.selectedChat || null;
|
||||
const selectedChatId = selectedChat ? selectedChat.id : null;
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [chatHistory, isLoading, isFetching]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNewChat && selectedChat && selectedChat.chat) {
|
||||
const messages: ChatMessage[] = selectedChat.chat
|
||||
.split(/(?=^(User:|Bot:))/m)
|
||||
.map((msg:any) => {
|
||||
const trimmed = msg.trim();
|
||||
const sender = trimmed.startsWith('User:') ? 'User' : 'Assistant';
|
||||
return {
|
||||
sender,
|
||||
text: trimmed.replace(/^User:|^Bot:/, '').trim(),
|
||||
};
|
||||
});
|
||||
setChatHistory(messages);
|
||||
} else {
|
||||
setChatHistory([]);
|
||||
}
|
||||
}, [isNewChat, selectedChat]);
|
||||
|
||||
|
||||
const formatMessage = (text: string) => {
|
||||
let lines: string[] = [];
|
||||
|
||||
if (text.includes('\n')) {
|
||||
lines = text.split('\n');
|
||||
} else {
|
||||
lines = text.split(/(?=\d+\.\s+)/);
|
||||
}
|
||||
|
||||
lines = lines.map((line) => line.trim()).filter((line) => line !== '');
|
||||
if (lines.length === 0) return null;
|
||||
|
||||
return lines.map((line, index) => {
|
||||
if (/^\d+\.\s*/.test(line)) {
|
||||
const colonIndex = line.indexOf(':');
|
||||
if (colonIndex !== -1) {
|
||||
const firstPart = line.substring(0, colonIndex);
|
||||
const rest = line.substring(colonIndex + 1);
|
||||
return (
|
||||
<div key={index} className="mb-1">
|
||||
<strong>{firstPart.trim()}</strong>: {rest.trim()}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key={index} className="mb-1">
|
||||
<strong>{line}</strong>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <div key={index}>{line}</div>;
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!message.trim()) return;
|
||||
const userMessage = message.trim();
|
||||
setMessage('');
|
||||
setChatHistory((prev) => [...prev, { sender: 'User', text: userMessage }]);
|
||||
|
||||
const storedUser = localStorage.getItem('user');
|
||||
const email = storedUser ? JSON.parse(storedUser).email : '';
|
||||
const payload = selectedChatId
|
||||
? { query: userMessage, chatId: selectedChatId, email }
|
||||
: { query: userMessage, email };
|
||||
|
||||
try {
|
||||
const res = await sendChatQuestion(payload).unwrap();
|
||||
let bestAnswer = res.response.best_answer;
|
||||
if (typeof bestAnswer !== 'string') {
|
||||
bestAnswer = String(bestAnswer);
|
||||
}
|
||||
bestAnswer = bestAnswer.trim();
|
||||
|
||||
if (bestAnswer) {
|
||||
setChatHistory((prev) => [...prev, { sender: 'Assistant', text: bestAnswer }]);
|
||||
}
|
||||
|
||||
if (!selectedChatId && res.response.chatId) {
|
||||
const updatedChatHistory = [...chatHistory, { sender: 'User', text: userMessage }];
|
||||
if (bestAnswer) {
|
||||
updatedChatHistory.push({ sender: 'Assistant', text: bestAnswer });
|
||||
}
|
||||
const chatString = updatedChatHistory
|
||||
.map((msg) => (msg.sender === 'User' ? 'User: ' : 'Bot: ') + msg.text)
|
||||
.join('\n');
|
||||
navigate(`/dashboard/chat/${res.response.chatId}`, {
|
||||
replace: true,
|
||||
state: {
|
||||
selectedChat: {
|
||||
id: res.response.chatId,
|
||||
chat: chatString,
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setChatHistory((prev) => [
|
||||
...prev,
|
||||
{ sender: 'Assistant', text: 'Something went wrong' },
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-end items-center p-4 gap-8 h-full w-full">
|
||||
<div className="w-full p-2 rounded overflow-y-auto h-full mb-4">
|
||||
{chatHistory.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<h1 className="text-xl">Start a New Chat</h1>
|
||||
</div>
|
||||
) : (
|
||||
chatHistory.map((msg, index) => {
|
||||
const formattedMessage = formatMessage(msg.text);
|
||||
if (!formattedMessage) return null;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex mb-2 ${msg.sender === 'User' ? 'justify-end' : 'justify-start items-start'}`}
|
||||
>
|
||||
{msg.sender === 'Assistant' && (
|
||||
<img
|
||||
src={callCenterIcon}
|
||||
alt="Call Center Icon"
|
||||
className="w-6 h-6 mr-2"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`p-3 rounded-lg max-w-md ${
|
||||
msg.sender === 'User'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 text-gray-800'
|
||||
}`}
|
||||
>
|
||||
{formattedMessage}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
{(isLoading || isFetching) && (
|
||||
<div className="flex mb-2 justify-start items-start">
|
||||
<img src={callCenterIcon} alt="Call Center Icon" className="w-6 h-6 mr-2" />
|
||||
<div className="p-3 rounded-lg max-w-md bg-gray-200 text-gray-800 flex items-center">
|
||||
<svg
|
||||
className="animate-spin h-5 w-5 mr-3 text-gray-500"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Assistant is typing...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
<div className="w-2/3 mb-20">
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Waiting for your question..."
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
disabled={isLoading || isFetching}
|
||||
className="w-full px-5 py-2 rounded-l-xl outline-none border border-gray-300"
|
||||
/>
|
||||
<button
|
||||
onClick={onSubmit}
|
||||
disabled={isLoading || isFetching}
|
||||
className="bg-black text-white font-semibold px-4 py-2 rounded-r-xl hover:bg-gray-800 disabled:opacity-50"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useLazySendChatQuestionQuery } from '../store/api/chatApi';
|
||||
import callCenterIcon from '../assets/call-center.png';
|
||||
|
||||
interface ChatMessage {
|
||||
sender: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const HomePage: React.FC = () => {
|
||||
const [sendChatQuestion, { isLoading, isFetching }] = useLazySendChatQuestionQuery();
|
||||
const [message, setMessage] = useState('');
|
||||
const [chatHistory, setChatHistory] = useState<ChatMessage[]>([]);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const state = (location.state as any) || {};
|
||||
|
||||
const isNewChat = state.newChat === true;
|
||||
const selectedChat = state.selectedChat || null;
|
||||
const selectedChatId = selectedChat ? selectedChat.id : null;
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [chatHistory, isLoading, isFetching]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNewChat && selectedChat && selectedChat.chat) {
|
||||
const messages: ChatMessage[] = selectedChat.chat
|
||||
.split(/(?=^(User:|Bot:))/m)
|
||||
.map((msg:any) => {
|
||||
const trimmed = msg.trim();
|
||||
const sender = trimmed.startsWith('User:') ? 'User' : 'Assistant';
|
||||
return {
|
||||
sender,
|
||||
text: trimmed.replace(/^User:|^Bot:/, '').trim(),
|
||||
};
|
||||
});
|
||||
setChatHistory(messages);
|
||||
} else {
|
||||
setChatHistory([]);
|
||||
}
|
||||
}, [isNewChat, selectedChat]);
|
||||
|
||||
|
||||
const formatMessage = (text: string) => {
|
||||
let lines: string[] = [];
|
||||
|
||||
if (text.includes('\n')) {
|
||||
lines = text.split('\n');
|
||||
} else {
|
||||
lines = text.split(/(?=\d+\.\s+)/);
|
||||
}
|
||||
|
||||
lines = lines.map((line) => line.trim()).filter((line) => line !== '');
|
||||
if (lines.length === 0) return null;
|
||||
|
||||
return lines.map((line, index) => {
|
||||
if (/^\d+\.\s*/.test(line)) {
|
||||
const colonIndex = line.indexOf(':');
|
||||
if (colonIndex !== -1) {
|
||||
const firstPart = line.substring(0, colonIndex);
|
||||
const rest = line.substring(colonIndex + 1);
|
||||
return (
|
||||
<div key={index} className="mb-1">
|
||||
<strong>{firstPart.trim()}</strong>: {rest.trim()}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key={index} className="mb-1">
|
||||
<strong>{line}</strong>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <div key={index}>{line}</div>;
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!message.trim()) return;
|
||||
const userMessage = message.trim();
|
||||
setMessage('');
|
||||
setChatHistory((prev) => [...prev, { sender: 'User', text: userMessage }]);
|
||||
|
||||
const storedUser = localStorage.getItem('user');
|
||||
const email = storedUser ? JSON.parse(storedUser).email : '';
|
||||
const payload = selectedChatId
|
||||
? { query: userMessage, chatId: selectedChatId, email }
|
||||
: { query: userMessage, email };
|
||||
|
||||
try {
|
||||
const res = await sendChatQuestion(payload).unwrap();
|
||||
let bestAnswer = res.response.best_answer;
|
||||
if (typeof bestAnswer !== 'string') {
|
||||
bestAnswer = String(bestAnswer);
|
||||
}
|
||||
bestAnswer = bestAnswer.trim();
|
||||
|
||||
if (bestAnswer) {
|
||||
setChatHistory((prev) => [...prev, { sender: 'Assistant', text: bestAnswer }]);
|
||||
}
|
||||
|
||||
if (!selectedChatId && res.response.chatId) {
|
||||
const updatedChatHistory = [...chatHistory, { sender: 'User', text: userMessage }];
|
||||
if (bestAnswer) {
|
||||
updatedChatHistory.push({ sender: 'Assistant', text: bestAnswer });
|
||||
}
|
||||
const chatString = updatedChatHistory
|
||||
.map((msg) => (msg.sender === 'User' ? 'User: ' : 'Bot: ') + msg.text)
|
||||
.join('\n');
|
||||
navigate(`/dashboard/chat/${res.response.chatId}`, {
|
||||
replace: true,
|
||||
state: {
|
||||
selectedChat: {
|
||||
id: res.response.chatId,
|
||||
chat: chatString,
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setChatHistory((prev) => [
|
||||
...prev,
|
||||
{ sender: 'Assistant', text: 'Something went wrong' },
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-end items-center p-4 gap-8 h-full w-full">
|
||||
<div className="w-full p-2 rounded overflow-y-auto h-full mb-4">
|
||||
{chatHistory.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<h1 className="text-xl">Start a New Chat</h1>
|
||||
</div>
|
||||
) : (
|
||||
chatHistory.map((msg, index) => {
|
||||
const formattedMessage = formatMessage(msg.text);
|
||||
if (!formattedMessage) return null;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex mb-2 ${msg.sender === 'User' ? 'justify-end' : 'justify-start items-start'}`}
|
||||
>
|
||||
{msg.sender === 'Assistant' && (
|
||||
<img
|
||||
src={callCenterIcon}
|
||||
alt="Call Center Icon"
|
||||
className="w-6 h-6 mr-2"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`p-3 rounded-lg max-w-md ${
|
||||
msg.sender === 'User'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 text-gray-800'
|
||||
}`}
|
||||
>
|
||||
{formattedMessage}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
{(isLoading || isFetching) && (
|
||||
<div className="flex mb-2 justify-start items-start">
|
||||
<img src={callCenterIcon} alt="Call Center Icon" className="w-6 h-6 mr-2" />
|
||||
<div className="p-3 rounded-lg max-w-md bg-gray-200 text-gray-800 flex items-center">
|
||||
<svg
|
||||
className="animate-spin h-5 w-5 mr-3 text-gray-500"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Assistant is typing...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
<div className="w-2/3 mb-20">
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Waiting for your question..."
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
disabled={isLoading || isFetching}
|
||||
className="w-full px-5 py-2 rounded-l-xl outline-none border border-gray-300"
|
||||
/>
|
||||
<button
|
||||
onClick={onSubmit}
|
||||
disabled={isLoading || isFetching}
|
||||
className="bg-black text-white font-semibold px-4 py-2 rounded-r-xl hover:bg-gray-800 disabled:opacity-50"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
|
@ -1,186 +1,186 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import BackImage from '../assets/smallheadicon.png';
|
||||
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
||||
import { Box, Avatar } from '@mui/material';
|
||||
import gsap from 'gsap';
|
||||
import { useGSAP } from '@gsap/react';
|
||||
import LogoutIcon from '@mui/icons-material/Logout';
|
||||
import LoginIcon from '@mui/icons-material/Login';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
const BouncingArrow = () => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
mt: 2,
|
||||
animation: 'bounce 1s infinite',
|
||||
'@keyframes bounce': {
|
||||
'0%, 100%': {
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
'50%': {
|
||||
transform: 'translateY(-10px)',
|
||||
},
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ArrowDownwardIcon fontSize="large" />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface NavbarProps {
|
||||
user: any;
|
||||
setUser: (user: any) => void;
|
||||
}
|
||||
|
||||
const Navbar: React.FC<NavbarProps> = ({ user, setUser }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSignOut = () => {
|
||||
setUser(null);
|
||||
localStorage.removeItem('user');
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="w-full bg-white shadow-md py-4 px-2 sm:px-8 flex justify-between items-center fixed top-0 left-0 right-0 z-50">
|
||||
<div className="text-2xl font-semibold text-dark-blue flex items-center">
|
||||
Health AI
|
||||
<img src={BackImage} width={25} alt="Logo" />
|
||||
</div>
|
||||
<ul className="flex space-x-6 text-gray-600">
|
||||
<li>
|
||||
<Link to="/" className="hover:text-bright-blue transition duration-300">
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/about" className="hover:text-bright-blue transition duration-300">
|
||||
About
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/contact" className="hover:text-bright-blue transition duration-300">
|
||||
Contact
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="flex items-center">
|
||||
{user ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar alt={user.name} src={user.picture} onClick={() => navigate('/profile')} />
|
||||
<LogoutIcon
|
||||
onClick={handleSignOut}
|
||||
sx={{ cursor: 'pointer', color: '#0d47a1', fontSize: '30px' }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<LoginIcon
|
||||
onClick={() => navigate('/register')}
|
||||
sx={{ cursor: 'pointer', color: '#0d47a1', fontSize: '30px' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [user, setUser] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
setUser(JSON.parse(storedUser));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useGSAP(() => {
|
||||
gsap.from('#mainheading', { opacity: 0.3, ease: 'power2.inOut', duration: 0.5 });
|
||||
gsap.from('#secondheading', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.3, duration: 0.5 });
|
||||
gsap.from('#button', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.5, duration: 0.5 });
|
||||
gsap.from('#features', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.7, duration: 0.5 });
|
||||
gsap.from('#arrow', { opacity: 0, ease: 'power2.inOut', delay: 2, duration: 0.2 });
|
||||
gsap.to('#button', { opacity: 1, ease: 'power2.inOut', delay: 2.5, duration: 0.5 });
|
||||
}, []);
|
||||
|
||||
const handleGetStartedClick = () => {
|
||||
if (!user) {
|
||||
navigate('/register');
|
||||
} else {
|
||||
navigate('/dashboard');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ backgroundColor: '#d0e7ff' }} className="min-h-screen flex flex-col">
|
||||
<div className="flex-grow flex flex-col items-center justify-center bg-gradient-to-b text-gray-800 p-4 pt-20">
|
||||
<Navbar user={user} setUser={setUser} />
|
||||
|
||||
<div className="flex flex-col items-center">
|
||||
<h1
|
||||
id="mainheading"
|
||||
className="text-4xl flex items-center sm:text-5xl md:text-6xl font-semibold mb-4 text-center text-dark-blue"
|
||||
>
|
||||
AI Assistant for Your Health
|
||||
</h1>
|
||||
|
||||
<p
|
||||
id="secondheading"
|
||||
className="text-base sm:text-lg md:text-xl text-center max-w-2xl mb-8 text-gray-700"
|
||||
>
|
||||
A solution for personalized health support, including tailored medication guidance and wellness
|
||||
insights. Take care of yourself with the power of modern technology.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-6 mb-10" id="features">
|
||||
<div className="bg-white p-6 rounded-lg max-w-xs text-center shadow-md">
|
||||
<h3 className="text-xl font-medium mb-3 text-dark-blue">Personalized Prescription</h3>
|
||||
<p className="text-gray-600">
|
||||
Receive tailored medication recommendations specifically designed for your needs.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg max-w-xs text-center shadow-md">
|
||||
<h3 className="text-xl font-medium mb-3 text-dark-blue">Health Monitoring</h3>
|
||||
<p className="text-gray-600">
|
||||
Stay informed about your health with real-time monitoring and AI-driven insights.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg max-w-xs text-center shadow-md">
|
||||
<h3 className="text-xl font-medium mb-3 text-dark-blue">Advanced AI Support</h3>
|
||||
<p className="text-gray-600">
|
||||
Utilize AI support to ensure you're following the best routines for a healthier lifestyle.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="arrow" className="flex flex-col items-center mt-10 z-0">
|
||||
<p className="text-gray-600">Try it out</p>
|
||||
<BouncingArrow />
|
||||
</div>
|
||||
|
||||
<div id="button-wrapper" className="flex justify-center mt-6">
|
||||
<button
|
||||
id="button"
|
||||
onClick={handleGetStartedClick}
|
||||
className="bg-bright-blue text-white font-medium py-2 px-5 rounded hover:bg-deep-blue transition duration-300 shadow-md"
|
||||
>
|
||||
Get started
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className="text-center text-gray-500 p-4">
|
||||
<p>© {new Date().getFullYear()} Health AI. All rights reserved.</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Home, Navbar };
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import BackImage from '../assets/smallheadicon.png';
|
||||
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
||||
import { Box, Avatar } from '@mui/material';
|
||||
import gsap from 'gsap';
|
||||
import { useGSAP } from '@gsap/react';
|
||||
import LogoutIcon from '@mui/icons-material/Logout';
|
||||
import LoginIcon from '@mui/icons-material/Login';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
const BouncingArrow = () => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
mt: 2,
|
||||
animation: 'bounce 1s infinite',
|
||||
'@keyframes bounce': {
|
||||
'0%, 100%': {
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
'50%': {
|
||||
transform: 'translateY(-10px)',
|
||||
},
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ArrowDownwardIcon fontSize="large" />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface NavbarProps {
|
||||
user: any;
|
||||
setUser: (user: any) => void;
|
||||
}
|
||||
|
||||
const Navbar: React.FC<NavbarProps> = ({ user, setUser }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSignOut = () => {
|
||||
setUser(null);
|
||||
localStorage.removeItem('user');
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="w-full bg-white shadow-md py-4 px-2 sm:px-8 flex justify-between items-center fixed top-0 left-0 right-0 z-50">
|
||||
<div className="text-2xl font-semibold text-dark-blue flex items-center">
|
||||
Health AI
|
||||
<img src={BackImage} width={25} alt="Logo" />
|
||||
</div>
|
||||
<ul className="flex space-x-6 text-gray-600">
|
||||
<li>
|
||||
<Link to="/" className="hover:text-bright-blue transition duration-300">
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/about" className="hover:text-bright-blue transition duration-300">
|
||||
About
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/contact" className="hover:text-bright-blue transition duration-300">
|
||||
Contact
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="flex items-center">
|
||||
{user ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar alt={user.name} src={user.picture} onClick={() => navigate('/profile')} />
|
||||
<LogoutIcon
|
||||
onClick={handleSignOut}
|
||||
sx={{ cursor: 'pointer', color: '#0d47a1', fontSize: '30px' }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<LoginIcon
|
||||
onClick={() => navigate('/register')}
|
||||
sx={{ cursor: 'pointer', color: '#0d47a1', fontSize: '30px' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [user, setUser] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
setUser(JSON.parse(storedUser));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useGSAP(() => {
|
||||
gsap.from('#mainheading', { opacity: 0.3, ease: 'power2.inOut', duration: 0.5 });
|
||||
gsap.from('#secondheading', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.3, duration: 0.5 });
|
||||
gsap.from('#button', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.5, duration: 0.5 });
|
||||
gsap.from('#features', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.7, duration: 0.5 });
|
||||
gsap.from('#arrow', { opacity: 0, ease: 'power2.inOut', delay: 2, duration: 0.2 });
|
||||
gsap.to('#button', { opacity: 1, ease: 'power2.inOut', delay: 2.5, duration: 0.5 });
|
||||
}, []);
|
||||
|
||||
const handleGetStartedClick = () => {
|
||||
if (!user) {
|
||||
navigate('/register');
|
||||
} else {
|
||||
navigate('/dashboard');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ backgroundColor: '#d0e7ff' }} className="min-h-screen flex flex-col">
|
||||
<div className="flex-grow flex flex-col items-center justify-center bg-gradient-to-b text-gray-800 p-4 pt-20">
|
||||
<Navbar user={user} setUser={setUser} />
|
||||
|
||||
<div className="flex flex-col items-center">
|
||||
<h1
|
||||
id="mainheading"
|
||||
className="text-4xl flex items-center sm:text-5xl md:text-6xl font-semibold mb-4 text-center text-dark-blue"
|
||||
>
|
||||
AI Assistant for Your Health
|
||||
</h1>
|
||||
|
||||
<p
|
||||
id="secondheading"
|
||||
className="text-base sm:text-lg md:text-xl text-center max-w-2xl mb-8 text-gray-700"
|
||||
>
|
||||
A solution for personalized health support, including tailored medication guidance and wellness
|
||||
insights. Take care of yourself with the power of modern technology.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-6 mb-10" id="features">
|
||||
<div className="bg-white p-6 rounded-lg max-w-xs text-center shadow-md">
|
||||
<h3 className="text-xl font-medium mb-3 text-dark-blue">Personalized Prescription</h3>
|
||||
<p className="text-gray-600">
|
||||
Receive tailored medication recommendations specifically designed for your needs.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg max-w-xs text-center shadow-md">
|
||||
<h3 className="text-xl font-medium mb-3 text-dark-blue">Health Monitoring</h3>
|
||||
<p className="text-gray-600">
|
||||
Stay informed about your health with real-time monitoring and AI-driven insights.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg max-w-xs text-center shadow-md">
|
||||
<h3 className="text-xl font-medium mb-3 text-dark-blue">Advanced AI Support</h3>
|
||||
<p className="text-gray-600">
|
||||
Utilize AI support to ensure you're following the best routines for a healthier lifestyle.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="arrow" className="flex flex-col items-center mt-10 z-0">
|
||||
<p className="text-gray-600">Try it out</p>
|
||||
<BouncingArrow />
|
||||
</div>
|
||||
|
||||
<div id="button-wrapper" className="flex justify-center mt-6">
|
||||
<button
|
||||
id="button"
|
||||
onClick={handleGetStartedClick}
|
||||
className="bg-bright-blue text-white font-medium py-2 px-5 rounded hover:bg-deep-blue transition duration-300 shadow-md"
|
||||
>
|
||||
Get started
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className="text-center text-gray-500 p-4">
|
||||
<p>© {new Date().getFullYear()} Health AI. All rights reserved.</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Home, Navbar };
|
||||
|
@ -1,33 +1,33 @@
|
||||
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"
|
||||
|
||||
|
||||
// type chatQuestion = {
|
||||
|
||||
// }
|
||||
|
||||
const chatApi = createApi({
|
||||
reducerPath: 'chat',
|
||||
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:5000' }),
|
||||
endpoints: (builder) => ({
|
||||
sendTestVersion: builder.query<string, any>({
|
||||
query: (body) => ({
|
||||
url: '/create-answer',
|
||||
method: 'POST',
|
||||
body: body
|
||||
}),
|
||||
transformResponse: ({ response }) => response
|
||||
}),
|
||||
sendChatQuestion: builder.query<any, any>({
|
||||
query: (body) => ({
|
||||
url: '/api/chat',
|
||||
method: 'POST',
|
||||
body: body
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
||||
export default chatApi
|
||||
export const { useLazySendTestVersionQuery,useLazySendChatQuestionQuery } = chatApi
|
||||
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"
|
||||
|
||||
|
||||
// type chatQuestion = {
|
||||
|
||||
// }
|
||||
|
||||
const chatApi = createApi({
|
||||
reducerPath: 'chat',
|
||||
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:5000' }),
|
||||
endpoints: (builder) => ({
|
||||
sendTestVersion: builder.query<string, any>({
|
||||
query: (body) => ({
|
||||
url: '/create-answer',
|
||||
method: 'POST',
|
||||
body: body
|
||||
}),
|
||||
transformResponse: ({ response }) => response
|
||||
}),
|
||||
sendChatQuestion: builder.query<any, any>({
|
||||
query: (body) => ({
|
||||
url: '/api/chat',
|
||||
method: 'POST',
|
||||
body: body
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
||||
export default chatApi
|
||||
export const { useLazySendTestVersionQuery,useLazySendChatQuestionQuery } = chatApi
|
||||
|
@ -1,19 +1,19 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import chatApi from "./api/chatApi";
|
||||
import { setupListeners } from '@reduxjs/toolkit/query';
|
||||
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
[chatApi.reducerPath]: chatApi.reducer
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(chatApi.middleware),
|
||||
})
|
||||
|
||||
setupListeners(store.dispatch);
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import chatApi from "./api/chatApi";
|
||||
import { setupListeners } from '@reduxjs/toolkit/query';
|
||||
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
[chatApi.reducerPath]: chatApi.reducer
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(chatApi.middleware),
|
||||
})
|
||||
|
||||
setupListeners(store.dispatch);
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
|
||||
export default store;
|
2
frontend/src/vite-env.d.ts
vendored
2
frontend/src/vite-env.d.ts
vendored
@ -1 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite/client" />
|
||||
|
@ -1,22 +1,22 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'light-blue': '#dbeafe',
|
||||
'soft-blue': '#bfdbfe',
|
||||
'light-cyan': '#e0f7fa',
|
||||
'dark-blue': '#1e3a8a',
|
||||
'bright-blue': '#2563eb',
|
||||
'deep-blue': '#1d4ed8',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'light-blue': '#dbeafe',
|
||||
'soft-blue': '#bfdbfe',
|
||||
'light-cyan': '#e0f7fa',
|
||||
'dark-blue': '#1e3a8a',
|
||||
'bright-blue': '#2563eb',
|
||||
'deep-blue': '#1d4ed8',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
|
@ -1,25 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
|
@ -1,23 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user