upd project, decker func, upd frontend/backend/agent
This commit is contained in:
parent
677ae05159
commit
5b6d7728ce
@ -1,17 +1,17 @@
|
|||||||
# Используем базовый образ Python
|
|
||||||
FROM python:3.12
|
FROM python:3.12
|
||||||
|
|
||||||
# Устанавливаем рабочую директорию в контейнере
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Копируем все файлы проекта в контейнер
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Устанавливаем зависимости из requirements.txt
|
|
||||||
RUN pip install -r requirements.txt
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
# Делаем скрипт ожидания исполняемым
|
|
||||||
RUN chmod +x wait-for-elasticsearch.sh
|
RUN chmod +x wait-for-elasticsearch.sh
|
||||||
|
|
||||||
# Запускаем скрипт ожидания перед запуском бэкенда
|
|
||||||
CMD ["./wait-for-elasticsearch.sh", "python", "server.py"]
|
CMD ["./wait-for-elasticsearch.sh", "python", "server.py"]
|
||||||
|
Binary file not shown.
@ -30,10 +30,10 @@ def index_documents(data):
|
|||||||
'full_data': item
|
'full_data': item
|
||||||
})
|
})
|
||||||
|
|
||||||
sys.stdout.write(f"\rПроиндексировано {i} из {total_documents} документов")
|
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
print("\nИндексирование завершено.")
|
|
||||||
|
|
||||||
|
|
||||||
data_path = "../../data_adc_databaza/cleaned_general_info_additional.json"
|
data_path = "../../data_adc_databaza/cleaned_general_info_additional.json"
|
||||||
|
@ -8,7 +8,7 @@ es = Elasticsearch([{'host': 'localhost', 'port': 9200, 'scheme': 'http'}])
|
|||||||
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
||||||
|
|
||||||
def create_index():
|
def create_index():
|
||||||
# Определяем маппинг для индекса
|
|
||||||
mapping = {
|
mapping = {
|
||||||
"mappings": {
|
"mappings": {
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -18,11 +18,11 @@ def create_index():
|
|||||||
},
|
},
|
||||||
"vector": {
|
"vector": {
|
||||||
"type": "dense_vector",
|
"type": "dense_vector",
|
||||||
"dims": 384 # Размерность векторного представления
|
"dims": 384
|
||||||
},
|
},
|
||||||
"full_data": {
|
"full_data": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"enabled": False # Отключаем индексацию вложенных данных
|
"enabled": False
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,19 +53,15 @@ def index_documents(data):
|
|||||||
}
|
}
|
||||||
actions.append(action)
|
actions.append(action)
|
||||||
|
|
||||||
# Отображение прогресса
|
|
||||||
print(f"Индексируется документ {i}/{total_docs}", end='\r')
|
|
||||||
|
|
||||||
# Опционально: индексируем пакетами по N документов
|
|
||||||
if i % 100 == 0 or i == total_docs:
|
if i % 100 == 0 or i == total_docs:
|
||||||
bulk(es, actions)
|
bulk(es, actions)
|
||||||
actions = []
|
actions = []
|
||||||
|
|
||||||
# Если остались неиндексированные документы
|
|
||||||
if actions:
|
if actions:
|
||||||
bulk(es, actions)
|
bulk(es, actions)
|
||||||
|
|
||||||
print("\nИндексирование завершено.")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
create_index()
|
create_index()
|
||||||
|
583
Backend/model.py
583
Backend/model.py
@ -3,20 +3,18 @@ import requests
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
import difflib
|
|
||||||
from requests.exceptions import HTTPError
|
from requests.exceptions import HTTPError
|
||||||
from elasticsearch import Elasticsearch
|
from elasticsearch import Elasticsearch
|
||||||
from langchain.chains import SequentialChain
|
|
||||||
from langchain.chains import LLMChain, SequentialChain
|
|
||||||
from langchain_huggingface import HuggingFaceEmbeddings
|
from langchain_huggingface import HuggingFaceEmbeddings
|
||||||
from langchain_elasticsearch import ElasticsearchStore
|
from langchain_elasticsearch import ElasticsearchStore
|
||||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||||
from langchain.docstore.document import Document
|
import psycopg2
|
||||||
# from googletrans import Translator
|
from psycopg2.extras import RealDictCursor
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
config_file_path = "config.json"
|
config_file_path = "config.json"
|
||||||
with open(config_file_path, 'r') as config_file:
|
with open(config_file_path, 'r') as config_file:
|
||||||
config = json.load(config_file)
|
config = json.load(config_file)
|
||||||
@ -26,69 +24,90 @@ if not mistral_api_key:
|
|||||||
raise ValueError("Mistral API key not found in configuration.")
|
raise ValueError("Mistral API key not found in configuration.")
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# translate all answer to slovak(temporary closed :) ) #
|
# Simple functions for translation (stub)
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# translator = Translator()
|
|
||||||
def translate_to_slovak(text: str) -> str:
|
def translate_to_slovak(text: str) -> str:
|
||||||
"""
|
|
||||||
Переводит весь текст на словацкий с логированием изменений.
|
|
||||||
Сейчас функция является заглушкой и возвращает исходный текст без изменений.
|
|
||||||
"""
|
|
||||||
# if not text.strip():
|
|
||||||
# return text
|
|
||||||
#
|
|
||||||
# logger.info("Translation - Before: " + text)
|
|
||||||
# try:
|
|
||||||
# mid_result = translator.translate(text, src='auto', dest='en').text
|
|
||||||
# final_result = translator.translate(mid_result, src='en', dest='sk').text
|
|
||||||
# logger.info("Translation - After: " + final_result)
|
|
||||||
# before_words = text.split()
|
|
||||||
# after_words = final_result.split()
|
|
||||||
# diff = list(difflib.ndiff(before_words, after_words))
|
|
||||||
# changed_words = [word[2:] for word in diff if word.startswith('+ ')]
|
|
||||||
# if changed_words:
|
|
||||||
# logger.info("Changed words: " + ", ".join(changed_words))
|
|
||||||
# else:
|
|
||||||
# logger.info("No changed words detected.")
|
|
||||||
# return final_result
|
|
||||||
# except Exception as e:
|
|
||||||
# logger.error(f"Translation error: {e}")
|
|
||||||
# return text
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# Функция перевода описания лекарства с сохранением названия (до двоеточия) #
|
|
||||||
###############################################################################
|
|
||||||
def translate_preserving_medicine_names(text: str) -> str:
|
def translate_preserving_medicine_names(text: str) -> str:
|
||||||
"""
|
|
||||||
Ищет строки вида "номер. Название лекарства: описание..." и переводит только описание,
|
|
||||||
оставляя название без изменений.
|
|
||||||
Сейчас функция является заглушкой и возвращает исходный текст без изменений.
|
|
||||||
"""
|
|
||||||
# pattern = re.compile(r'^(\d+\.\s*[^:]+:\s*)(.*)$', re.MULTILINE)
|
|
||||||
#
|
|
||||||
# def replacer(match):
|
|
||||||
# prefix = match.group(1)
|
|
||||||
# description = match.group(2)
|
|
||||||
# logger.info("Translating description: " + description)
|
|
||||||
# translated_description = translate_to_slovak(description)
|
|
||||||
# logger.info("Translated description: " + translated_description)
|
|
||||||
# diff = list(difflib.ndiff(description.split(), translated_description.split()))
|
|
||||||
# changed_words = [word[2:] for word in diff if word.startswith('+ ')]
|
|
||||||
# if changed_words:
|
|
||||||
# logger.info("Changed words in description: " + ", ".join(changed_words))
|
|
||||||
# else:
|
|
||||||
# logger.info("No changed words in description detected.")
|
|
||||||
# return prefix + translated_description
|
|
||||||
#
|
|
||||||
# if pattern.search(text):
|
|
||||||
# return pattern.sub(replacer, text)
|
|
||||||
# else:
|
|
||||||
# return translate_to_slovak(text)
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Custom Mistral LLM #
|
# Function for evaluating the completeness of the answer
|
||||||
|
###############################################################################
|
||||||
|
def evaluate_complete_answer(query: str, answer: str) -> dict:
|
||||||
|
evaluation_prompt = (
|
||||||
|
f"Vyhodnoť nasledujúcu odpoveď na základe týchto kritérií:\n"
|
||||||
|
f"1. Odpoveď obsahuje odporúčania liekov vrátane názvu, stručného vysvetlenia a, ak bolo žiadané, aj dávkovanie alebo čas užívania.\n"
|
||||||
|
f"2. Ak otázka obsahovala dodatočné požiadavky, odpoveď má samostatnú časť, ktorá tieto požiadavky rieši.\n\n"
|
||||||
|
f"Otázka: '{query}'\n"
|
||||||
|
f"Odpoveď: '{answer}'\n\n"
|
||||||
|
"Na základe týchto kritérií daj odpovedi hodnotenie od 0 do 10, kde 10 znamená, že odpoveď je úplne logická a obsahuje všetky požadované informácie. "
|
||||||
|
"Vráť len číslo."
|
||||||
|
)
|
||||||
|
score_str = llm_small.generate_text(prompt=evaluation_prompt, max_tokens=50, temperature=0.3)
|
||||||
|
try:
|
||||||
|
score = float(score_str.strip())
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error parsing evaluation score: {e}")
|
||||||
|
score = 0.0
|
||||||
|
return {"rating": round(score, 2), "explanation": "Evaluation based on required criteria."}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Function for validating the response logic
|
||||||
|
###############################################################################
|
||||||
|
def validate_answer_logic(query: str, answer: str) -> str:
|
||||||
|
validation_prompt = (
|
||||||
|
f"Otázka: '{query}'\n"
|
||||||
|
f"Odpoveď: '{answer}'\n\n"
|
||||||
|
"Analyzuj prosím túto odpoveď. Ak odpoveď neobsahuje všetky dodatočné informácie, na ktoré sa pýtal používateľ, "
|
||||||
|
"alebo ak odporúčania liekov nie sú úplné (napr. chýba dávkovanie alebo čas užívania, ak boli takéto požiadavky v otázke), "
|
||||||
|
"vytvor opravenú odpoveď, ktorá je logicky konzistentná s otázkou. "
|
||||||
|
"Odpovedz v slovenčine a iba čistou, konečnou odpoveďou bez ďalších komentárov."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
validated_answer = llm_small.generate_text(prompt=validation_prompt, max_tokens=800, temperature=0.5)
|
||||||
|
logger.info(f"Validated answer: {validated_answer}")
|
||||||
|
return validated_answer
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error during answer validation: {e}")
|
||||||
|
return answer
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Function for creating a dynamic prompt with information from documents
|
||||||
|
###############################################################################
|
||||||
|
def build_dynamic_prompt(query: str, documents: list) -> str:
|
||||||
|
documents_str = "\n".join(documents)
|
||||||
|
prompt = (
|
||||||
|
f"Otázka: '{query}'.\n"
|
||||||
|
"Na základe nasledujúcich informácií o liekoch:\n"
|
||||||
|
f"{documents_str}\n\n"
|
||||||
|
"Analyzuj uvedenú otázku a zisti, či obsahuje dodatočné požiadavky okrem odporúčania liekov. "
|
||||||
|
"Ak áno, v odpovedi najprv uveď odporúčané lieky – pre každý liek uveď jeho názov, stručné vysvetlenie a, ak je to relevantné, "
|
||||||
|
"odporúčané dávkovanie alebo čas užívania, a potom v ďalšej časti poskytn ú odpoveď na dodatočné požiadavky. "
|
||||||
|
"Odpovedaj priamo a ľudským, priateľským tónom v číslovanom zozname, bez zbytočných úvodných fráz. "
|
||||||
|
"Odpoveď musí byť v slovenčine. "
|
||||||
|
"Prosím, odpovedaj v priateľskom, zdvorilom a profesionálnom tóne, bez akýchkoľvek agresívnych či drzých výrazov."
|
||||||
|
)
|
||||||
|
return prompt
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Function to get user data from the database via endpoint /api/get_user_data
|
||||||
|
###############################################################################
|
||||||
|
def get_user_data_from_db(chat_id: str) -> str:
|
||||||
|
try:
|
||||||
|
response = requests.get("http://localhost:5000/api/get_user_data", params={"chatId": chat_id})
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
return data.get("user_data", "")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Nezískané user_data, status: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving user_data from DB: {e}", exc_info=True)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Class for calling Mistral LLM
|
||||||
###############################################################################
|
###############################################################################
|
||||||
class CustomMistralLLM:
|
class CustomMistralLLM:
|
||||||
def __init__(self, api_key: str, endpoint_url: str, model_name: str):
|
def __init__(self, api_key: str, endpoint_url: str, model_name: str):
|
||||||
@ -96,7 +115,7 @@ class CustomMistralLLM:
|
|||||||
self.endpoint_url = endpoint_url
|
self.endpoint_url = endpoint_url
|
||||||
self.model_name = model_name
|
self.model_name = model_name
|
||||||
|
|
||||||
def generate_text(self, prompt: str, max_tokens=512, temperature=0.7, retries=3, delay=2):
|
def generate_text(self, prompt: str, max_tokens=812, temperature=0.7, retries=3, delay=2):
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@ -123,25 +142,24 @@ class CustomMistralLLM:
|
|||||||
else:
|
else:
|
||||||
logger.error(f"HTTP Error: {e}")
|
logger.error(f"HTTP Error: {e}")
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
logger.error(f"Error: {str(e)}")
|
logger.error(f"Error: {str(ex)}")
|
||||||
raise e
|
raise ex
|
||||||
raise Exception("Reached maximum number of retries for API request")
|
raise Exception("Reached maximum number of retries for API request")
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Initialize embeddings and Elasticsearch store #
|
# Initialisation of Embeddings and Elasticsearch
|
||||||
###############################################################################
|
###############################################################################
|
||||||
logger.info("Loading HuggingFaceEmbeddings model...")
|
logger.info("Loading HuggingFaceEmbeddings model...")
|
||||||
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
||||||
|
|
||||||
index_name = 'drug_docs'
|
index_name = "drug_docs"
|
||||||
|
|
||||||
if config.get("useCloud", False):
|
if config.get("useCloud", False):
|
||||||
logger.info("Using cloud Elasticsearch.")
|
logger.info("Using cloud Elasticsearch.")
|
||||||
cloud_id = "tt:dXMtZWFzdC0yLmF3cy5lbGFzdGljLWNsb3VkLmNvbTo0NDMkOGM3ODQ0ZWVhZTEyNGY3NmFjNjQyNDFhNjI4NmVhYzMkZTI3YjlkNTQ0ODdhNGViNmEyMTcxMjMxNmJhMWI0ZGU="
|
cloud_id = "tt:dXMtZWFzdC0yLmF3cy5lbGFzdGljLWNsb3VkLmNvbTo0NDMkOGM3ODQ0ZWVhZTEyNGY3NmFjNjQyNDFhNjI4NmVhYzMkZTI3YjlkNTQ0ODdhNGViNmEyMTcxMjMxNmJhMWI0ZGU="
|
||||||
vectorstore = ElasticsearchStore(
|
vectorstore = ElasticsearchStore(
|
||||||
es_cloud_id=cloud_id,
|
es_cloud_id=cloud_id,
|
||||||
index_name='drug_docs',
|
index_name=index_name,
|
||||||
embedding=embeddings,
|
embedding=embeddings,
|
||||||
es_user="elastic",
|
es_user="elastic",
|
||||||
es_password="sSz2BEGv56JRNjGFwoQ191RJ"
|
es_password="sSz2BEGv56JRNjGFwoQ191RJ"
|
||||||
@ -149,22 +167,21 @@ if config.get("useCloud", False):
|
|||||||
else:
|
else:
|
||||||
logger.info("Using local Elasticsearch.")
|
logger.info("Using local Elasticsearch.")
|
||||||
vectorstore = ElasticsearchStore(
|
vectorstore = ElasticsearchStore(
|
||||||
es_url="http://localhost:9200",
|
es_url="http://elasticsearch:9200",
|
||||||
index_name=index_name,
|
index_name=index_name,
|
||||||
embedding=embeddings,
|
embedding=embeddings,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Connected to {'cloud' if config.get('useCloud', False) else 'local'} Elasticsearch.")
|
logger.info("Connected to Elasticsearch.")
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Initialize Mistral models (small & large) #
|
# Initialisation of LLM small & large
|
||||||
###############################################################################
|
###############################################################################
|
||||||
llm_small = CustomMistralLLM(
|
llm_small = CustomMistralLLM(
|
||||||
api_key=mistral_api_key,
|
api_key=mistral_api_key,
|
||||||
endpoint_url="https://api.mistral.ai/v1/chat/completions",
|
endpoint_url="https://api.mistral.ai/v1/chat/completions",
|
||||||
model_name="mistral-small-latest"
|
model_name="mistral-small-latest"
|
||||||
)
|
)
|
||||||
|
|
||||||
llm_large = CustomMistralLLM(
|
llm_large = CustomMistralLLM(
|
||||||
api_key=mistral_api_key,
|
api_key=mistral_api_key,
|
||||||
endpoint_url="https://api.mistral.ai/v1/chat/completions",
|
endpoint_url="https://api.mistral.ai/v1/chat/completions",
|
||||||
@ -172,144 +189,310 @@ llm_large = CustomMistralLLM(
|
|||||||
)
|
)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Helper function to evaluate model output #
|
# Request classification function: vyhladavanie vs. upresnenie
|
||||||
###############################################################################
|
###############################################################################
|
||||||
def evaluate_results(query, summaries, model_name):
|
def classify_query(query: str, chat_history: str = "") -> str:
|
||||||
query_keywords = query.split()
|
if not chat_history.strip():
|
||||||
total_score = 0
|
return "vyhladavanie"
|
||||||
explanation = []
|
prompt = (
|
||||||
for i, summary in enumerate(summaries):
|
"Ty si zdravotnícky expert, ktorý analyzuje otázky používateľov. "
|
||||||
length_score = min(len(summary) / 100, 10)
|
"Analyzuj nasledujúci dopyt a urči, či ide o dopyt na vyhľadanie liekov alebo "
|
||||||
total_score += length_score
|
"o upresnenie/doplnenie už poskytnutej odpovede.\n"
|
||||||
explanation.append(f"Document {i+1}: Length score - {length_score}")
|
"Ak dopyt obsahuje výrazy ako 'čo pit', 'aké lieky', 'odporuč liek', 'hľadám liek', "
|
||||||
keyword_matches = sum(1 for word in query_keywords if word.lower() in summary.lower())
|
"odpovedaj slovom 'vyhľadávanie'.\n"
|
||||||
keyword_score = min(keyword_matches * 2, 10)
|
"Ak dopyt slúži na upresnenie, napríklad obsahuje výrazy ako 'a nie na predpis', "
|
||||||
total_score += keyword_score
|
"'upresni', 'este raz', odpovedaj slovom 'upresnenie'.\n"
|
||||||
explanation.append(f"Document {i+1}: Keyword match score - {keyword_score}")
|
f"Dopyt: \"{query}\""
|
||||||
final_score = total_score / len(summaries) if summaries else 0
|
|
||||||
explanation_summary = "\n".join(explanation)
|
|
||||||
logger.info(f"Evaluation for model {model_name}: {final_score}/10")
|
|
||||||
logger.info(f"Explanation:\n{explanation_summary}")
|
|
||||||
return {"rating": round(final_score, 2), "explanation": explanation_summary}
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# validation of recieved answer is it correct for user question #
|
|
||||||
###############################################################################
|
|
||||||
def validate_answer_logic(query: str, answer: str) -> str:
|
|
||||||
"""
|
|
||||||
Проверяет, соответствует ли ответ логике вопроса.
|
|
||||||
Если, например, вопрос относится к ľudským liekom a obsahuje otázku na dávkovanie,
|
|
||||||
odpoveď musí obsahovať iba lieky vhodné pre ľudí s uvedením správneho dávkovania.
|
|
||||||
"""
|
|
||||||
validation_prompt = (
|
|
||||||
f"Otázka: '{query}'\n"
|
|
||||||
f"Odpoveď: '{answer}'\n\n"
|
|
||||||
"Analyzuj prosím túto odpoveď. Ak odpoveď obsahuje odporúčania liekov, ktoré nie sú vhodné pre ľudí, "
|
|
||||||
"alebo ak neobsahuje správne informácie o dávkovaní, oprav ju tak, aby bola logicky konzistentná s otázkou. "
|
|
||||||
"Odpoveď musí obsahovať iba lieky určené pre ľudí a pri potrebe aj presné informácie o dávkovaní (napr. v gramoch). "
|
|
||||||
"Ak je odpoveď logická a korektná, vráť pôvodnú odpoveď bez zmien. "
|
|
||||||
"Odpovedz v slovenčine a iba čistou, konečnou odpoveďou bez ďalších komentárov."
|
|
||||||
)
|
)
|
||||||
try:
|
classification = llm_small.generate_text(prompt=prompt, max_tokens=20, temperature=0.3)
|
||||||
validated_answer = llm_small.generate_text(prompt=validation_prompt, max_tokens=500, temperature=0.5)
|
classification = classification.strip().lower()
|
||||||
logger.info(f"Validated answer: {validated_answer}")
|
logger.info(f"Klasifikácia dopytu: {classification}")
|
||||||
return validated_answer
|
if "vyhládzanie" in classification or "vyhľadávanie" in classification:
|
||||||
except Exception as e:
|
return "vyhladavanie"
|
||||||
logger.error(f"Error during answer validation: {e}")
|
elif "upresnenie" in classification:
|
||||||
return answer
|
return "upresnenie"
|
||||||
|
return "vyhladavanie"
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Main function: process_query_with_mistral (Slovak prompt) #
|
# Template for upresnenie dopytu
|
||||||
###############################################################################
|
###############################################################################
|
||||||
def process_query_with_mistral(query, k=10):
|
def build_upresnenie_prompt_no_history(chat_history: str, user_query: str) -> str:
|
||||||
|
prompt = f"""
|
||||||
|
Ty si zdravotnícky expert. Máš k dispozícii históriu chatu a novú upresňujúcu otázku.
|
||||||
|
|
||||||
|
Ak v histórii chatu už existuje jasná odpoveď na túto upresňujúcu otázku, napíš:
|
||||||
|
"FOUND_IN_HISTORY: <ľudský vysvetľajúci text>"
|
||||||
|
|
||||||
|
Ak však v histórii chatu nie je dostatok informácií, napíš:
|
||||||
|
"NO_ANSWER_IN_HISTORY: <krátky vyhľadávací dotaz do Elasticsearch>"
|
||||||
|
|
||||||
|
V časti <krátky vyhľadávací dotaz> zahrň kľúčové slová z pôvodnej otázky aj z upresnenia.
|
||||||
|
|
||||||
|
=== ZAČIATOK HISTÓRIE CHatu ===
|
||||||
|
{chat_history}
|
||||||
|
=== KONIEC HISTÓRIE CHatu ===
|
||||||
|
|
||||||
|
Upresňujúca otázka od používateľa:
|
||||||
|
"{user_query}"
|
||||||
|
"""
|
||||||
|
return prompt
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Function for retrieving the last vyhladavacieho dopytu z histórie
|
||||||
|
###############################################################################
|
||||||
|
def extract_last_vyhladavacie_query(chat_history: str) -> str:
|
||||||
|
lines = chat_history.splitlines()
|
||||||
|
last_query = ""
|
||||||
|
for line in reversed(lines):
|
||||||
|
if line.startswith("User:"):
|
||||||
|
last_query = line[len("User:"):].strip()
|
||||||
|
break
|
||||||
|
return last_query
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Agent class for data storage: vek, anamneza, predpis, user_data, search_query
|
||||||
|
###############################################################################
|
||||||
|
class ConversationalAgent:
|
||||||
|
def __init__(self):
|
||||||
|
self.long_term_memory = {
|
||||||
|
"vek": None,
|
||||||
|
"anamneza": None,
|
||||||
|
"predpis": None,
|
||||||
|
"user_data": None,
|
||||||
|
"search_query": None
|
||||||
|
}
|
||||||
|
|
||||||
|
def update_memory(self, key, value):
|
||||||
|
self.long_term_memory[key] = value
|
||||||
|
|
||||||
|
def get_memory(self, key):
|
||||||
|
return self.long_term_memory.get(key, None)
|
||||||
|
|
||||||
|
def load_memory_from_history(self, chat_history: str):
|
||||||
|
memory_match = re.search(r"\[MEMORY\](.*?)\[/MEMORY\]", chat_history, re.DOTALL)
|
||||||
|
if memory_match:
|
||||||
|
try:
|
||||||
|
memory_data = json.loads(memory_match.group(1))
|
||||||
|
self.long_term_memory.update(memory_data)
|
||||||
|
logger.info(f"Nahraná pamäť z histórie: {self.long_term_memory}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Chyba pri načítaní pamäte: {e}")
|
||||||
|
|
||||||
|
def parse_user_info(self, query: str):
|
||||||
|
text_lower = query.lower()
|
||||||
|
if re.search(r"\d+\s*(rok(ov|y)?|years?)", text_lower):
|
||||||
|
self.update_memory("user_data", query)
|
||||||
|
age_match = re.search(r"(\d{1,3})\s*(rok(ov|y)?|years?)", text_lower)
|
||||||
|
if age_match:
|
||||||
|
self.update_memory("vek", age_match.group(1))
|
||||||
|
if ("nemá" in text_lower or "nema" in text_lower) and ("chronické" in text_lower or "alerg" in text_lower):
|
||||||
|
self.update_memory("anamneza", "Žiadne chronické ochorenia ani alergie")
|
||||||
|
elif (("chronické" in text_lower or "alerg" in text_lower) and ("má" in text_lower or "ma" in text_lower)):
|
||||||
|
self.update_memory("anamneza", "Má chronické ochorenie alebo alergie (nespecifikované)")
|
||||||
|
if "voľnopredajný" in text_lower:
|
||||||
|
self.update_memory("predpis", "volnopredajny")
|
||||||
|
elif "na predpis" in text_lower:
|
||||||
|
self.update_memory("predpis", "na predpis")
|
||||||
|
|
||||||
|
def analyze_input(self, query: str) -> dict:
|
||||||
|
self.parse_user_info(query)
|
||||||
|
missing_info = {}
|
||||||
|
if not self.get_memory("vek"):
|
||||||
|
missing_info["vek"] = "Prosím, uveďte vek pacienta."
|
||||||
|
if not self.get_memory("anamneza"):
|
||||||
|
missing_info["anamnéza"] = "Má pacient nejaké chronické ochorenia alebo alergie?"
|
||||||
|
if not self.get_memory("predpis"):
|
||||||
|
missing_info["predpis"] = "Ide o liek na predpis alebo voľnopredajný liek?"
|
||||||
|
return missing_info
|
||||||
|
|
||||||
|
def ask_follow_up(self, missing_info: dict) -> str:
|
||||||
|
return " ".join(missing_info.values())
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Main function process_query_with_mistral with updated logic
|
||||||
|
###############################################################################
|
||||||
|
CHAT_HISTORY_ENDPOINT = "http://localhost:5000/api/chat_history_detail"
|
||||||
|
|
||||||
|
def process_query_with_mistral(query: str, chat_id: str, chat_context: str, k=10):
|
||||||
logger.info("Processing query started.")
|
logger.info("Processing query started.")
|
||||||
|
|
||||||
|
|
||||||
|
chat_history = ""
|
||||||
|
if chat_context:
|
||||||
|
chat_history = chat_context
|
||||||
|
elif chat_id:
|
||||||
try:
|
try:
|
||||||
# --- Vector search ---
|
params = {"id": chat_id}
|
||||||
vector_results = vectorstore.similarity_search(query, k=k)
|
r = requests.get(CHAT_HISTORY_ENDPOINT, params=params)
|
||||||
vector_documents = [hit.metadata.get('text', '') for hit in vector_results]
|
if r.status_code == 200:
|
||||||
max_docs = 5
|
data = r.json()
|
||||||
max_doc_length = 1000
|
chat_data = data.get("chat", "")
|
||||||
vector_documents = [doc[:max_doc_length] for doc in vector_documents[:max_docs]]
|
if isinstance(chat_data, dict):
|
||||||
if vector_documents:
|
chat_history = chat_data.get("chat", "")
|
||||||
vector_prompt = (
|
|
||||||
f"Otázka: '{query}'.\n"
|
|
||||||
"Na základe nasledujúcich informácií o liekoch:\n"
|
|
||||||
f"{vector_documents}\n\n"
|
|
||||||
"Prosím, uveďte tri najvhodnejšie lieky alebo riešenia pre daný problém. "
|
|
||||||
"Pre každý liek uveďte jeho názov, stručné a jasné vysvetlenie, prečo je vhodný, a ak je to relevantné, "
|
|
||||||
"aj odporúčané dávkovanie (napr. v gramoch alebo v iných vhodných jednotkách). "
|
|
||||||
"Odpovedajte priamo a ľudským, priateľským tónom v číslovanom zozname, bez nepotrebných úvodných fráz. "
|
|
||||||
"Odpoveď musí byť v slovenčine."
|
|
||||||
)
|
|
||||||
summary_small_vector = llm_small.generate_text(prompt=vector_prompt, max_tokens=700, temperature=0.7)
|
|
||||||
summary_large_vector = llm_large.generate_text(prompt=vector_prompt, max_tokens=700, temperature=0.7)
|
|
||||||
splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
|
|
||||||
split_summary_small_vector = splitter.split_text(summary_small_vector)
|
|
||||||
split_summary_large_vector = splitter.split_text(summary_large_vector)
|
|
||||||
small_vector_eval = evaluate_results(query, split_summary_small_vector, 'Mistral Small')
|
|
||||||
large_vector_eval = evaluate_results(query, split_summary_large_vector, 'Mistral Large')
|
|
||||||
else:
|
else:
|
||||||
small_vector_eval = {"rating": 0, "explanation": "No results"}
|
chat_history = chat_data or ""
|
||||||
large_vector_eval = {"rating": 0, "explanation": "No results"}
|
logger.info(f"História chatu načítaná pre chatId: {chat_id}")
|
||||||
summary_small_vector = ""
|
|
||||||
summary_large_vector = ""
|
|
||||||
|
|
||||||
# --- Text search ---
|
|
||||||
es_results = vectorstore.client.search(
|
|
||||||
index=index_name,
|
|
||||||
body={"size": k, "query": {"match": {"text": query}}}
|
|
||||||
)
|
|
||||||
text_documents = [hit['_source'].get('text', '') for hit in es_results['hits']['hits']]
|
|
||||||
text_documents = [doc[:max_doc_length] for doc in text_documents[:max_docs]]
|
|
||||||
if text_documents:
|
|
||||||
text_prompt = (
|
|
||||||
f"Otázka: '{query}'.\n"
|
|
||||||
"Na základe nasledujúcich informácií o liekoch:\n"
|
|
||||||
f"{text_documents}\n\n"
|
|
||||||
"Prosím, uveďte tri najvhodnejšie lieky alebo riešenia pre daný problém. "
|
|
||||||
"Pre každý liek uveďte jeho názov, stručné a jasné vysvetlenie, prečo je vhodný, a ak je to relevantné, "
|
|
||||||
"aj odporúčané dávkovanie (napr. v gramoch alebo v iných vhodných jednotkách). "
|
|
||||||
"Odpovedajte priamo a ľudským, priateľským tónom v číslovanom zozname, bez nepotrebných úvodných fráz. "
|
|
||||||
"Odpoveď musí byť v slovenčine."
|
|
||||||
)
|
|
||||||
summary_small_text = llm_small.generate_text(prompt=text_prompt, max_tokens=700, temperature=0.7)
|
|
||||||
summary_large_text = llm_large.generate_text(prompt=text_prompt, max_tokens=700, temperature=0.7)
|
|
||||||
splitter_text = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
|
|
||||||
split_summary_small_text = splitter_text.split_text(summary_small_text)
|
|
||||||
split_summary_large_text = splitter_text.split_text(summary_large_text)
|
|
||||||
small_text_eval = evaluate_results(query, split_summary_small_text, 'Mistral Small')
|
|
||||||
large_text_eval = evaluate_results(query, split_summary_large_text, 'Mistral Large')
|
|
||||||
else:
|
else:
|
||||||
small_text_eval = {"rating": 0, "explanation": "No results"}
|
logger.warning(f"Nepodarilo sa načítať históriu (status {r.status_code}) pre chatId: {chat_id}")
|
||||||
large_text_eval = {"rating": 0, "explanation": "No results"}
|
|
||||||
summary_small_text = ""
|
|
||||||
summary_large_text = ""
|
|
||||||
|
|
||||||
# Porovnanie výsledkov a výber najlepšieho
|
|
||||||
all_results = [
|
|
||||||
{"eval": small_vector_eval, "summary": summary_small_vector, "model": "Mistral Small Vector"},
|
|
||||||
{"eval": large_vector_eval, "summary": summary_large_vector, "model": "Mistral Large Vector"},
|
|
||||||
{"eval": small_text_eval, "summary": summary_small_text, "model": "Mistral Small Text"},
|
|
||||||
{"eval": large_text_eval, "summary": summary_large_text, "model": "Mistral Large Text"},
|
|
||||||
]
|
|
||||||
best_result = max(all_results, key=lambda x: x["eval"]["rating"])
|
|
||||||
logger.info(f"Best result from model {best_result['model']} with score {best_result['eval']['rating']}.")
|
|
||||||
|
|
||||||
# Dodatočná kontrola logiky odpovede
|
|
||||||
validated_answer = validate_answer_logic(query, best_result["summary"])
|
|
||||||
|
|
||||||
|
|
||||||
polished_answer = translate_preserving_medicine_names(validated_answer)
|
|
||||||
return {
|
|
||||||
"best_answer": polished_answer,
|
|
||||||
"model": best_result["model"],
|
|
||||||
"rating": best_result["eval"]["rating"],
|
|
||||||
"explanation": best_result["eval"]["explanation"]
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error: {str(e)}")
|
logger.error(f"Chyba pri načítaní histórie: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
agent = ConversationalAgent()
|
||||||
|
if chat_history:
|
||||||
|
agent.load_memory_from_history(chat_history)
|
||||||
|
|
||||||
|
|
||||||
|
existing_user_data = ""
|
||||||
|
if chat_id:
|
||||||
|
existing_user_data = get_user_data_from_db(chat_id)
|
||||||
|
|
||||||
|
|
||||||
|
agent.parse_user_info(query)
|
||||||
|
missing_info = agent.analyze_input(query)
|
||||||
|
|
||||||
|
|
||||||
|
if not existing_user_data:
|
||||||
|
|
||||||
|
if "Prosím, uveďte vek pacienta" in chat_history:
|
||||||
|
if chat_id:
|
||||||
|
update_payload = {"chatId": chat_id, "userData": query}
|
||||||
|
try:
|
||||||
|
update_response = requests.post("http://localhost:5000/api/save_user_data", json=update_payload)
|
||||||
|
if update_response.status_code == 200:
|
||||||
|
logger.info("User data was successfully updated via endpoint /api/save_user_data (data question flag).")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Failed to update data (data question flag): {update_response.text}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error when updating user_data via endpoint (data question flag): {e}")
|
||||||
|
|
||||||
|
if missing_info:
|
||||||
|
logger.info(f"Chýbajúce informácie: {missing_info}")
|
||||||
|
combined_missing_text = " ".join(missing_info.values())
|
||||||
|
if query.strip() not in combined_missing_text:
|
||||||
|
if chat_id:
|
||||||
|
update_payload = {"chatId": chat_id, "userData": query}
|
||||||
|
try:
|
||||||
|
update_response = requests.post("http://localhost:5000/api/save_user_data", json=update_payload)
|
||||||
|
if update_response.status_code == 200:
|
||||||
|
logger.info("User data was successfully updated via endpoint /api/save_user_data.")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Failed to update the data: {update_response.text}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error when updating user_data via endpoint: {e}")
|
||||||
return {
|
return {
|
||||||
"best_answer": "An error occurred during query processing.",
|
"best_answer": combined_missing_text,
|
||||||
"error": str(e)
|
"model": "FollowUp (new chat)",
|
||||||
|
"rating": 0,
|
||||||
|
"explanation": "Additional data pre pokračovanie is required.",
|
||||||
|
"patient_data": query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
qtype = classify_query(query, chat_history)
|
||||||
|
logger.info(f"Typ dopytu: {qtype}")
|
||||||
|
logger.info(f"Chat context (snippet): {chat_history[:200]}...")
|
||||||
|
|
||||||
|
|
||||||
|
if qtype == "vyhladavanie":
|
||||||
|
user_data_db = get_user_data_from_db(chat_id)
|
||||||
|
if user_data_db:
|
||||||
|
query = query + " Udaje cloveka: " + user_data_db
|
||||||
|
agent.long_term_memory["search_query"] = query
|
||||||
|
|
||||||
|
|
||||||
|
if qtype == "upresnenie":
|
||||||
|
original_search = agent.long_term_memory.get("search_query")
|
||||||
|
if not original_search:
|
||||||
|
original_search = extract_last_vyhladavacie_query(chat_history)
|
||||||
|
if original_search is None:
|
||||||
|
original_search = ""
|
||||||
|
combined_query = (original_search + " " + query).strip()
|
||||||
|
user_data_db = get_user_data_from_db(chat_id)
|
||||||
|
if user_data_db:
|
||||||
|
combined_query += " Udaje cloveka: " + user_data_db
|
||||||
|
logger.info(f"Combined query for search: {combined_query}")
|
||||||
|
|
||||||
|
upres_prompt = build_upresnenie_prompt_no_history(chat_history, combined_query)
|
||||||
|
response_str = llm_small.generate_text(upres_prompt, max_tokens=1200, temperature=0.5)
|
||||||
|
normalized = response_str.strip()
|
||||||
|
logger.info(f"Upresnenie prompt response: {normalized}")
|
||||||
|
|
||||||
|
if re.match(r"(?i)^found_in_history:\s*", normalized):
|
||||||
|
logger.info("Zistený FOUND_IN_HISTORY – vykonávame vyhľadávanie s kombinovaným dopytom.")
|
||||||
|
elif re.match(r"(?i)^no_answer_in_history:\s*", normalized):
|
||||||
|
parts = re.split(r"(?i)^no_answer_in_history:\s*", normalized, maxsplit=1)
|
||||||
|
if len(parts) >= 2:
|
||||||
|
combined_query = parts[1].strip()
|
||||||
|
logger.info(f"Upravený vyhľadávací dopyт z NO_ANSWER_IN_HISTORY: {combined_query}")
|
||||||
|
|
||||||
|
vector_results = vectorstore.similarity_search(combined_query, k=k)
|
||||||
|
max_docs = 5
|
||||||
|
max_len = 1000
|
||||||
|
vector_docs = [hit.metadata.get("text", "")[:max_len] for hit in vector_results[:max_docs]]
|
||||||
|
if not vector_docs:
|
||||||
|
return {
|
||||||
|
"best_answer": "Ľutujem, nenašli sa žiadne relevantné informácie.",
|
||||||
|
"model": "Upresnenie-NoResults",
|
||||||
|
"rating": 0,
|
||||||
|
"explanation": "No results from search."
|
||||||
|
}
|
||||||
|
joined_docs = "\n".join(vector_docs)
|
||||||
|
final_prompt = (
|
||||||
|
f"Otázka: {combined_query}\n\n"
|
||||||
|
"Na základe týchto informácií:\n"
|
||||||
|
f"{joined_docs}\n\n"
|
||||||
|
"Vygeneruj odporúčanie liekov alebo vysvetlenie, ak je to relevantné."
|
||||||
|
)
|
||||||
|
ans_small = llm_small.generate_text(final_prompt, max_tokens=1200, temperature=0.7)
|
||||||
|
ans_large = llm_large.generate_text(final_prompt, max_tokens=1200, temperature=0.7)
|
||||||
|
val_small = validate_answer_logic(combined_query, ans_small)
|
||||||
|
val_large = validate_answer_logic(combined_query, ans_large)
|
||||||
|
eval_small = evaluate_complete_answer(combined_query, val_small)
|
||||||
|
eval_large = evaluate_complete_answer(combined_query, val_large)
|
||||||
|
candidates = [
|
||||||
|
{"summary": val_small, "eval": eval_small, "model": "Mistral Small"},
|
||||||
|
{"summary": val_large, "eval": eval_large, "model": "Mistral Large"},
|
||||||
|
]
|
||||||
|
best = max(candidates, key=lambda x: x["eval"]["rating"])
|
||||||
|
logger.info(f"Odpoveď od modelu {best['model']} má rating: {best['eval']['rating']}/10")
|
||||||
|
final_answer = translate_preserving_medicine_names(best["summary"])
|
||||||
|
memory_json = json.dumps(agent.long_term_memory)
|
||||||
|
memory_block = f"[MEMORY]{memory_json}[/MEMORY]"
|
||||||
|
final_answer_with_memory = final_answer + "\n\n"
|
||||||
|
return {
|
||||||
|
"best_answer": final_answer_with_memory,
|
||||||
|
"model": best["model"],
|
||||||
|
"rating": best["eval"]["rating"],
|
||||||
|
"explanation": best["eval"]["explanation"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
vector_results = vectorstore.similarity_search(query, k=k)
|
||||||
|
max_docs = 5
|
||||||
|
max_len = 1000
|
||||||
|
vector_docs = [hit.metadata.get("text", "")[:max_len] for hit in vector_results[:max_docs]]
|
||||||
|
if not vector_docs:
|
||||||
|
return {
|
||||||
|
"best_answer": "Ľutujem, nenašli sa žiadne relevantné informácie.",
|
||||||
|
"model": "Vyhladavanie-NoDocs",
|
||||||
|
"rating": 0,
|
||||||
|
"explanation": "No results"
|
||||||
|
}
|
||||||
|
joined_docs = "\n".join(vector_docs)
|
||||||
|
final_prompt = (
|
||||||
|
f"Otázka: {query}\n\n"
|
||||||
|
"Na základe týchto informácií:\n"
|
||||||
|
f"{joined_docs}\n\n"
|
||||||
|
"Vygeneruj odporúčanie liekov alebo vysvetlenie, ak je to relevantné."
|
||||||
|
)
|
||||||
|
answer = llm_small.generate_text(final_prompt, max_tokens=1200, temperature=0.7)
|
||||||
|
memory_json = json.dumps(agent.long_term_memory)
|
||||||
|
memory_block = f"[MEMORY]{memory_json}[/MEMORY]"
|
||||||
|
answer_with_memory = answer + "\n\n"
|
||||||
|
return {
|
||||||
|
"best_answer": answer_with_memory,
|
||||||
|
"model": "Vyhladavanie-Final",
|
||||||
|
"rating": 9,
|
||||||
|
"explanation": "Vyhľadávacia cesta"
|
||||||
|
}
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
import torch
|
|
||||||
import logging
|
|
||||||
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
||||||
from elasticsearch import Elasticsearch
|
|
||||||
|
|
||||||
# Настройка логирования
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Подключение к Elasticsearch
|
|
||||||
es = Elasticsearch(
|
|
||||||
["https://localhost:9200"],
|
|
||||||
basic_auth=("elastic", "S7DoO3ma=G=9USBPbqq3"), # Ваш пароль
|
|
||||||
verify_certs=False
|
|
||||||
)
|
|
||||||
index_name = 'drug_docs'
|
|
||||||
|
|
||||||
# Загрузка токенизатора и модели
|
|
||||||
model_name = "Qwen/Qwen2.5-7B-Instruct"
|
|
||||||
device = "cuda:0" if torch.cuda.is_available() else "cpu"
|
|
||||||
|
|
||||||
model = AutoModelForCausalLM.from_pretrained(
|
|
||||||
model_name,
|
|
||||||
torch_dtype="auto",
|
|
||||||
device_map="auto"
|
|
||||||
)
|
|
||||||
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
|
||||||
|
|
||||||
# Проверка наличия pad_token
|
|
||||||
if tokenizer.pad_token is None:
|
|
||||||
tokenizer.pad_token = tokenizer.eos_token
|
|
||||||
|
|
||||||
def text_search(query, k=10, max_doc_length=300, max_docs=3):
|
|
||||||
try:
|
|
||||||
es_results = es.search(
|
|
||||||
index=index_name,
|
|
||||||
body={"size": k, "query": {"match": {"text": query}}}
|
|
||||||
)
|
|
||||||
text_documents = [hit['_source'].get('text', '') for hit in es_results['hits']['hits']]
|
|
||||||
text_documents = [doc[:max_doc_length] for doc in text_documents[:max_docs]]
|
|
||||||
return text_documents
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка поиска: {str(e)}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Пример запроса для поиска
|
|
||||||
query = "čo piť pri horúčke"
|
|
||||||
text_documents = text_search(query)
|
|
||||||
|
|
||||||
# Обрезаем текст, если он превышает предел токенов модели
|
|
||||||
max_tokens_per_input = 1024 # Установим более низкое значение для max_tokens
|
|
||||||
context_text = ' '.join(text_documents)
|
|
||||||
input_text = (
|
|
||||||
f"Informácie o liekoch: {context_text[:max_tokens_per_input]}\n"
|
|
||||||
"Uveďte tri konkrétne lieky alebo riešenia s veľmi krátkym vysvetlením pre každý z nich.\n"
|
|
||||||
"Odpoveď v slovenčine:"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Токенизация входного текста
|
|
||||||
inputs = tokenizer(input_text, return_tensors="pt", max_length=max_tokens_per_input, truncation=True).to(device)
|
|
||||||
|
|
||||||
try:
|
|
||||||
generated_ids = model.generate(
|
|
||||||
inputs.input_ids,
|
|
||||||
attention_mask=inputs.attention_mask,
|
|
||||||
max_new_tokens=300, # Снижено значение
|
|
||||||
temperature=0.7,
|
|
||||||
top_k=50,
|
|
||||||
top_p=0.9,
|
|
||||||
do_sample=False, # Отключено семплирование для детерминированного вывода
|
|
||||||
pad_token_id=tokenizer.pad_token_id
|
|
||||||
)
|
|
||||||
response = tokenizer.decode(generated_ids[0], skip_special_tokens=True, errors='ignore')
|
|
||||||
print("Сгенерированный текст:", response)
|
|
||||||
except RuntimeError as e:
|
|
||||||
print(f"Произошла ошибка во время генерации: {e}")
|
|
||||||
|
|
@ -1,308 +0,0 @@
|
|||||||
"""A simple command-line interactive chat demo for Qwen2.5-Instruct model with left-padding using bos_token."""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import platform
|
|
||||||
import shutil
|
|
||||||
from copy import deepcopy
|
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
import torch
|
|
||||||
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
|
|
||||||
from transformers.trainer_utils import set_seed
|
|
||||||
|
|
||||||
DEFAULT_CKPT_PATH = "Qwen/Qwen2.5-7B-Instruct"
|
|
||||||
|
|
||||||
_WELCOME_MSG = """\
|
|
||||||
Welcome to use Qwen2.5-Instruct model, type text to start chat, type :h to show command help.
|
|
||||||
"""
|
|
||||||
_HELP_MSG = """\
|
|
||||||
Commands:
|
|
||||||
:help / :h Show this help message
|
|
||||||
:exit / :quit / :q Exit the demo
|
|
||||||
:clear / :cl Clear screen
|
|
||||||
:clear-history / :clh Clear history
|
|
||||||
:history / :his Show history
|
|
||||||
:seed Show current random seed
|
|
||||||
:seed <N> Set random seed to <N>
|
|
||||||
:conf Show current generation config
|
|
||||||
:conf <key>=<value> Change generation config
|
|
||||||
:reset-conf Reset generation config
|
|
||||||
"""
|
|
||||||
|
|
||||||
_ALL_COMMAND_NAMES = [
|
|
||||||
"help",
|
|
||||||
"h",
|
|
||||||
"exit",
|
|
||||||
"quit",
|
|
||||||
"q",
|
|
||||||
"clear",
|
|
||||||
"cl",
|
|
||||||
"clear-history",
|
|
||||||
"clh",
|
|
||||||
"history",
|
|
||||||
"his",
|
|
||||||
"seed",
|
|
||||||
"conf",
|
|
||||||
"reset-conf",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _setup_readline():
|
|
||||||
try:
|
|
||||||
import readline
|
|
||||||
except ImportError:
|
|
||||||
return
|
|
||||||
|
|
||||||
_matches = []
|
|
||||||
|
|
||||||
def _completer(text, state):
|
|
||||||
nonlocal _matches
|
|
||||||
|
|
||||||
if state == 0:
|
|
||||||
_matches = [
|
|
||||||
cmd_name for cmd_name in _ALL_COMMAND_NAMES if cmd_name.startswith(text)
|
|
||||||
]
|
|
||||||
if 0 <= state < len(_matches):
|
|
||||||
return _matches[state]
|
|
||||||
return None
|
|
||||||
|
|
||||||
readline.set_completer(_completer)
|
|
||||||
readline.parse_and_bind("tab: complete")
|
|
||||||
|
|
||||||
|
|
||||||
def _load_model_tokenizer(args):
|
|
||||||
tokenizer = AutoTokenizer.from_pretrained(
|
|
||||||
args.checkpoint_path,
|
|
||||||
resume_download=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set bos_token for left-padding
|
|
||||||
if tokenizer.pad_token is None:
|
|
||||||
tokenizer.pad_token = tokenizer.bos_token
|
|
||||||
|
|
||||||
device_map = "cpu" if args.cpu_only else "auto"
|
|
||||||
|
|
||||||
model = AutoModelForCausalLM.from_pretrained(
|
|
||||||
args.checkpoint_path,
|
|
||||||
torch_dtype="auto",
|
|
||||||
device_map=device_map,
|
|
||||||
resume_download=True,
|
|
||||||
).eval()
|
|
||||||
|
|
||||||
# Conservative generation config
|
|
||||||
model.generation_config.max_new_tokens = 256
|
|
||||||
model.generation_config.temperature = 0.7
|
|
||||||
model.generation_config.top_k = 50
|
|
||||||
model.generation_config.top_p = 0.9
|
|
||||||
model.generation_config.pad_token_id = tokenizer.pad_token_id
|
|
||||||
model.generation_config.eos_token_id = tokenizer.eos_token_id
|
|
||||||
model.generation_config.do_sample = False
|
|
||||||
|
|
||||||
return model, tokenizer
|
|
||||||
|
|
||||||
|
|
||||||
def _gc():
|
|
||||||
import gc
|
|
||||||
|
|
||||||
gc.collect()
|
|
||||||
if torch.cuda.is_available():
|
|
||||||
torch.cuda.empty_cache()
|
|
||||||
|
|
||||||
|
|
||||||
def _clear_screen():
|
|
||||||
if platform.system() == "Windows":
|
|
||||||
os.system("cls")
|
|
||||||
else:
|
|
||||||
os.system("clear")
|
|
||||||
|
|
||||||
|
|
||||||
def _print_history(history):
|
|
||||||
terminal_width = shutil.get_terminal_size()[0]
|
|
||||||
print(f"History ({len(history)})".center(terminal_width, "="))
|
|
||||||
for index, (query, response) in enumerate(history):
|
|
||||||
print(f"User[{index}]: {query}")
|
|
||||||
print(f"Qwen[{index}]: {response}")
|
|
||||||
print("=" * terminal_width)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_input() -> str:
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
message = input("User> ").strip()
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
print("[ERROR] Encoding error in input")
|
|
||||||
continue
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
exit(1)
|
|
||||||
if message:
|
|
||||||
return message
|
|
||||||
print("[ERROR] Query is empty")
|
|
||||||
|
|
||||||
|
|
||||||
def _chat_stream(model, tokenizer, query, history):
|
|
||||||
conversation = []
|
|
||||||
for query_h, response_h in history:
|
|
||||||
conversation.append({"role": "user", "content": query_h})
|
|
||||||
conversation.append({"role": "assistant", "content": response_h})
|
|
||||||
conversation.append({"role": "user", "content": query})
|
|
||||||
input_text = tokenizer.apply_chat_template(
|
|
||||||
conversation,
|
|
||||||
add_generation_prompt=True,
|
|
||||||
tokenize=False,
|
|
||||||
)
|
|
||||||
# Perform left-padding with bos_token
|
|
||||||
inputs = tokenizer(
|
|
||||||
[input_text],
|
|
||||||
return_tensors="pt",
|
|
||||||
padding="longest",
|
|
||||||
truncation=True,
|
|
||||||
pad_to_multiple_of=8,
|
|
||||||
max_length=1024,
|
|
||||||
add_special_tokens=False
|
|
||||||
).to(model.device)
|
|
||||||
|
|
||||||
# Update attention_mask for left-padding compatibility
|
|
||||||
inputs["attention_mask"] = inputs["attention_mask"].flip(dims=[1])
|
|
||||||
|
|
||||||
streamer = TextIteratorStreamer(
|
|
||||||
tokenizer=tokenizer, skip_prompt=True, timeout=60.0, skip_special_tokens=True
|
|
||||||
)
|
|
||||||
generation_kwargs = {
|
|
||||||
**inputs,
|
|
||||||
"streamer": streamer,
|
|
||||||
}
|
|
||||||
thread = Thread(target=model.generate, kwargs=generation_kwargs)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
for new_text in streamer:
|
|
||||||
yield new_text
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Qwen2.5-Instruct command-line interactive chat demo."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-c",
|
|
||||||
"--checkpoint-path",
|
|
||||||
type=str,
|
|
||||||
default=DEFAULT_CKPT_PATH,
|
|
||||||
help="Checkpoint name or path, default to %(default)r",
|
|
||||||
)
|
|
||||||
parser.add_argument("-s", "--seed", type=int, default=1234, help="Random seed")
|
|
||||||
parser.add_argument(
|
|
||||||
"--cpu-only", action="store_true", help="Run demo with CPU only"
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
history, response = [], ""
|
|
||||||
|
|
||||||
model, tokenizer = _load_model_tokenizer(args)
|
|
||||||
orig_gen_config = deepcopy(model.generation_config)
|
|
||||||
|
|
||||||
_setup_readline()
|
|
||||||
|
|
||||||
_clear_screen()
|
|
||||||
print(_WELCOME_MSG)
|
|
||||||
|
|
||||||
seed = args.seed
|
|
||||||
|
|
||||||
while True:
|
|
||||||
query = _get_input()
|
|
||||||
|
|
||||||
# Process commands.
|
|
||||||
if query.startswith(":"):
|
|
||||||
command_words = query[1:].strip().split()
|
|
||||||
if not command_words:
|
|
||||||
command = ""
|
|
||||||
else:
|
|
||||||
command = command_words[0]
|
|
||||||
|
|
||||||
if command in ["exit", "quit", "q"]:
|
|
||||||
break
|
|
||||||
elif command in ["clear", "cl"]:
|
|
||||||
_clear_screen()
|
|
||||||
print(_WELCOME_MSG)
|
|
||||||
_gc()
|
|
||||||
continue
|
|
||||||
elif command in ["clear-history", "clh"]:
|
|
||||||
print(f"[INFO] All {len(history)} history cleared")
|
|
||||||
history.clear()
|
|
||||||
_gc()
|
|
||||||
continue
|
|
||||||
elif command in ["help", "h"]:
|
|
||||||
print(_HELP_MSG)
|
|
||||||
continue
|
|
||||||
elif command in ["history", "his"]:
|
|
||||||
_print_history(history)
|
|
||||||
continue
|
|
||||||
elif command in ["seed"]:
|
|
||||||
if len(command_words) == 1:
|
|
||||||
print(f"[INFO] Current random seed: {seed}")
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
new_seed_s = command_words[1]
|
|
||||||
try:
|
|
||||||
new_seed = int(new_seed_s)
|
|
||||||
except ValueError:
|
|
||||||
print(
|
|
||||||
f"[WARNING] Fail to change random seed: {new_seed_s!r} is not a valid number"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print(f"[INFO] Random seed changed to {new_seed}")
|
|
||||||
seed = new_seed
|
|
||||||
continue
|
|
||||||
elif command in ["conf"]:
|
|
||||||
if len(command_words) == 1:
|
|
||||||
print(model.generation_config)
|
|
||||||
else:
|
|
||||||
for key_value_pairs_str in command_words[1:]:
|
|
||||||
eq_idx = key_value_pairs_str.find("=")
|
|
||||||
if eq_idx == -1:
|
|
||||||
print("[WARNING] format: <key>=<value>")
|
|
||||||
continue
|
|
||||||
conf_key, conf_value_str = (
|
|
||||||
key_value_pairs_str[:eq_idx],
|
|
||||||
key_value_pairs_str[eq_idx + 1 :],
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
conf_value = eval(conf_value_str)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f"[INFO] Change config: model.generation_config.{conf_key} = {conf_value}"
|
|
||||||
)
|
|
||||||
setattr(model.generation_config, conf_key, conf_value)
|
|
||||||
continue
|
|
||||||
elif command in ["reset-conf"]:
|
|
||||||
print("[INFO] Reset generation config")
|
|
||||||
model.generation_config = deepcopy(orig_gen_config)
|
|
||||||
print(model.generation_config)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Run chat.
|
|
||||||
set_seed(seed)
|
|
||||||
_clear_screen()
|
|
||||||
print(f"\nUser: {query}")
|
|
||||||
print(f"\nQwen: ", end="")
|
|
||||||
try:
|
|
||||||
partial_text = ""
|
|
||||||
for new_text in _chat_stream(model, tokenizer, query, history):
|
|
||||||
print(new_text, end="", flush=True)
|
|
||||||
partial_text += new_text
|
|
||||||
response = partial_text
|
|
||||||
print()
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("[WARNING] Generation interrupted")
|
|
||||||
continue
|
|
||||||
|
|
||||||
history.append((query, response))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -189,3 +189,8 @@ wrapt==1.16.0
|
|||||||
xformers==0.0.28.post1
|
xformers==0.0.28.post1
|
||||||
yarl==1.9.4
|
yarl==1.9.4
|
||||||
zipp==3.20.2
|
zipp==3.20.2
|
||||||
|
flask
|
||||||
|
flask-cors
|
||||||
|
psycopg2-binary
|
||||||
|
google-auth
|
||||||
|
|
||||||
|
@ -1,50 +1,76 @@
|
|||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
|
|
||||||
# Сохраняем оригинальную функцию time.time
|
|
||||||
_real_time = time.time
|
|
||||||
# Переопределяем time.time для смещения времени на 1 секунду назад
|
|
||||||
time.time = lambda: _real_time() - 1
|
|
||||||
|
|
||||||
from flask import Flask, request, jsonify
|
from flask import Flask, request, jsonify
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from google.oauth2 import id_token
|
from google.oauth2 import id_token
|
||||||
from google.auth.transport import requests
|
from google.auth.transport import requests as google_requests
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
# Импортируем функцию обработки из model.py
|
|
||||||
from model import process_query_with_mistral
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
from psycopg2.extras import RealDictCursor
|
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 = {
|
DATABASE_CONFIG = {
|
||||||
"dbname": "postgres",
|
"dbname": "HealthAIDB",
|
||||||
"user": "postgres",
|
"user": "postgres",
|
||||||
"password": "healthai!",
|
"password": "Oleg2005",
|
||||||
"host": "health-ai-user-db.cxeum6cmct3r.eu-west-1.rds.amazonaws.com",
|
"host": "postgres",
|
||||||
"port": 5432,
|
"port": 5432,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Подключение к базе данных
|
import logging
|
||||||
try:
|
|
||||||
conn = psycopg2.connect(**DATABASE_CONFIG)
|
|
||||||
print("Подключение к базе данных успешно установлено")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Ошибка подключения к базе данных: {e}")
|
|
||||||
conn = None
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Создаем Flask приложение
|
try:
|
||||||
app = Flask(__name__)
|
conn = psycopg2.connect(**DATABASE_CONFIG)
|
||||||
CORS(app, resources={r"/api/*": {"origins": "http://localhost:5173"}})
|
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": "*"}})
|
||||||
|
|
||||||
# Ваш Google Client ID
|
|
||||||
CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com"
|
CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com"
|
||||||
|
|
||||||
def save_user_to_db(name, email, google_id=None, password=None):
|
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:
|
try:
|
||||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
@ -56,94 +82,115 @@ def save_user_to_db(name, email, google_id=None, password=None):
|
|||||||
(name, email, google_id, password)
|
(name, email, google_id, password)
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
print(f"User {name} ({email}) saved successfully!")
|
logger.info(f"User {name} ({email}) saved successfully")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error saving user to database: {e}")
|
logger.error(f"Error saving user {name} ({email}) to database: {e}", exc_info=True)
|
||||||
|
|
||||||
# Эндпоинт для верификации токенов Google OAuth
|
|
||||||
@app.route('/api/verify', methods=['POST'])
|
@app.route('/api/verify', methods=['POST'])
|
||||||
def verify_token():
|
def verify_token():
|
||||||
|
logger.info("Received token verification request")
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
token = data.get('token')
|
token = data.get('token')
|
||||||
if not token:
|
if not token:
|
||||||
|
logger.warning("Token not provided in request")
|
||||||
return jsonify({'error': 'No token provided'}), 400
|
return jsonify({'error': 'No token provided'}), 400
|
||||||
try:
|
try:
|
||||||
id_info = id_token.verify_oauth2_token(token, requests.Request(), CLIENT_ID)
|
id_info = id_token.verify_oauth2_token(token, google_requests.Request(), CLIENT_ID)
|
||||||
user_email = id_info.get('email')
|
user_email = id_info.get('email')
|
||||||
user_name = id_info.get('name')
|
user_name = id_info.get('name')
|
||||||
google_id = id_info.get('sub') # Уникальный идентификатор пользователя Google
|
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)
|
save_user_to_db(name=user_name, email=user_email, google_id=google_id)
|
||||||
logger.info(f"User authenticated and saved: {user_name} ({user_email})")
|
|
||||||
return jsonify({'message': 'Authentication successful', 'user': {'email': user_email, 'name': user_name}}), 200
|
return jsonify({'message': 'Authentication successful', 'user': {'email': user_email, 'name': user_name}}), 200
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.error(f"Token verification failed: {e}")
|
logger.error(f"Token verification error: {e}", exc_info=True)
|
||||||
return jsonify({'error': 'Invalid token'}), 400
|
return jsonify({'error': 'Invalid token'}), 400
|
||||||
|
|
||||||
# Эндпоинт для регистрации пользователя с проверкой на дублирование
|
|
||||||
@app.route('/api/register', methods=['POST'])
|
@app.route('/api/register', methods=['POST'])
|
||||||
def register():
|
def register():
|
||||||
|
logger.info("Received new user registration request")
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
name = data.get('name')
|
name = data.get('name')
|
||||||
email = data.get('email')
|
email = data.get('email')
|
||||||
password = data.get('password') # Рекомендуется хэшировать пароль
|
password = data.get('password')
|
||||||
if not all([name, email, password]):
|
if not all([name, email, password]):
|
||||||
|
logger.warning("Not all required fields provided for registration")
|
||||||
return jsonify({'error': 'All fields are required'}), 400
|
return jsonify({'error': 'All fields are required'}), 400
|
||||||
try:
|
try:
|
||||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
|
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
|
||||||
existing_user = cur.fetchone()
|
existing_user = cur.fetchone()
|
||||||
if existing_user:
|
if existing_user:
|
||||||
|
logger.warning(f"User with email {email} already exists")
|
||||||
return jsonify({'error': 'User already exists'}), 409
|
return jsonify({'error': 'User already exists'}), 409
|
||||||
save_user_to_db(name=name, email=email, password=password)
|
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
|
return jsonify({'message': 'User registered successfully'}), 201
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"Error during user registration: {e}", exc_info=True)
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
# Эндпоинт для логина пользователя
|
|
||||||
@app.route('/api/login', methods=['POST'])
|
@app.route('/api/login', methods=['POST'])
|
||||||
def login():
|
def login():
|
||||||
|
logger.info("Received login request")
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
email = data.get('email')
|
email = data.get('email')
|
||||||
password = data.get('password')
|
password = data.get('password')
|
||||||
if not all([email, password]):
|
if not all([email, password]):
|
||||||
|
logger.warning("Email or password not provided")
|
||||||
return jsonify({'error': 'Email and password are required'}), 400
|
return jsonify({'error': 'Email and password are required'}), 400
|
||||||
try:
|
try:
|
||||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
|
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
|
||||||
user = cur.fetchone()
|
user = cur.fetchone()
|
||||||
if not user:
|
if not user or user.get('password') != password:
|
||||||
return jsonify({'error': 'Invalid credentials'}), 401
|
logger.warning(f"Invalid credentials for email: {email}")
|
||||||
if user.get('password') != password:
|
|
||||||
return jsonify({'error': 'Invalid credentials'}), 401
|
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
|
return jsonify({'message': 'Login successful', 'user': {'name': user.get('name'), 'email': user.get('email')}}), 200
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"Error during user login: {e}", exc_info=True)
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
# Объединённый эндпоинт для обработки запроса чата
|
|
||||||
@app.route('/api/chat', methods=['POST'])
|
@app.route('/api/chat', methods=['POST'])
|
||||||
def chat():
|
def chat():
|
||||||
|
logger.info("Received chat request")
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
query = data.get('query', '')
|
query = data.get('query', '')
|
||||||
user_email = data.get('email') # email пользователя (если передается)
|
user_email = data.get('email')
|
||||||
chat_id = data.get('chatId') # параметр для обновления существующего чата
|
chat_id = data.get('chatId')
|
||||||
|
|
||||||
if not query:
|
if not query:
|
||||||
|
logger.warning("No query provided")
|
||||||
return jsonify({'error': 'No query provided'}), 400
|
return jsonify({'error': 'No query provided'}), 400
|
||||||
|
|
||||||
# Вызов функции для обработки запроса (например, чат-бота)
|
logger.info(f"Processing request for chatId: {chat_id if chat_id else 'new chat'} | Query: {query}")
|
||||||
response_obj = process_query_with_mistral(query)
|
|
||||||
best_answer = ""
|
# Retrieve chat context from the database
|
||||||
if isinstance(response_obj, dict):
|
chat_context = ""
|
||||||
best_answer = response_obj.get("best_answer", "")
|
if chat_id:
|
||||||
else:
|
try:
|
||||||
best_answer = str(response_obj)
|
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}")
|
||||||
|
|
||||||
# Форматирование ответа с использованием re.sub
|
|
||||||
best_answer = re.sub(r'[*#]', '', 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'(\d\.\s)', r'\n\n\1', best_answer)
|
||||||
best_answer = re.sub(r':\s-', r':\n-', best_answer)
|
best_answer = re.sub(r':\s-', r':\n-', best_answer)
|
||||||
|
|
||||||
# Если chatId передан, обновляем существующий чат, иначе создаем новый чат
|
# Update or create chat_history record including user_data if available
|
||||||
if chat_id:
|
if chat_id:
|
||||||
try:
|
try:
|
||||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
@ -151,8 +198,13 @@ def chat():
|
|||||||
existing_chat = cur.fetchone()
|
existing_chat = cur.fetchone()
|
||||||
if existing_chat:
|
if existing_chat:
|
||||||
updated_chat = existing_chat['chat'] + f"\nUser: {query}\nBot: {best_answer}"
|
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))
|
cur.execute("UPDATE chat_history SET chat = %s WHERE id = %s", (updated_chat, chat_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
logger.info(f"Chat history for chatId {chat_id} updated successfully")
|
||||||
else:
|
else:
|
||||||
with conn.cursor(cursor_factory=RealDictCursor) as cur2:
|
with conn.cursor(cursor_factory=RealDictCursor) as cur2:
|
||||||
cur2.execute(
|
cur2.execute(
|
||||||
@ -162,7 +214,9 @@ def chat():
|
|||||||
new_chat_id = cur2.fetchone()['id']
|
new_chat_id = cur2.fetchone()['id']
|
||||||
conn.commit()
|
conn.commit()
|
||||||
chat_id = new_chat_id
|
chat_id = new_chat_id
|
||||||
|
logger.info(f"New chat created with chatId: {chat_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating/creating chat history: {e}", exc_info=True)
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
@ -174,45 +228,98 @@ def chat():
|
|||||||
new_chat_id = cur.fetchone()['id']
|
new_chat_id = cur.fetchone()['id']
|
||||||
conn.commit()
|
conn.commit()
|
||||||
chat_id = new_chat_id
|
chat_id = new_chat_id
|
||||||
|
logger.info(f"New chat created with chatId: {chat_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating new chat: {e}", exc_info=True)
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
# Возвращаем текстовый ответ и новый chatId, если чат был создан
|
return jsonify({'response': {'best_answer': best_answer, 'model': response_obj.get("model", ""), 'chatId': chat_id}}), 200
|
||||||
return jsonify({'response': {'best_answer': best_answer, 'model': 'Mistral Small Vector', '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'])
|
@app.route('/api/chat_history', methods=['GET'])
|
||||||
def get_chat_history():
|
def get_chat_history():
|
||||||
|
logger.info("Received request to get chat history")
|
||||||
user_email = request.args.get('email')
|
user_email = request.args.get('email')
|
||||||
if not user_email:
|
if not user_email:
|
||||||
return jsonify({'error': 'User email is required'}), 400
|
return jsonify({'error': 'User email is required'}), 400
|
||||||
try:
|
try:
|
||||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"SELECT id, chat, created_at FROM chat_history WHERE user_email = %s ORDER BY created_at DESC",
|
"SELECT id, chat, user_data, created_at FROM chat_history WHERE user_email = %s ORDER BY created_at DESC",
|
||||||
(user_email,)
|
(user_email,)
|
||||||
)
|
)
|
||||||
history = cur.fetchall()
|
history = cur.fetchall()
|
||||||
return jsonify({'history': history}), 200
|
return jsonify({'history': history}), 200
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
# Эндпоинт для получения деталей чата по ID
|
|
||||||
@app.route('/api/chat_history_detail', methods=['GET'])
|
@app.route('/api/chat_history_detail', methods=['GET'])
|
||||||
def chat_history_detail():
|
def chat_history_detail():
|
||||||
|
logger.info("Received request to get chat details")
|
||||||
chat_id = request.args.get('id')
|
chat_id = request.args.get('id')
|
||||||
if not chat_id:
|
if not chat_id:
|
||||||
return jsonify({'error': 'Chat id is required'}), 400
|
return jsonify({'error': 'Chat id is required'}), 400
|
||||||
try:
|
try:
|
||||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
cur.execute("SELECT id, chat, created_at FROM chat_history WHERE id = %s", (chat_id,))
|
cur.execute("SELECT id, chat, user_data, created_at FROM chat_history WHERE id = %s", (chat_id,))
|
||||||
chat = cur.fetchone()
|
chat = cur.fetchone()
|
||||||
if not chat:
|
if not chat:
|
||||||
return jsonify({'error': 'Chat not found'}), 404
|
return jsonify({'error': 'Chat not found'}), 404
|
||||||
return jsonify({'chat': chat}), 200
|
return jsonify({'chat': chat}), 200
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
logger.info("Starting Flask application")
|
||||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
|
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
from sentence_transformers import SentenceTransformer, util
|
from sentence_transformers import SentenceTransformer, util
|
||||||
|
|
||||||
# Загрузка модели из Hugging Face
|
|
||||||
model = SentenceTransformer("TUKE-DeutscheTelekom/slovakbert-skquad-mnlr") # Замените на ID нужной модели
|
|
||||||
|
|
||||||
# Пример предложений на словацком языке
|
model = SentenceTransformer("TUKE-DeutscheTelekom/slovakbert-skquad-mnlr")
|
||||||
|
|
||||||
|
|
||||||
sentences = [
|
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.",
|
"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?",
|
"V ktorom roku vznikol druhý drevený most cez záliv Zlatý roh?",
|
||||||
"Aká je priemerná dĺžka života v Eritrei?"
|
"Aká je priemerná dĺžka života v Eritrei?"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Получение эмбеддингов для каждого предложения
|
|
||||||
embeddings = model.encode(sentences)
|
embeddings = model.encode(sentences)
|
||||||
print("Shape of embeddings:", embeddings.shape) # Вывод формы эмбеддингов, например (3, 768)
|
print("Shape of embeddings:", embeddings.shape)
|
||||||
|
|
||||||
|
|
||||||
# Вычисление сходства между предложениями
|
|
||||||
similarities = util.cos_sim(embeddings, embeddings)
|
similarities = util.cos_sim(embeddings, embeddings)
|
||||||
print("Similarity matrix:\n", similarities)
|
print("Similarity matrix:\n", similarities)
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
echo "Ожидание готовности Elasticsearch..."
|
echo "Waiting for Elasticsearch..."
|
||||||
|
|
||||||
# Проверяем доступность Elasticsearch
|
|
||||||
while ! curl -s http://elasticsearch:9200 > /dev/null; do
|
while ! curl -s http://elasticsearch:9200 > /dev/null; do
|
||||||
sleep 5
|
sleep 5
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Elasticsearch готов. Запуск бэкенда..."
|
echo "Elasticsearch is ready. Starting backend..."
|
||||||
|
|
||||||
# Запускаем бэкенд
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
@ -21,6 +21,20 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
start_period: 40s
|
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:
|
backend:
|
||||||
container_name: backend_container
|
container_name: backend_container
|
||||||
build:
|
build:
|
||||||
@ -30,9 +44,12 @@ services:
|
|||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
environment:
|
environment:
|
||||||
- ELASTICSEARCH_HOST=http://elasticsearch:9200
|
- ELASTICSEARCH_HOST=http://elasticsearch:9200
|
||||||
|
- DATABASE_HOST=postgres
|
||||||
depends_on:
|
depends_on:
|
||||||
elasticsearch:
|
elasticsearch:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
postgres:
|
||||||
|
condition: service_started
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
|
|
||||||
@ -42,7 +59,7 @@ services:
|
|||||||
context: ./frontend
|
context: ./frontend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "5173:5173"
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
networks:
|
networks:
|
||||||
@ -51,3 +68,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
app-network:
|
app-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
FROM docker.elastic.co/elasticsearch/elasticsearch:8.14.3
|
FROM docker.elastic.co/elasticsearch/elasticsearch:8.14.3
|
||||||
|
|
||||||
# Отключаем безопасность для упрощения доступа
|
|
||||||
ENV discovery.type=single-node
|
ENV discovery.type=single-node
|
||||||
ENV xpack.security.enabled=false
|
ENV xpack.security.enabled=false
|
||||||
|
|
||||||
# Копируем проиндексированные данные в директорию данных Elasticsearch
|
|
||||||
COPY --chown=elasticsearch:elasticsearch data/ /usr/share/elasticsearch/data
|
COPY --chown=elasticsearch:elasticsearch data/ /usr/share/elasticsearch/data
|
||||||
|
|
||||||
# Устанавливаем права доступа
|
|
||||||
RUN chmod -R 0775 /usr/share/elasticsearch/data
|
RUN chmod -R 0775 /usr/share/elasticsearch/data
|
||||||
|
|
||||||
# Удаляем файлы блокировок (добавьте эти команды)
|
|
||||||
RUN find /usr/share/elasticsearch/data -type f -name "*.lock" -delete
|
RUN find /usr/share/elasticsearch/data -type f -name "*.lock" -delete
|
||||||
RUN rm -f /usr/share/elasticsearch/data/nodes/0/node.lock
|
RUN rm -f /usr/share/elasticsearch/data/nodes/0/node.lock
|
||||||
|
@ -1,26 +1,26 @@
|
|||||||
# Используем базовый образ Node.js
|
|
||||||
FROM node:18-alpine
|
FROM node:18-alpine
|
||||||
|
|
||||||
# Устанавливаем рабочую директорию
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Копируем package.json и package-lock.json
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Устанавливаем зависимости
|
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
# Копируем файлы проекта
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Сборка приложения
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Устанавливаем сервер для обслуживания статических файлов
|
|
||||||
RUN npm install -g serve
|
RUN npm install -g serve
|
||||||
|
|
||||||
# Открываем порт
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
# Запуск фронтенда
|
EXPOSE 5173
|
||||||
CMD ["serve", "-s", "dist", "-l", "3000"]
|
|
||||||
|
|
||||||
|
CMD ["serve", "-s", "dist", "-l", "5173"]
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import { BrowserRouter as Router, Route, Routes, Outlet } from 'react-router-dom';
|
import { BrowserRouter as Router, Route, Routes, Outlet } from 'react-router-dom';
|
||||||
import Navigation from './Components/Navigation';
|
import Navigation from './Components/Navigation';
|
||||||
import LandingPage from './pages/LandingPage';
|
import {Home} from './pages/LandingPage';
|
||||||
import RegistrationForm from "./Components/RegistrationForm";
|
import RegistrationForm from "./Components/RegistrationForm";
|
||||||
import LoginForm from "./Components/LoginForm";
|
import LoginForm from "./Components/LoginForm";
|
||||||
import ChatHistory from "./Components/ChatHistory";
|
import ChatHistory from "./Components/ChatHistory";
|
||||||
import HomePage from './pages/HomePage';
|
import HomePage from './pages/HomePage';
|
||||||
import NewChatPage from "./Components/NewChatPage";
|
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 = () => (
|
const Layout = () => (
|
||||||
<div className="flex w-full h-screen dark:bg-slate-200">
|
<div className="flex w-full h-screen dark:bg-slate-200">
|
||||||
@ -22,13 +25,14 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<LandingPage />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/register" element={<RegistrationForm />} />
|
<Route path="/register" element={<RegistrationForm />} />
|
||||||
<Route path="/login" element={<LoginForm />} />
|
<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="/dashboard" element={<Layout />}>
|
||||||
{/* Новый чат */}
|
|
||||||
<Route path="new-chat" element={<NewChatPage />} />
|
<Route path="new-chat" element={<NewChatPage />} />
|
||||||
{/* Существующий чат (после создания нового, URL обновится) */}
|
|
||||||
<Route path="chat/:id" element={<HomePage />} />
|
<Route path="chat/:id" element={<HomePage />} />
|
||||||
<Route path="history" element={<ChatHistory />} />
|
<Route path="history" element={<ChatHistory />} />
|
||||||
<Route index element={<HomePage />} />
|
<Route index element={<HomePage />} />
|
||||||
|
109
frontend/src/Components/About.tsx
Normal file
109
frontend/src/Components/About.tsx
Normal file
@ -0,0 +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;
|
@ -15,7 +15,6 @@ const ChatDetails: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chat && id) {
|
if (!chat && id) {
|
||||||
// Если данные не переданы через state, можно попробовать получить их с сервера
|
|
||||||
fetch(`http://localhost:5000/api/chat_history_detail?id=${encodeURIComponent(id)}`)
|
fetch(`http://localhost:5000/api/chat_history_detail?id=${encodeURIComponent(id)}`)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Box, Typography, Paper, IconButton } from '@mui/material';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
|
||||||
interface ChatHistoryItem {
|
interface ChatHistoryItem {
|
||||||
id: number;
|
id: number;
|
||||||
@ -32,48 +34,124 @@ const ChatHistory: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// При клике перенаправляем пользователя на /dashboard/chat/{chatId}
|
|
||||||
const handleClick = (item: ChatHistoryItem) => {
|
const handleClick = (item: ChatHistoryItem) => {
|
||||||
navigate(`/dashboard/chat/${item.id}`, { state: { selectedChat: item } });
|
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 (
|
return (
|
||||||
<div style={{ padding: '20px' }}>
|
<Box
|
||||||
<h1>Chat History</h1>
|
sx={{
|
||||||
{error && <p style={{ color: 'red' }}>{error}</p>}
|
width: '100%',
|
||||||
{history.length === 0 && !error ? (
|
height: '100vh',
|
||||||
<p>No chat history found.</p>
|
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>
|
||||||
) : (
|
) : (
|
||||||
<ul style={{ listStyleType: 'none', padding: 0 }}>
|
<Box sx={{ maxWidth: '800px', mx: 'auto' }}>
|
||||||
{history.map((item) => {
|
{history.length === 0 ? (
|
||||||
// Извлекаем первую строку из сохранённого чата.
|
<Typography
|
||||||
// Предполагаем, что чат хранится в формате: "User: <вопрос>\nBot: <ответ>\n..."
|
variant="body1"
|
||||||
|
sx={{ textAlign: 'center', color: '#424242' }}
|
||||||
|
>
|
||||||
|
No chat history found.
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
history.map((item) => {
|
||||||
const lines = item.chat.split("\n");
|
const lines = item.chat.split("\n");
|
||||||
let firstUserMessage = lines[0];
|
let firstUserMessage = lines[0];
|
||||||
if (firstUserMessage.startsWith("User:")) {
|
if (firstUserMessage.startsWith("User:")) {
|
||||||
firstUserMessage = firstUserMessage.replace("User:", "").trim();
|
firstUserMessage = firstUserMessage.replace("User:", "").trim();
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<li
|
<Paper
|
||||||
key={item.id}
|
key={item.id}
|
||||||
style={{
|
sx={{
|
||||||
marginBottom: '15px',
|
p: 2,
|
||||||
borderBottom: '1px solid #ccc',
|
mb: 2,
|
||||||
paddingBottom: '10px',
|
cursor: 'pointer',
|
||||||
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)}
|
onClick={() => handleClick(item)}
|
||||||
>
|
>
|
||||||
<div>
|
<Typography
|
||||||
<strong>{firstUserMessage}</strong>
|
variant="subtitle1"
|
||||||
</div>
|
sx={{ fontWeight: 'bold', color: '#0d47a1' }}
|
||||||
<small>{new Date(item.created_at).toLocaleString()}</small>
|
>
|
||||||
</li>
|
{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>
|
||||||
);
|
);
|
||||||
})}
|
})
|
||||||
</ul>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
129
frontend/src/Components/Contact.tsx
Normal file
129
frontend/src/Components/Contact.tsx
Normal file
@ -0,0 +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;
|
@ -1,100 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Form, Field } from 'react-final-form';
|
|
||||||
|
|
||||||
interface FormValues {
|
|
||||||
healthGoal: string;
|
|
||||||
dietType?: string;
|
|
||||||
exerciseLevel?: string;
|
|
||||||
hydrationGoal?: string;
|
|
||||||
userInput: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EatingForm: React.FC = () => {
|
|
||||||
const onSubmit = (values: FormValues) => {
|
|
||||||
console.log('Form values:', values);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form<FormValues>
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
render={({ handleSubmit, form }) => {
|
|
||||||
const healthGoal = form.getFieldState("healthGoal")?.value;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col items-center p-8 bg-gray-100 rounded-lg shadow-lg max-w-md mx-auto">
|
|
||||||
<h2 className="text-2xl font-semibold text-gray-800 mb-6">Select Your Health Goal</h2>
|
|
||||||
|
|
||||||
{/* Health Goal Selection */}
|
|
||||||
<div className="w-full mb-4">
|
|
||||||
<label className="text-gray-700 mb-2 block">Health Goal</label>
|
|
||||||
<Field<string> name="healthGoal" component="select" className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500">
|
|
||||||
<option value="">Select your goal</option>
|
|
||||||
<option value="weight_loss">Weight Loss</option>
|
|
||||||
<option value="muscle_gain">Muscle Gain</option>
|
|
||||||
<option value="improve_energy">Improve Energy</option>
|
|
||||||
<option value="enhance_focus">Enhance Focus</option>
|
|
||||||
<option value="general_health">General Health</option>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Dynamic Fields Based on Health Goal */}
|
|
||||||
{healthGoal === 'weight_loss' && (
|
|
||||||
<div className="w-full mb-4">
|
|
||||||
<label className="text-gray-700 mb-2 block">Diet Type</label>
|
|
||||||
<Field<string> name="dietType" component="select" className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500">
|
|
||||||
<option value="">Select diet type</option>
|
|
||||||
<option value="keto">Keto</option>
|
|
||||||
<option value="low_carb">Low Carb</option>
|
|
||||||
<option value="intermittent_fasting">Intermittent Fasting</option>
|
|
||||||
<option value="mediterranean">Mediterranean</option>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{healthGoal === 'muscle_gain' && (
|
|
||||||
<div className="w-full mb-4">
|
|
||||||
<label className="text-gray-700 mb-2 block">Exercise Level</label>
|
|
||||||
<Field<string> name="exerciseLevel" component="select" className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500">
|
|
||||||
<option value="">Select exercise level</option>
|
|
||||||
<option value="beginner">Beginner</option>
|
|
||||||
<option value="intermediate">Intermediate</option>
|
|
||||||
<option value="advanced">Advanced</option>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{healthGoal === 'improve_energy' && (
|
|
||||||
<div className="w-full mb-4">
|
|
||||||
<label className="text-gray-700 mb-2 block">Hydration Goal</label>
|
|
||||||
<Field<string> name="hydrationGoal" component="select" className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500">
|
|
||||||
<option value="">Select hydration goal</option>
|
|
||||||
<option value="2_liters">2 Liters</option>
|
|
||||||
<option value="3_liters">3 Liters</option>
|
|
||||||
<option value="4_liters">4 Liters</option>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* User Input */}
|
|
||||||
<div className="w-full mb-4">
|
|
||||||
<label className="text-gray-700 mb-2 block">Your Preferences</label>
|
|
||||||
<Field<string>
|
|
||||||
name="userInput"
|
|
||||||
component="input"
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter your preferences or comments"
|
|
||||||
className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" className="px-6 py-3 text-white bg-blue-500 rounded-lg hover:bg-blue-600 transition-colors">
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EatingForm;
|
|
@ -1,235 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
export type Muscle = 'neck' | 'chest' | 'biceps' | 'forearms' | 'quadriceps' | 'calves' | 'abs' | 'shoulders' | 'trapezius';
|
|
||||||
|
|
||||||
const MuscleDiagram: React.FC = () => {
|
|
||||||
const [highlightedMuscle, setHighlightedMuscle] = useState<Muscle | null>(null);
|
|
||||||
|
|
||||||
const handleMouseEnter = (muscle: Muscle) => {
|
|
||||||
setHighlightedMuscle(muscle);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
|
||||||
setHighlightedMuscle(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
// const handleMuscleClick = (muscle: Muscle) => {
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
|
|
||||||
const getDarkerColor = (baseColor: string) => {
|
|
||||||
const colorValue = parseInt(baseColor.slice(1), 16);
|
|
||||||
const r = Math.max((colorValue >> 16) - 20, 0);
|
|
||||||
const g = Math.max(((colorValue >> 8) & 0x00ff) - 20, 0);
|
|
||||||
const b = Math.max((colorValue & 0x0000ff) - 20, 0);
|
|
||||||
return `#${(r << 16 | g << 8 | b).toString(16).padStart(6, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const baseColor = "#f1c27d";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ textAlign: 'center', width: '200px', margin: '0 auto' }}>
|
|
||||||
<svg viewBox="0 0 200 400" width="200" height="400">
|
|
||||||
{/* Голова */}
|
|
||||||
<circle
|
|
||||||
cx="100"
|
|
||||||
cy="50"
|
|
||||||
r="30"
|
|
||||||
fill={baseColor}
|
|
||||||
stroke="black"
|
|
||||||
id="head"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Шея */}
|
|
||||||
<rect
|
|
||||||
x="90"
|
|
||||||
y="70"
|
|
||||||
width="20"
|
|
||||||
height="30"
|
|
||||||
fill={highlightedMuscle === 'neck' ? getDarkerColor(baseColor) : baseColor}
|
|
||||||
stroke="black"
|
|
||||||
id="neck"
|
|
||||||
onMouseEnter={() => handleMouseEnter('neck')}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Трапеции */}
|
|
||||||
<path
|
|
||||||
d="M70,100 Q100,60 130,100 L120,110 Q100,90 80,110 Z"
|
|
||||||
fill={highlightedMuscle === 'trapezius' ? getDarkerColor(baseColor) : baseColor}
|
|
||||||
stroke="black"
|
|
||||||
id="trapezius"
|
|
||||||
onMouseEnter={() => handleMouseEnter('trapezius')}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Грудные мышцы */}
|
|
||||||
<path
|
|
||||||
d="M70,100 L130,100 C135,125 135,125 130,150 Q100,170 70,150 C65,125 65,125 70,100 Z"
|
|
||||||
fill={highlightedMuscle === 'chest' ? getDarkerColor(baseColor) : baseColor}
|
|
||||||
stroke="black"
|
|
||||||
id="chest"
|
|
||||||
onMouseEnter={() => handleMouseEnter('chest')}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Плечи */}
|
|
||||||
<path
|
|
||||||
d="M70,100 L60,120 L70,130 L80,110 Z"
|
|
||||||
fill={highlightedMuscle === 'shoulders' ? getDarkerColor(baseColor) : baseColor}
|
|
||||||
stroke="black"
|
|
||||||
id="leftShoulder"
|
|
||||||
onMouseEnter={() => handleMouseEnter('shoulders')}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M130,100 L140,120 L130,130 L120,110 Z"
|
|
||||||
fill={highlightedMuscle === 'shoulders' ? getDarkerColor(baseColor) : baseColor}
|
|
||||||
stroke="black"
|
|
||||||
id="rightShoulder"
|
|
||||||
onMouseEnter={() => handleMouseEnter('shoulders')}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Бицепсы */}
|
|
||||||
<g transform="rotate(25,60,130)">
|
|
||||||
<rect
|
|
||||||
x="55"
|
|
||||||
y="125"
|
|
||||||
width="15"
|
|
||||||
height="35"
|
|
||||||
rx="5"
|
|
||||||
ry="5"
|
|
||||||
fill={highlightedMuscle === 'biceps' ? getDarkerColor(baseColor) : baseColor}
|
|
||||||
stroke="black"
|
|
||||||
id="leftBicep"
|
|
||||||
onMouseEnter={() => handleMouseEnter('biceps')}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g transform="rotate(-25,140,130)">
|
|
||||||
<rect
|
|
||||||
x="130"
|
|
||||||
y="125"
|
|
||||||
width="15"
|
|
||||||
height="35"
|
|
||||||
rx="5"
|
|
||||||
ry="5"
|
|
||||||
fill={highlightedMuscle === 'biceps' ? getDarkerColor(baseColor) : baseColor}
|
|
||||||
stroke="black"
|
|
||||||
id="rightBicep"
|
|
||||||
onMouseEnter={() => handleMouseEnter('biceps')}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
{/* Предплечья */}
|
|
||||||
<g transform="rotate(25,60,130)">
|
|
||||||
<rect
|
|
||||||
x="55"
|
|
||||||
y="160"
|
|
||||||
width="15"
|
|
||||||
height="35"
|
|
||||||
rx="5"
|
|
||||||
ry="5"
|
|
||||||
fill={highlightedMuscle === 'forearms' ? getDarkerColor(baseColor) : baseColor}
|
|
||||||
stroke="black"
|
|
||||||
id="leftForearm"
|
|
||||||
onMouseEnter={() => handleMouseEnter('forearms')}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g transform="rotate(-25,140,130)">
|
|
||||||
<rect
|
|
||||||
x="130"
|
|
||||||
y="160"
|
|
||||||
width="15"
|
|
||||||
height="35"
|
|
||||||
rx="5"
|
|
||||||
ry="5"
|
|
||||||
fill={highlightedMuscle === 'forearms' ? getDarkerColor(baseColor) : baseColor}
|
|
||||||
stroke="black"
|
|
||||||
id="rightForearm"
|
|
||||||
onMouseEnter={() => handleMouseEnter('forearms')}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
{/* Пресс */}
|
|
||||||
<path
|
|
||||||
d="M70,150 L130,150 L130,210 Q100,250 70,210 Z"
|
|
||||||
fill={highlightedMuscle === 'abs' ? getDarkerColor(baseColor) : baseColor}
|
|
||||||
stroke="black"
|
|
||||||
id="abs"
|
|
||||||
onMouseEnter={() => handleMouseEnter('abs')}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Квадрицепсы */}
|
|
||||||
<g transform="rotate(5,75,260)">
|
|
||||||
<ellipse
|
|
||||||
cx="75"
|
|
||||||
cy="260"
|
|
||||||
rx="15"
|
|
||||||
ry="35"
|
|
||||||
fill={highlightedMuscle === 'quadriceps' ? getDarkerColor(baseColor) : baseColor}
|
|
||||||
stroke="black"
|
|
||||||
id="leftQuadricep"
|
|
||||||
onMouseEnter={() => handleMouseEnter('quadriceps')}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g transform="rotate(-5,125,260)">
|
|
||||||
<ellipse
|
|
||||||
cx="125"
|
|
||||||
cy="260"
|
|
||||||
rx="15"
|
|
||||||
ry="35"
|
|
||||||
fill={highlightedMuscle === 'quadriceps' ? getDarkerColor(baseColor) : baseColor}
|
|
||||||
stroke="black"
|
|
||||||
id="rightQuadricep"
|
|
||||||
onMouseEnter={() => handleMouseEnter('quadriceps')}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
{/* Икроножные мышцы */}
|
|
||||||
<g transform="rotate(5,75,260)">
|
|
||||||
<ellipse
|
|
||||||
cx="75"
|
|
||||||
cy="325"
|
|
||||||
rx="12"
|
|
||||||
ry="30"
|
|
||||||
fill={highlightedMuscle === 'calves' ? getDarkerColor(baseColor) : baseColor}
|
|
||||||
stroke="black"
|
|
||||||
id="leftCalf"
|
|
||||||
onMouseEnter={() => handleMouseEnter('calves')}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g transform="rotate(-5,125,260)">
|
|
||||||
<ellipse
|
|
||||||
cx="125"
|
|
||||||
cy="325"
|
|
||||||
rx="12"
|
|
||||||
ry="30"
|
|
||||||
fill={highlightedMuscle === 'calves' ? getDarkerColor(baseColor) : baseColor}
|
|
||||||
stroke="black"
|
|
||||||
id="rightCalf"
|
|
||||||
onMouseEnter={() => handleMouseEnter('calves')}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
{/* Отображение названия мышечной группы */}
|
|
||||||
<div style={{ marginTop: '20px', fontSize: '18px' }}>
|
|
||||||
{highlightedMuscle
|
|
||||||
? `Выделено: ${highlightedMuscle}`
|
|
||||||
: 'Наведите на мышцу, чтобы увидеть название'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MuscleDiagram;
|
|
@ -10,12 +10,11 @@ const LoginFormContent: React.FC = () => {
|
|||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Приведение типов для получения значений инпутов
|
|
||||||
const emailElement = document.getElementById('email') as HTMLInputElement | null;
|
const emailElement = document.getElementById('email') as HTMLInputElement | null;
|
||||||
const passwordElement = document.getElementById('password') as HTMLInputElement | null;
|
const passwordElement = document.getElementById('password') as HTMLInputElement | null;
|
||||||
|
|
||||||
if (!emailElement || !passwordElement) {
|
if (!emailElement || !passwordElement) {
|
||||||
console.error('Один или несколько инпутов отсутствуют');
|
console.error('One or many inputs are missing');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,10 +40,10 @@ const LoginFormContent: React.FC = () => {
|
|||||||
localStorage.setItem('user', JSON.stringify(loggedInUser));
|
localStorage.setItem('user', JSON.stringify(loggedInUser));
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
} else {
|
} else {
|
||||||
console.error('Ошибка:', data.error);
|
console.error('Error:', data.error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при входе:', error);
|
console.error('Error loginning:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -64,14 +63,15 @@ const LoginFormContent: React.FC = () => {
|
|||||||
localStorage.setItem('user', JSON.stringify(loggedInUser));
|
localStorage.setItem('user', JSON.stringify(loggedInUser));
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка верификации токена:', error);
|
console.error('Error token verification:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGoogleLoginError = (error: any) => {
|
const handleGoogleLoginError = () => {
|
||||||
console.error('Ошибка аутентификации через Google:', error);
|
console.error('Error auth through Google');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@ -1,275 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { Form, Field } from "react-final-form";
|
|
||||||
import Slider from "@mui/material/Slider";
|
|
||||||
import { useLazySendTestVersionQuery } from "../store/api/chatApi";
|
|
||||||
import { LuLoader2 } from "react-icons/lu";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
interface FormValues {
|
|
||||||
age?: number;
|
|
||||||
height?: number;
|
|
||||||
weight?: number;
|
|
||||||
healthGoal?: string;
|
|
||||||
dietType?: string;
|
|
||||||
exerciseLevel?: string;
|
|
||||||
hydrationGoal?: string;
|
|
||||||
userInput?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const MultiStepForm: React.FC = () => {
|
|
||||||
const [formValues, setFormValues] = useState<FormValues>({});
|
|
||||||
const [stage, setStage] = useState<number>(1);
|
|
||||||
const [data, setData] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const [sendTestMessage, { isLoading, isFetching }] = useLazySendTestVersionQuery()
|
|
||||||
|
|
||||||
const nextStage = () => setStage((prev) => prev + 1);
|
|
||||||
const previousStage = () => setStage((prev) => prev - 1);
|
|
||||||
|
|
||||||
const saveFormData = (values: FormValues) => {
|
|
||||||
setFormValues((prev) => ({
|
|
||||||
...prev,
|
|
||||||
...values,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = (values: FormValues) => {
|
|
||||||
saveFormData(values);
|
|
||||||
nextStage();
|
|
||||||
};
|
|
||||||
console.log(isLoading)
|
|
||||||
|
|
||||||
const finalSubmit = async () => {
|
|
||||||
const res = await sendTestMessage(formValues).unwrap()
|
|
||||||
setData(res)
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectEmoji = (
|
|
||||||
value: number | undefined,
|
|
||||||
thresholds: number[],
|
|
||||||
emojis: string[]
|
|
||||||
) => {
|
|
||||||
if (value === undefined) return null;
|
|
||||||
if (value <= thresholds[0]) return emojis[0];
|
|
||||||
if (value <= thresholds[1]) return emojis[1];
|
|
||||||
return emojis[2];
|
|
||||||
};
|
|
||||||
|
|
||||||
return !data ? (
|
|
||||||
<div className="w-full max-w-md bg-white rounded-lg shadow-lg p-8 text-center">
|
|
||||||
|
|
||||||
<h1 className="text-2xl font-bold text-gray-700 mb-6">
|
|
||||||
Fill in your profile and get some advices
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<Form<FormValues>
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
initialValues={formValues}
|
|
||||||
render={({ handleSubmit, values }) => (
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
{stage === 1 && (<> <div>
|
|
||||||
<h2 className="text-xl font-semibold text-gray-600 mb-4">
|
|
||||||
Stage 1: Base information
|
|
||||||
</h2>
|
|
||||||
<div className="text-3xl mb-4">
|
|
||||||
{selectEmoji(values.age, [17, 50], ["👶", "🧑", "👴"])}
|
|
||||||
</div>
|
|
||||||
<Field
|
|
||||||
name="age"
|
|
||||||
parse={(value) => (value === "" ? 0 : Number(value))}
|
|
||||||
>
|
|
||||||
{({ input }) => (
|
|
||||||
<input
|
|
||||||
{...input}
|
|
||||||
type="number"
|
|
||||||
placeholder="Enter age"
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-indigo-500"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<div className="text-3xl mb-4">
|
|
||||||
{selectEmoji(values.height, [150, 175], ["🌱", "🌳", "🌲"])}
|
|
||||||
</div>
|
|
||||||
<Field
|
|
||||||
name="height"
|
|
||||||
parse={(value) => (value === "" ? 0 : Number(value))}
|
|
||||||
>
|
|
||||||
{({ input }) => (
|
|
||||||
<input
|
|
||||||
{...input}
|
|
||||||
type="number"
|
|
||||||
placeholder="Enter height"
|
|
||||||
min={0}
|
|
||||||
max={250}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-indigo-500"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div >
|
|
||||||
<h2 className="text-xl font-semibold text-gray-600 mb-4">
|
|
||||||
</h2>
|
|
||||||
<Field<string> name="dietType" component="select" className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500">
|
|
||||||
<option value="">Select your goal</option>
|
|
||||||
<option value="weight_loss">Weight Loss</option>
|
|
||||||
<option value="muscle_gain">Muscle Gain</option>
|
|
||||||
<option value="improve_energy">Improve Energy</option>
|
|
||||||
<option value="enhance_focus">Enhance Focus</option>
|
|
||||||
<option value="general_health">General Health</option>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="text-3xl mb-4">
|
|
||||||
{selectEmoji(values.weight, [70, 99], ["🐭", "🐱", "🐘"])}
|
|
||||||
</div>
|
|
||||||
<Field
|
|
||||||
name="weight"
|
|
||||||
parse={(value) => (value === "" ? 0 : Number(value))}
|
|
||||||
>
|
|
||||||
{({ input }) => (
|
|
||||||
<div>
|
|
||||||
<Slider
|
|
||||||
value={input.value || 0}
|
|
||||||
onChange={(_, value) =>
|
|
||||||
input.onChange(
|
|
||||||
Array.isArray(value) ? value[0] : value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
min={0}
|
|
||||||
max={200}
|
|
||||||
className="text-indigo-500"
|
|
||||||
/>
|
|
||||||
<div className="text-gray-600 mt-2">
|
|
||||||
Current Weight: {input.value || 0} kg
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<div className="flex justify-end">
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={nextStage}
|
|
||||||
className="px-4 py-2 bg-bright-blue text-white rounded-md hover:bg-indigo-500"
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>)}
|
|
||||||
|
|
||||||
|
|
||||||
{stage === 2 && (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-semibold text-gray-600 mb-4">
|
|
||||||
Stage 2: Details
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="text-start">
|
|
||||||
<div className="mb-4">
|
|
||||||
<label className="text-gray-700 mb-2 block">Diet Type</label>
|
|
||||||
<Field<string> name="dietType" component="select" className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500">
|
|
||||||
<option value="">Select diet type</option>
|
|
||||||
<option value="keto">Keto</option>
|
|
||||||
<option value="low_carb">Low Carb</option>
|
|
||||||
<option value="intermittent_fasting">Intermittent Fasting</option>
|
|
||||||
<option value="mediterranean">Mediterranean</option>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<label className="text-gray-700 mb-2 block">Exercise Level</label>
|
|
||||||
<Field<string> name="exerciseLevel" component="select" className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500">
|
|
||||||
<option value="">Select exercise level</option>
|
|
||||||
<option value="beginner">Beginner</option>
|
|
||||||
<option value="intermediate">Intermediate</option>
|
|
||||||
<option value="advanced">Advanced</option>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<label className="text-gray-700 mb-2 block">Hydration Goal</label>
|
|
||||||
<Field<string> name="hydrationGoal" component="select" className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500">
|
|
||||||
<option value="">Select hydration goal</option>
|
|
||||||
<option value="2_liters">2 Liters</option>
|
|
||||||
<option value="3_liters">3 Liters</option>
|
|
||||||
<option value="4_liters">4 Liters</option>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<label className="text-gray-700 mb-2 block">Your Preferences</label>
|
|
||||||
<Field<string> name="userInput" component="input" type="text" placeholder="Enter your preferences or comments" className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500" />
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<button type="button" onClick={previousStage} className="px-4 py-2 bg-gray-400 text-white rounded-md hover:bg-gray-500">
|
|
||||||
Previous
|
|
||||||
</button>
|
|
||||||
<button type="submit" className="px-4 py-2 bg-bright-blue text-white rounded-md hover:bg-indigo-500">
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{stage === 3 && (
|
|
||||||
<div className="text-start">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-600 mb-4 text-center">Summary</h2>
|
|
||||||
<p><strong>Age:</strong> {formValues.age}</p>
|
|
||||||
<p><strong>Height:</strong> {formValues.height} cm</p>
|
|
||||||
<p><strong>Weight:</strong> {formValues.weight} kg</p>
|
|
||||||
<p><strong>Health Goal:</strong> {formValues.healthGoal}</p>
|
|
||||||
<p><strong>Diet Type:</strong> {formValues.dietType || "Not specified"}</p>
|
|
||||||
<p><strong>Exercise Level:</strong> {formValues.exerciseLevel || "Not specified"}</p>
|
|
||||||
<p><strong>Hydration Goal:</strong> {formValues.hydrationGoal || "Not specified"}</p>
|
|
||||||
<p><strong>User Input:</strong> {formValues.userInput || "Not specified"}</p>
|
|
||||||
<div className="flex justify-between mt-4">
|
|
||||||
<button type="button" disabled={isLoading} onClick={previousStage} className="px-4 py-2 bg-gray-400 text-white rounded-md hover:bg-gray-500">
|
|
||||||
Previous
|
|
||||||
</button>
|
|
||||||
<button type="button" disabled={isLoading} onClick={finalSubmit} className="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600">
|
|
||||||
{isLoading || isFetching ? <LuLoader2 className="animate-spin" /> : 'Confirm'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (<div className="w-full flex flex-col items-center gap-6">
|
|
||||||
<h1 className="text-4xl flex items-center sm:text-5xl md:text-6xl font-semibold mb-4 text-center text-dark-blue">
|
|
||||||
Advices for your health
|
|
||||||
</h1>
|
|
||||||
<p className="w-1/2">{data}</p>
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<Link to='/dashboard'>
|
|
||||||
<button className="bg-bright-blue text-white font-medium py-2 px-5 rounded hover:bg-deep-blue transition duration-300 shadow-md">
|
|
||||||
Get started with full version
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
<button onClick={() => { setData(null), setStage(1) }} className="px-4 py-2 bg-gray-400 text-white rounded-md hover:bg-gray-500">
|
|
||||||
Try again
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MultiStepForm;
|
|
@ -2,9 +2,8 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Avatar from '@mui/material/Avatar';
|
import Avatar from '@mui/material/Avatar';
|
||||||
import { MdAddCircleOutline, MdOutlineDarkMode } from "react-icons/md";
|
import { MdAddCircleOutline } from "react-icons/md";
|
||||||
import { GoHistory } from "react-icons/go";
|
import { GoHistory } from "react-icons/go";
|
||||||
import { CiLight } from "react-icons/ci";
|
|
||||||
import { CgLogIn } from "react-icons/cg";
|
import { CgLogIn } from "react-icons/cg";
|
||||||
import BackImage from '../assets/smallheadicon.png';
|
import BackImage from '../assets/smallheadicon.png';
|
||||||
|
|
||||||
@ -17,7 +16,7 @@ export interface NavigationItem {
|
|||||||
const NavigationItems: NavigationItem[] = [
|
const NavigationItems: NavigationItem[] = [
|
||||||
{
|
{
|
||||||
title: 'New Chat',
|
title: 'New Chat',
|
||||||
link: '/dashboard/new-chat', // Перенаправляем сразу на новый чат
|
link: '/dashboard/new-chat',
|
||||||
icon: <MdAddCircleOutline size={30} />
|
icon: <MdAddCircleOutline size={30} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -50,11 +49,8 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => {
|
|||||||
}
|
}
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
const handleThemeSwitch = () => {
|
|
||||||
setTheme(theme === "dark" ? "light" : "dark");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Загружаем данные пользователя из localStorage (если имеются)
|
|
||||||
const [user, setUser] = useState<any>(null);
|
const [user, setUser] = useState<any>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedUser = localStorage.getItem('user');
|
const storedUser = localStorage.getItem('user');
|
||||||
@ -106,7 +102,6 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Блок с иконкой пользователя и переключателем темы */}
|
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<Link to={user ? '/profile' : '/login'} className="flex items-center">
|
<Link to={user ? '/profile' : '/login'} className="flex items-center">
|
||||||
<IconButton
|
<IconButton
|
||||||
@ -124,24 +119,24 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => {
|
|||||||
)}
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Link>
|
</Link>
|
||||||
<button onClick={handleThemeSwitch} className='flex items-center gap-2'>
|
{/*<button onClick={handleThemeSwitch} className='flex items-center gap-2'>*/}
|
||||||
<IconButton
|
{/* <IconButton*/}
|
||||||
sx={{
|
{/* sx={{*/}
|
||||||
width: 40,
|
{/* width: 40,*/}
|
||||||
height: 40,
|
{/* height: 40,*/}
|
||||||
borderRadius: 2,
|
{/* borderRadius: 2,*/}
|
||||||
background: theme === 'dark' ? 'white' : 'initial',
|
{/* background: theme === 'dark' ? 'white' : 'initial',*/}
|
||||||
'&:focus-visible': {
|
{/* '&:focus-visible': {*/}
|
||||||
outline: '2px solid blue',
|
{/* outline: '2px solid blue',*/}
|
||||||
outlineOffset: '0px',
|
{/* outlineOffset: '0px',*/}
|
||||||
borderRadius: '4px',
|
{/* borderRadius: '4px',*/}
|
||||||
},
|
{/* },*/}
|
||||||
}}
|
{/* }}*/}
|
||||||
>
|
{/* >*/}
|
||||||
{theme === 'light' ? <CiLight size={30} /> : <MdOutlineDarkMode size={30} />}
|
{/* {theme === 'light' ? <CiLight size={30} /> : <MdOutlineDarkMode size={30} />}*/}
|
||||||
</IconButton>
|
{/* </IconButton>*/}
|
||||||
{isExpanded && (theme === 'light' ? 'Light mode' : 'Dark mode')}
|
{/* {isExpanded && (theme === 'light' ? 'Light mode' : 'Dark mode')}*/}
|
||||||
</button>
|
{/*</button>*/}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -72,7 +72,7 @@ const NewChatPage: React.FC = () => {
|
|||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
setChatHistory(prev => [
|
setChatHistory(prev => [
|
||||||
...prev,
|
...prev,
|
||||||
{ sender: 'Assistant', text: 'Что-то пошло не так' }
|
{ sender: 'Assistant', text: 'Something went wrong' }
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
233
frontend/src/Components/Profile.tsx
Normal file
233
frontend/src/Components/Profile.tsx
Normal file
@ -0,0 +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;
|
@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { GoogleLogin, GoogleOAuthProvider } from '@react-oauth/google';
|
import { GoogleLogin, GoogleOAuthProvider } from '@react-oauth/google';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import gsap from 'gsap';
|
|
||||||
|
|
||||||
const CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com";
|
const CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com";
|
||||||
|
|
||||||
@ -11,7 +10,6 @@ const RegistrationFormContent: React.FC = () => {
|
|||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Приведение типов для получения значений инпутов
|
|
||||||
const nameElement = document.getElementById('name') as HTMLInputElement | null;
|
const nameElement = document.getElementById('name') as HTMLInputElement | null;
|
||||||
const emailElement = document.getElementById('email') as HTMLInputElement | null;
|
const emailElement = document.getElementById('email') as HTMLInputElement | null;
|
||||||
const passwordElement = document.getElementById('password') as HTMLInputElement | null;
|
const passwordElement = document.getElementById('password') as HTMLInputElement | null;
|
||||||
@ -27,7 +25,6 @@ const RegistrationFormContent: React.FC = () => {
|
|||||||
const password = passwordElement.value;
|
const password = passwordElement.value;
|
||||||
const confirmPassword = confirmPasswordElement.value;
|
const confirmPassword = confirmPasswordElement.value;
|
||||||
|
|
||||||
// Проверка совпадения паролей
|
|
||||||
if (password !== confirmPassword) {
|
if (password !== confirmPassword) {
|
||||||
console.error('Passwords do not match');
|
console.error('Passwords do not match');
|
||||||
alert('Passwords do not match');
|
alert('Passwords do not match');
|
||||||
@ -45,7 +42,6 @@ const RegistrationFormContent: React.FC = () => {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
console.log('User registered successfully:', data.message);
|
console.log('User registered successfully:', data.message);
|
||||||
// Создаем объект пользователя для авторизации (placeholder для аватара)
|
|
||||||
const loggedInUser = {
|
const loggedInUser = {
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
@ -55,7 +51,7 @@ const RegistrationFormContent: React.FC = () => {
|
|||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
} else {
|
} else {
|
||||||
console.error('Error:', data.error);
|
console.error('Error:', data.error);
|
||||||
alert(data.error); // Показываем сообщение об ошибке, например "User already exists"
|
alert(data.error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error registering user:', error);
|
console.error('Error registering user:', error);
|
||||||
@ -79,14 +75,15 @@ const RegistrationFormContent: React.FC = () => {
|
|||||||
localStorage.setItem('user', JSON.stringify(loggedInUser));
|
localStorage.setItem('user', JSON.stringify(loggedInUser));
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка верификации токена:', error);
|
console.error('Error tiken verification:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGoogleLoginError = (error: any) => {
|
const handleGoogleLoginError = () => {
|
||||||
console.error('Ошибка аутентификации:', error);
|
console.error('Error auth: Google login failed');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@ -1,201 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { Form, Field, FieldRenderProps } from "react-final-form";
|
|
||||||
import Slider from "@mui/material/Slider";
|
|
||||||
|
|
||||||
interface FormValues {
|
|
||||||
age?: number;
|
|
||||||
height?: number;
|
|
||||||
weight?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserMetricsForm: React.FC = () => {
|
|
||||||
const [stage, setStage] = useState<number>(1);
|
|
||||||
|
|
||||||
const next = () => setStage((prev) => prev + 1);
|
|
||||||
const previous = () => setStage((prev) => prev - 1);
|
|
||||||
|
|
||||||
const onSubmit = (values: FormValues) => {
|
|
||||||
console.log("Form submitted:", values);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to select emoji based on value
|
|
||||||
const selectEmoji = (value: number | undefined, thresholds: number[], emojis: (string | JSX.Element)[]) => {
|
|
||||||
if (value === undefined) return null;
|
|
||||||
if (value <= thresholds[0]) return emojis[0];
|
|
||||||
if (value <= thresholds[1]) return emojis[1];
|
|
||||||
return emojis[2];
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center min-h-screen bg-gray-200">
|
|
||||||
<div className="w-full max-w-md bg-white rounded-lg shadow-lg p-8 text-center">
|
|
||||||
<h1 className="text-2xl font-bold text-gray-700 mb-6">
|
|
||||||
User Metrics Form
|
|
||||||
</h1>
|
|
||||||
<Form<FormValues>
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
render={({ handleSubmit, values }) => (
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
{/* Stage 1: Age */}
|
|
||||||
{stage === 1 && (
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-semibold text-gray-600 mb-4">
|
|
||||||
Stage 1: Age
|
|
||||||
</h2>
|
|
||||||
<div className="text-3xl mb-4">
|
|
||||||
{selectEmoji(values.age, [17, 50], ["👶", "🧑", "👴"])}
|
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<label htmlFor="age" className="block text-gray-500 mb-2">
|
|
||||||
Age
|
|
||||||
</label>
|
|
||||||
<Field<number>
|
|
||||||
name="age"
|
|
||||||
parse={(value) => (value === undefined ? 0 : Number(value))} // Changed to return 0 instead of undefined
|
|
||||||
>
|
|
||||||
{({ input, meta }) => (
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
{...input}
|
|
||||||
id="age"
|
|
||||||
type="number"
|
|
||||||
placeholder="Enter age"
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-indigo-500"
|
|
||||||
/>
|
|
||||||
{meta.touched && meta.error && (
|
|
||||||
<span className="text-red-500 text-sm">{meta.error}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={next}
|
|
||||||
className="px-4 py-2 bg-indigo-500 text-white rounded-md hover:bg-indigo-600"
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Stage 2: Height */}
|
|
||||||
{stage === 2 && (
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-semibold text-gray-600 mb-4">
|
|
||||||
Stage 2: Height
|
|
||||||
</h2>
|
|
||||||
<div className="text-3xl mb-4">
|
|
||||||
{selectEmoji(values.height, [150, 175], ["🌼", "🧍🏻", "🦒"])}
|
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<label htmlFor="height" className="block text-gray-500 mb-2">
|
|
||||||
Height (cm)
|
|
||||||
</label>
|
|
||||||
<Field<number>
|
|
||||||
name="height"
|
|
||||||
parse={(value) => (value === undefined ? 0 : Number(value))} // Changed to return 0 instead of undefined
|
|
||||||
>
|
|
||||||
{({ input, meta }) => (
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
{...input}
|
|
||||||
id="height"
|
|
||||||
type="number"
|
|
||||||
placeholder="Enter height"
|
|
||||||
min={0}
|
|
||||||
max={250}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-indigo-500"
|
|
||||||
/>
|
|
||||||
{meta.touched && meta.error && (
|
|
||||||
<span className="text-red-500 text-sm">{meta.error}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={previous}
|
|
||||||
className="px-4 py-2 bg-gray-400 text-white rounded-md hover:bg-gray-500"
|
|
||||||
>
|
|
||||||
Previous
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={next}
|
|
||||||
className="px-4 py-2 bg-indigo-500 text-white rounded-md hover:bg-indigo-600"
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Stage 3: Weight */}
|
|
||||||
{stage === 3 && (
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-semibold text-gray-600 mb-4">
|
|
||||||
Stage 3: Weight
|
|
||||||
</h2>
|
|
||||||
<div className="text-3xl mb-4">
|
|
||||||
{selectEmoji(values.weight, [70, 99], ["🐭", "🐱", "🐘"])}
|
|
||||||
</div>
|
|
||||||
<div className="mb-6">
|
|
||||||
<label htmlFor="weight" className="block text-gray-500 mb-2">
|
|
||||||
Weight (kg)
|
|
||||||
</label>
|
|
||||||
<Field<number> name="weight">
|
|
||||||
{({ input, meta }: FieldRenderProps<number, HTMLElement>) => (
|
|
||||||
<div>
|
|
||||||
<Slider
|
|
||||||
value={input.value || 0}
|
|
||||||
onChange={(_, value) => {
|
|
||||||
const newValue = Array.isArray(value) ? value[0] : value;
|
|
||||||
input.onChange(newValue);
|
|
||||||
}}
|
|
||||||
min={0}
|
|
||||||
max={200}
|
|
||||||
className="text-indigo-500"
|
|
||||||
/>
|
|
||||||
<div className="text-gray-600 mt-2">
|
|
||||||
Weight: {input.value || 0} kg
|
|
||||||
</div>
|
|
||||||
{meta.touched && meta.error && (
|
|
||||||
<span className="text-red-500 text-sm">{meta.error}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={previous}
|
|
||||||
className="px-4 py-2 bg-gray-400 text-white rounded-md hover:bg-gray-500"
|
|
||||||
>
|
|
||||||
Previous
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="px-4 py-2 bg-indigo-500 text-white rounded-md hover:bg-indigo-600"
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UserMetricsForm;
|
|
@ -30,7 +30,7 @@ const HomePage: React.FC = () => {
|
|||||||
if (!isNewChat && selectedChat && selectedChat.chat) {
|
if (!isNewChat && selectedChat && selectedChat.chat) {
|
||||||
const messages: ChatMessage[] = selectedChat.chat
|
const messages: ChatMessage[] = selectedChat.chat
|
||||||
.split(/(?=^(User:|Bot:))/m)
|
.split(/(?=^(User:|Bot:))/m)
|
||||||
.map((msg) => {
|
.map((msg:any) => {
|
||||||
const trimmed = msg.trim();
|
const trimmed = msg.trim();
|
||||||
const sender = trimmed.startsWith('User:') ? 'User' : 'Assistant';
|
const sender = trimmed.startsWith('User:') ? 'User' : 'Assistant';
|
||||||
return {
|
return {
|
||||||
@ -44,10 +44,7 @@ const HomePage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [isNewChat, selectedChat]);
|
}, [isNewChat, selectedChat]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Функция форматирования сообщения.
|
|
||||||
* Если в ответе отсутствуют символы перевода строки, пытаемся разбить текст по нумерованным пунктам.
|
|
||||||
*/
|
|
||||||
const formatMessage = (text: string) => {
|
const formatMessage = (text: string) => {
|
||||||
let lines: string[] = [];
|
let lines: string[] = [];
|
||||||
|
|
||||||
|
@ -1,17 +1,14 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { CgLogIn } from "react-icons/cg";
|
|
||||||
import BackImage from '../assets/smallheadicon.png';
|
import BackImage from '../assets/smallheadicon.png';
|
||||||
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
||||||
import { Box, Button, Avatar, Modal, Typography } from '@mui/material';
|
import { Box, Avatar } from '@mui/material';
|
||||||
import gsap from 'gsap';
|
import gsap from 'gsap';
|
||||||
import { useGSAP } from '@gsap/react';
|
import { useGSAP } from '@gsap/react';
|
||||||
import { GoogleOAuthProvider, GoogleLogin } from '@react-oauth/google';
|
import LogoutIcon from '@mui/icons-material/Logout';
|
||||||
|
import LoginIcon from '@mui/icons-material/Login';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import RegistrationForm from "../Components/RegistrationForm";
|
|
||||||
|
|
||||||
const CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com";
|
|
||||||
|
|
||||||
// Компонент для анимации стрелки вниз
|
|
||||||
const BouncingArrow = () => {
|
const BouncingArrow = () => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -57,7 +54,7 @@ const Navbar: React.FC<NavbarProps> = ({ user, setUser }) => {
|
|||||||
</div>
|
</div>
|
||||||
<ul className="flex space-x-6 text-gray-600">
|
<ul className="flex space-x-6 text-gray-600">
|
||||||
<li>
|
<li>
|
||||||
<Link to="/dashboard" className="hover:text-bright-blue transition duration-300">
|
<Link to="/" className="hover:text-bright-blue transition duration-300">
|
||||||
Home
|
Home
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
@ -75,19 +72,17 @@ const Navbar: React.FC<NavbarProps> = ({ user, setUser }) => {
|
|||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{user ? (
|
{user ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Avatar alt={user.name} src={user.picture} />
|
<Avatar alt={user.name} src={user.picture} onClick={() => navigate('/profile')} />
|
||||||
<Button variant="outlined" size="small" onClick={handleSignOut}>
|
<LogoutIcon
|
||||||
Sign Out
|
onClick={handleSignOut}
|
||||||
</Button>
|
sx={{ cursor: 'pointer', color: '#0d47a1', fontSize: '30px' }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<LoginIcon
|
||||||
startIcon={<CgLogIn />}
|
|
||||||
variant="outlined"
|
|
||||||
onClick={() => navigate('/register')}
|
onClick={() => navigate('/register')}
|
||||||
>
|
sx={{ cursor: 'pointer', color: '#0d47a1', fontSize: '30px' }}
|
||||||
Sign in
|
/>
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@ -98,7 +93,6 @@ const Home: React.FC = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [user, setUser] = useState<any>(null);
|
const [user, setUser] = useState<any>(null);
|
||||||
|
|
||||||
// При загрузке страницы пытаемся загрузить данные пользователя из localStorage
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedUser = localStorage.getItem('user');
|
const storedUser = localStorage.getItem('user');
|
||||||
if (storedUser) {
|
if (storedUser) {
|
||||||
@ -106,7 +100,6 @@ const Home: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Анимация GSAP для элементов страницы
|
|
||||||
useGSAP(() => {
|
useGSAP(() => {
|
||||||
gsap.from('#mainheading', { opacity: 0.3, ease: 'power2.inOut', duration: 0.5 });
|
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('#secondheading', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.3, duration: 0.5 });
|
||||||
@ -116,23 +109,20 @@ const Home: React.FC = () => {
|
|||||||
gsap.to('#button', { opacity: 1, ease: 'power2.inOut', delay: 2.5, duration: 0.5 });
|
gsap.to('#button', { opacity: 1, ease: 'power2.inOut', delay: 2.5, duration: 0.5 });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Обработчик нажатия на кнопку "Get started"
|
|
||||||
const handleGetStartedClick = () => {
|
const handleGetStartedClick = () => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// Если пользователь не авторизован — переходим на страницу регистрации
|
|
||||||
navigate('/register');
|
navigate('/register');
|
||||||
} else {
|
} else {
|
||||||
// Если авторизован — переходим на страницу dashboard
|
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ backgroundColor: '#d0e7ff' }} className="min-h-screen">
|
<div style={{ backgroundColor: '#d0e7ff' }} className="min-h-screen flex flex-col">
|
||||||
<div className="h-screen flex flex-col items-center justify-center bg-gradient-to-b text-gray-800 p-4">
|
<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} />
|
<Navbar user={user} setUser={setUser} />
|
||||||
|
|
||||||
<div className="pt-20 flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<h1
|
<h1
|
||||||
id="mainheading"
|
id="mainheading"
|
||||||
className="text-4xl flex items-center sm:text-5xl md:text-6xl font-semibold mb-4 text-center text-dark-blue"
|
className="text-4xl flex items-center sm:text-5xl md:text-6xl font-semibold mb-4 text-center text-dark-blue"
|
||||||
@ -186,11 +176,11 @@ const Home: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer className="mt-auto text-center text-gray-500 p-4">
|
<footer className="text-center text-gray-500 p-4">
|
||||||
<p>© {new Date().getFullYear()} Health AI. All rights reserved.</p>
|
<p>© {new Date().getFullYear()} Health AI. All rights reserved.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Home;
|
export { Home, Navbar };
|
||||||
|
4
prepare.sh
Normal file
4
prepare.sh
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "Подготовка окружения: сборка Docker образов..."
|
||||||
|
docker-compose build
|
||||||
|
echo "Подготовка завершена."
|
Loading…
Reference in New Issue
Block a user