upd project, decker func, upd frontend/backend/agent

This commit is contained in:
oleh 2025-03-31 16:09:26 +02:00
parent 677ae05159
commit 5b6d7728ce
32 changed files with 1260 additions and 1613 deletions

View File

@ -1,17 +1,17 @@
# Используем базовый образ Python
FROM python:3.12
# Устанавливаем рабочую директорию в контейнере
WORKDIR /app
# Копируем все файлы проекта в контейнер
COPY . .
# Устанавливаем зависимости из requirements.txt
RUN pip install -r requirements.txt
# Делаем скрипт ожидания исполняемым
RUN chmod +x wait-for-elasticsearch.sh
# Запускаем скрипт ожидания перед запуском бэкенда
CMD ["./wait-for-elasticsearch.sh", "python", "server.py"]

View File

@ -30,10 +30,10 @@ def index_documents(data):
'full_data': item
})
sys.stdout.write(f"\rПроиндексировано {i} из {total_documents} документов")
sys.stdout.flush()
print("\nИндексирование завершено.")
data_path = "../../data_adc_databaza/cleaned_general_info_additional.json"

View File

@ -8,7 +8,7 @@ es = Elasticsearch([{'host': 'localhost', 'port': 9200, 'scheme': 'http'}])
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
def create_index():
# Определяем маппинг для индекса
mapping = {
"mappings": {
"properties": {
@ -18,11 +18,11 @@ def create_index():
},
"vector": {
"type": "dense_vector",
"dims": 384 # Размерность векторного представления
"dims": 384
},
"full_data": {
"type": "object",
"enabled": False # Отключаем индексацию вложенных данных
"enabled": False
}
}
}
@ -53,19 +53,15 @@ def index_documents(data):
}
actions.append(action)
# Отображение прогресса
print(f"Индексируется документ {i}/{total_docs}", end='\r')
# Опционально: индексируем пакетами по N документов
if i % 100 == 0 or i == total_docs:
bulk(es, actions)
actions = []
# Если остались неиндексированные документы
if actions:
bulk(es, actions)
print("\nИндексирование завершено.")
if __name__ == "__main__":
create_index()

View File

@ -3,20 +3,18 @@ import requests
import logging
import time
import re
import difflib
from requests.exceptions import HTTPError
from elasticsearch import Elasticsearch
from langchain.chains import SequentialChain
from langchain.chains import LLMChain, SequentialChain
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_elasticsearch import ElasticsearchStore
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.docstore.document import Document
# from googletrans import Translator
import psycopg2
from psycopg2.extras import RealDictCursor
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
config_file_path = "config.json"
with open(config_file_path, 'r') as 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.")
###############################################################################
# translate all answer to slovak(temporary closed :) ) #
# Simple functions for translation (stub)
###############################################################################
# translator = Translator()
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
###############################################################################
# Функция перевода описания лекарства с сохранением названия (до двоеточия) #
###############################################################################
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
###############################################################################
# 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:
def __init__(self, api_key: str, endpoint_url: str, model_name: str):
@ -96,7 +115,7 @@ class CustomMistralLLM:
self.endpoint_url = endpoint_url
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 = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
@ -123,25 +142,24 @@ class CustomMistralLLM:
else:
logger.error(f"HTTP Error: {e}")
raise e
except Exception as e:
logger.error(f"Error: {str(e)}")
raise e
except Exception as ex:
logger.error(f"Error: {str(ex)}")
raise ex
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...")
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
index_name = 'drug_docs'
index_name = "drug_docs"
if config.get("useCloud", False):
logger.info("Using cloud Elasticsearch.")
cloud_id = "tt:dXMtZWFzdC0yLmF3cy5lbGFzdGljLWNsb3VkLmNvbTo0NDMkOGM3ODQ0ZWVhZTEyNGY3NmFjNjQyNDFhNjI4NmVhYzMkZTI3YjlkNTQ0ODdhNGViNmEyMTcxMjMxNmJhMWI0ZGU="
vectorstore = ElasticsearchStore(
es_cloud_id=cloud_id,
index_name='drug_docs',
index_name=index_name,
embedding=embeddings,
es_user="elastic",
es_password="sSz2BEGv56JRNjGFwoQ191RJ"
@ -149,22 +167,21 @@ if config.get("useCloud", False):
else:
logger.info("Using local Elasticsearch.")
vectorstore = ElasticsearchStore(
es_url="http://localhost:9200",
es_url="http://elasticsearch:9200",
index_name=index_name,
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(
api_key=mistral_api_key,
endpoint_url="https://api.mistral.ai/v1/chat/completions",
model_name="mistral-small-latest"
)
llm_large = CustomMistralLLM(
api_key=mistral_api_key,
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):
query_keywords = query.split()
total_score = 0
explanation = []
for i, summary in enumerate(summaries):
length_score = min(len(summary) / 100, 10)
total_score += length_score
explanation.append(f"Document {i+1}: Length score - {length_score}")
keyword_matches = sum(1 for word in query_keywords if word.lower() in summary.lower())
keyword_score = min(keyword_matches * 2, 10)
total_score += keyword_score
explanation.append(f"Document {i+1}: Keyword match score - {keyword_score}")
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."
def classify_query(query: str, chat_history: str = "") -> str:
if not chat_history.strip():
return "vyhladavanie"
prompt = (
"Ty si zdravotnícky expert, ktorý analyzuje otázky používateľov. "
"Analyzuj nasledujúci dopyt a urči, či ide o dopyt na vyhľadanie liekov alebo "
"o upresnenie/doplnenie už poskytnutej odpovede.\n"
"Ak dopyt obsahuje výrazy ako 'čo pit', 'aké lieky', 'odporuč liek', 'hľadám liek', "
"odpovedaj slovom 'vyhľadávanie'.\n"
"Ak dopyt slúži na upresnenie, napríklad obsahuje výrazy ako 'a nie na predpis', "
"'upresni', 'este raz', odpovedaj slovom 'upresnenie'.\n"
f"Dopyt: \"{query}\""
)
try:
validated_answer = llm_small.generate_text(prompt=validation_prompt, max_tokens=500, 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
classification = llm_small.generate_text(prompt=prompt, max_tokens=20, temperature=0.3)
classification = classification.strip().lower()
logger.info(f"Klasifikácia dopytu: {classification}")
if "vyhládzanie" in classification or "vyhľadávanie" in classification:
return "vyhladavanie"
elif "upresnenie" in classification:
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 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 ("" 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.")
try:
# --- Vector search ---
vector_results = vectorstore.similarity_search(query, k=k)
vector_documents = [hit.metadata.get('text', '') for hit in vector_results]
chat_history = ""
if chat_context:
chat_history = chat_context
elif chat_id:
try:
params = {"id": chat_id}
r = requests.get(CHAT_HISTORY_ENDPOINT, params=params)
if r.status_code == 200:
data = r.json()
chat_data = data.get("chat", "")
if isinstance(chat_data, dict):
chat_history = chat_data.get("chat", "")
else:
chat_history = chat_data or ""
logger.info(f"História chatu načítaná pre chatId: {chat_id}")
else:
logger.warning(f"Nepodarilo sa načítať históriu (status {r.status_code}) pre chatId: {chat_id}")
except Exception as 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 {
"best_answer": combined_missing_text,
"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_doc_length = 1000
vector_documents = [doc[:max_doc_length] for doc in vector_documents[:max_docs]]
if vector_documents:
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:
small_vector_eval = {"rating": 0, "explanation": "No results"}
large_vector_eval = {"rating": 0, "explanation": "No results"}
summary_small_vector = ""
summary_large_vector = ""
# --- Text search ---
es_results = vectorstore.client.search(
index=index_name,
body={"size": k, "query": {"match": {"text": query}}}
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é."
)
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:
small_text_eval = {"rating": 0, "explanation": "No results"}
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"},
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_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)
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": polished_answer,
"model": best_result["model"],
"rating": best_result["eval"]["rating"],
"explanation": best_result["eval"]["explanation"]
}
except Exception as e:
logger.error(f"Error: {str(e)}")
return {
"best_answer": "An error occurred during query processing.",
"error": str(e)
"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"
}

View File

@ -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}")

View File

@ -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()

View File

@ -189,3 +189,8 @@ wrapt==1.16.0
xformers==0.0.28.post1
yarl==1.9.4
zipp==3.20.2
flask
flask-cors
psycopg2-binary
google-auth

View File

@ -1,50 +1,76 @@
import time
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_cors import CORS
from google.oauth2 import id_token
from google.auth.transport import requests
from google.auth.transport import requests as google_requests
import logging
# Импортируем функцию обработки из model.py
from model import process_query_with_mistral
import psycopg2
from psycopg2.extras import RealDictCursor
from model import process_query_with_mistral
# Параметры подключения к базе данных
_real_time = time.time
time.time = lambda: _real_time() - 1
# Database connection parameters
DATABASE_CONFIG = {
"dbname": "postgres",
"dbname": "HealthAIDB",
"user": "postgres",
"password": "healthai!",
"host": "health-ai-user-db.cxeum6cmct3r.eu-west-1.rds.amazonaws.com",
"password": "Oleg2005",
"host": "postgres",
"port": 5432,
}
# Подключение к базе данных
try:
conn = psycopg2.connect(**DATABASE_CONFIG)
print("Подключение к базе данных успешно установлено")
except Exception as e:
print(f"Ошибка подключения к базе данных: {e}")
conn = None
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Создаем Flask приложение
app = Flask(__name__)
CORS(app, resources={r"/api/*": {"origins": "http://localhost:5173"}})
try:
conn = psycopg2.connect(**DATABASE_CONFIG)
logger.info("Database connection established successfully")
except Exception as e:
logger.error(f"Error connecting to database: {e}", exc_info=True)
conn = None
def init_db():
create_users_query = """
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
google_id TEXT,
password TEXT
);
"""
create_chat_history_query = """
CREATE TABLE IF NOT EXISTS chat_history (
id SERIAL PRIMARY KEY,
user_email TEXT NOT NULL,
chat TEXT NOT NULL,
user_data TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""
try:
with conn.cursor() as cur:
cur.execute(create_users_query)
cur.execute(create_chat_history_query)
conn.commit()
logger.info("Database tables initialized successfully")
except Exception as e:
logger.error(f"Error initializing database tables: {e}", exc_info=True)
conn.rollback()
if conn:
init_db()
app = Flask(__name__)
CORS(app, resources={r"/api/*": {"origins": "*"}})
# Ваш Google Client ID
CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com"
def save_user_to_db(name, email, google_id=None, password=None):
logger.info(f"Saving user {name} with email: {email} to the database")
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(
@ -56,94 +82,115 @@ def save_user_to_db(name, email, google_id=None, password=None):
(name, email, google_id, password)
)
conn.commit()
print(f"User {name} ({email}) saved successfully!")
logger.info(f"User {name} ({email}) saved successfully")
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'])
def verify_token():
logger.info("Received token verification request")
data = request.get_json()
token = data.get('token')
if not token:
logger.warning("Token not provided in request")
return jsonify({'error': 'No token provided'}), 400
try:
id_info = id_token.verify_oauth2_token(token, requests.Request(), CLIENT_ID)
id_info = id_token.verify_oauth2_token(token, google_requests.Request(), CLIENT_ID)
user_email = id_info.get('email')
user_name = id_info.get('name')
google_id = id_info.get('sub') # Уникальный идентификатор пользователя 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)
logger.info(f"User authenticated and saved: {user_name} ({user_email})")
return jsonify({'message': 'Authentication successful', 'user': {'email': user_email, 'name': user_name}}), 200
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
# Эндпоинт для регистрации пользователя с проверкой на дублирование
@app.route('/api/register', methods=['POST'])
def register():
logger.info("Received new user registration request")
data = request.get_json()
name = data.get('name')
email = data.get('email')
password = data.get('password') # Рекомендуется хэшировать пароль
password = data.get('password')
if not all([name, email, password]):
logger.warning("Not all required fields provided for registration")
return jsonify({'error': 'All fields are required'}), 400
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
existing_user = cur.fetchone()
if existing_user:
logger.warning(f"User with email {email} already exists")
return jsonify({'error': 'User already exists'}), 409
save_user_to_db(name=name, email=email, password=password)
logger.info(f"User {name} ({email}) registered successfully")
return jsonify({'message': 'User registered successfully'}), 201
except Exception as e:
logger.error(f"Error during user registration: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
# Эндпоинт для логина пользователя
@app.route('/api/login', methods=['POST'])
def login():
logger.info("Received login request")
data = request.get_json()
email = data.get('email')
password = data.get('password')
if not all([email, password]):
logger.warning("Email or password not provided")
return jsonify({'error': 'Email and password are required'}), 400
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
user = cur.fetchone()
if not user:
return jsonify({'error': 'Invalid credentials'}), 401
if user.get('password') != password:
if not user or user.get('password') != password:
logger.warning(f"Invalid credentials for email: {email}")
return jsonify({'error': 'Invalid credentials'}), 401
logger.info(f"User {user.get('name')} ({email}) logged in successfully")
return jsonify({'message': 'Login successful', 'user': {'name': user.get('name'), 'email': user.get('email')}}), 200
except Exception as e:
logger.error(f"Error during user login: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
# Объединённый эндпоинт для обработки запроса чата
@app.route('/api/chat', methods=['POST'])
def chat():
logger.info("Received chat request")
data = request.get_json()
query = data.get('query', '')
user_email = data.get('email') # email пользователя (если передается)
chat_id = data.get('chatId') # параметр для обновления существующего чата
user_email = data.get('email')
chat_id = data.get('chatId')
if not query:
logger.warning("No query provided")
return jsonify({'error': 'No query provided'}), 400
# Вызов функции для обработки запроса (например, чат-бота)
response_obj = process_query_with_mistral(query)
best_answer = ""
if isinstance(response_obj, dict):
best_answer = response_obj.get("best_answer", "")
else:
best_answer = str(response_obj)
logger.info(f"Processing request for chatId: {chat_id if chat_id else 'new chat'} | Query: {query}")
# Retrieve chat context from the database
chat_context = ""
if chat_id:
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("SELECT chat, user_data FROM chat_history WHERE id = %s", (chat_id,))
result = cur.fetchone()
if result:
chat_context = result.get("chat", "")
logger.info(f"Loaded chat context for chatId {chat_id}: {chat_context}")
else:
logger.info(f"No chat context found for chatId {chat_id}")
except Exception as e:
logger.error(f"Error loading chat context from DB: {e}", exc_info=True)
logger.info("Calling process_query_with_mistral function")
response_obj = process_query_with_mistral(query, chat_id=chat_id, chat_context=chat_context)
best_answer = response_obj.get("best_answer", "") if isinstance(response_obj, dict) else str(response_obj)
logger.info(f"Response from process_query_with_mistral: {best_answer}")
# Форматирование ответа с использованием re.sub
best_answer = re.sub(r'[*#]', '', best_answer)
best_answer = re.sub(r'(\d\.\s)', r'\n\n\1', best_answer)
best_answer = re.sub(r':\s-', r':\n-', best_answer)
# Если chatId передан, обновляем существующий чат, иначе создаем новый чат
# Update or create chat_history record including user_data if available
if chat_id:
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
@ -151,8 +198,13 @@ def chat():
existing_chat = cur.fetchone()
if existing_chat:
updated_chat = existing_chat['chat'] + f"\nUser: {query}\nBot: {best_answer}"
cur.execute("UPDATE chat_history SET chat = %s WHERE id = %s", (updated_chat, chat_id))
if "patient_data" in response_obj:
cur.execute("UPDATE chat_history SET chat = %s, user_data = %s WHERE id = %s",
(updated_chat, response_obj["patient_data"], chat_id))
else:
cur.execute("UPDATE chat_history SET chat = %s WHERE id = %s", (updated_chat, chat_id))
conn.commit()
logger.info(f"Chat history for chatId {chat_id} updated successfully")
else:
with conn.cursor(cursor_factory=RealDictCursor) as cur2:
cur2.execute(
@ -162,7 +214,9 @@ def chat():
new_chat_id = cur2.fetchone()['id']
conn.commit()
chat_id = new_chat_id
logger.info(f"New chat created with chatId: {chat_id}")
except Exception as e:
logger.error(f"Error updating/creating chat history: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
else:
try:
@ -174,45 +228,98 @@ def chat():
new_chat_id = cur.fetchone()['id']
conn.commit()
chat_id = new_chat_id
logger.info(f"New chat created with chatId: {chat_id}")
except Exception as e:
logger.error(f"Error creating new chat: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
# Возвращаем текстовый ответ и новый chatId, если чат был создан
return jsonify({'response': {'best_answer': best_answer, 'model': 'Mistral Small Vector', 'chatId': chat_id}}), 200
return jsonify({'response': {'best_answer': best_answer, 'model': response_obj.get("model", ""), 'chatId': chat_id}}), 200
@app.route('/api/save_user_data', methods=['POST'])
def save_user_data():
logger.info("Received request to save user data")
data = request.get_json()
chat_id = data.get('chatId')
user_data = data.get('userData')
if not chat_id or not user_data:
return jsonify({'error': 'chatId and userData are required'}), 400
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("UPDATE chat_history SET user_data = %s WHERE id = %s", (user_data, chat_id))
conn.commit()
logger.info(f"User data for chatId {chat_id} updated successfully")
return jsonify({'message': 'User data updated successfully'}), 200
except Exception as e:
logger.error(f"Error updating user data: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
# Эндпоинт для получения истории чатов конкретного пользователя
@app.route('/api/chat_history', methods=['GET'])
def get_chat_history():
logger.info("Received request to get chat history")
user_email = request.args.get('email')
if not user_email:
return jsonify({'error': 'User email is required'}), 400
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(
"SELECT id, chat, 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,)
)
history = cur.fetchall()
return jsonify({'history': history}), 200
except Exception as e:
logger.error(f"Error getting chat history for {user_email}: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
@app.route('/api/chat_history', methods=['DELETE'])
def delete_chat():
logger.info("Received request to delete chat")
chat_id = request.args.get('id')
if not chat_id:
return jsonify({'error': 'Chat id is required'}), 400
try:
with conn.cursor() as cur:
cur.execute("DELETE FROM chat_history WHERE id = %s", (chat_id,))
conn.commit()
return jsonify({'message': 'Chat deleted successfully'}), 200
except Exception as e:
logger.error(f"Error deleting chat with chatId {chat_id}: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
@app.route('/api/get_user_data', methods=['GET'])
def get_user_data():
chat_id = request.args.get('chatId')
if not chat_id:
return jsonify({'error': 'Chat id is required'}), 400
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("SELECT user_data FROM chat_history WHERE id = %s", (chat_id,))
result = cur.fetchone()
if result and result.get("user_data"):
return jsonify({'user_data': result.get("user_data")}), 200
else:
return jsonify({'user_data': None}), 200
except Exception as e:
logger.error(f"Error retrieving user data: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
# Эндпоинт для получения деталей чата по ID
@app.route('/api/chat_history_detail', methods=['GET'])
def chat_history_detail():
logger.info("Received request to get chat details")
chat_id = request.args.get('id')
if not chat_id:
return jsonify({'error': 'Chat id is required'}), 400
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("SELECT id, chat, 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()
if not chat:
return jsonify({'error': 'Chat not found'}), 404
return jsonify({'chat': chat}), 200
except Exception as e:
logger.error(f"Error getting chat details for chatId {chat_id}: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
logger.info("Starting Flask application")
app.run(host='0.0.0.0', port=5000, debug=True)

View File

@ -1,19 +1,18 @@
from sentence_transformers import SentenceTransformer, util
# Загрузка модели из Hugging Face
model = SentenceTransformer("TUKE-DeutscheTelekom/slovakbert-skquad-mnlr") # Замените на ID нужной модели
# Пример предложений на словацком языке
model = SentenceTransformer("TUKE-DeutscheTelekom/slovakbert-skquad-mnlr")
sentences = [
"Prvý most cez Zlatý roh nechal vybudovať cisár Justinián I. V roku 1502 vypísal sultán Bajezid II. súťaž na nový most.",
"V ktorom roku vznikol druhý drevený most cez záliv Zlatý roh?",
"Aká je priemerná dĺžka života v Eritrei?"
]
# Получение эмбеддингов для каждого предложения
embeddings = model.encode(sentences)
print("Shape of embeddings:", embeddings.shape) # Вывод формы эмбеддингов, например (3, 768)
print("Shape of embeddings:", embeddings.shape)
# Вычисление сходства между предложениями
similarities = util.cos_sim(embeddings, embeddings)
print("Similarity matrix:\n", similarities)

View File

@ -1,13 +1,11 @@
#!/bin/sh
echo "Ожидание готовности Elasticsearch..."
echo "Waiting for Elasticsearch..."
# Проверяем доступность Elasticsearch
while ! curl -s http://elasticsearch:9200 > /dev/null; do
sleep 5
done
echo "Elasticsearch готов. Запуск бэкенда..."
echo "Elasticsearch is ready. Starting backend..."
# Запускаем бэкенд
exec "$@"

View File

@ -21,6 +21,20 @@ services:
retries: 5
start_period: 40s
postgres:
image: postgres:14
container_name: postgres_db
environment:
- POSTGRES_DB=HealthAIDB
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=Oleg2005
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- app-network
backend:
container_name: backend_container
build:
@ -30,9 +44,12 @@ services:
- "5000:5000"
environment:
- ELASTICSEARCH_HOST=http://elasticsearch:9200
- DATABASE_HOST=postgres
depends_on:
elasticsearch:
condition: service_healthy
postgres:
condition: service_started
networks:
- app-network
@ -42,7 +59,7 @@ services:
context: ./frontend
dockerfile: Dockerfile
ports:
- "3000:3000"
- "5173:5173"
depends_on:
- backend
networks:
@ -51,3 +68,6 @@ services:
networks:
app-network:
driver: bridge
volumes:
pgdata:

View File

@ -1,15 +1,15 @@
FROM docker.elastic.co/elasticsearch/elasticsearch:8.14.3
# Отключаем безопасность для упрощения доступа
ENV discovery.type=single-node
ENV xpack.security.enabled=false
# Копируем проиндексированные данные в директорию данных Elasticsearch
COPY --chown=elasticsearch:elasticsearch data/ /usr/share/elasticsearch/data
# Устанавливаем права доступа
RUN chmod -R 0775 /usr/share/elasticsearch/data
# Удаляем файлы блокировок (добавьте эти команды)
RUN find /usr/share/elasticsearch/data -type f -name "*.lock" -delete
RUN rm -f /usr/share/elasticsearch/data/nodes/0/node.lock

View File

@ -1,26 +1,26 @@
# Используем базовый образ Node.js
FROM node:18-alpine
# Устанавливаем рабочую директорию
WORKDIR /app
# Копируем package.json и package-lock.json
COPY package*.json ./
# Устанавливаем зависимости
RUN npm install
# Копируем файлы проекта
COPY . .
# Сборка приложения
RUN npm run build
# Устанавливаем сервер для обслуживания статических файлов
RUN npm install -g serve
# Открываем порт
EXPOSE 3000
# Запуск фронтенда
CMD ["serve", "-s", "dist", "-l", "3000"]
EXPOSE 5173
CMD ["serve", "-s", "dist", "-l", "5173"]

View File

@ -1,11 +1,14 @@
import { BrowserRouter as Router, Route, Routes, Outlet } from 'react-router-dom';
import Navigation from './Components/Navigation';
import LandingPage from './pages/LandingPage';
import {Home} from './pages/LandingPage';
import RegistrationForm from "./Components/RegistrationForm";
import LoginForm from "./Components/LoginForm";
import ChatHistory from "./Components/ChatHistory";
import HomePage from './pages/HomePage';
import NewChatPage from "./Components/NewChatPage";
import About from "./Components/About.tsx";
import Contact from "./Components/Contact.tsx";
import Profile from "./Components/Profile.tsx";
const Layout = () => (
<div className="flex w-full h-screen dark:bg-slate-200">
@ -22,13 +25,14 @@ function App() {
return (
<Router>
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/" element={<Home />} />
<Route path="/register" element={<RegistrationForm />} />
<Route path="/login" element={<LoginForm />} />
<Route path="/contact" element={<Contact />} />
<Route path="/profile" element={<Profile />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Layout />}>
{/* Новый чат */}
<Route path="new-chat" element={<NewChatPage />} />
{/* Существующий чат (после создания нового, URL обновится) */}
<Route path="chat/:id" element={<HomePage />} />
<Route path="history" element={<ChatHistory />} />
<Route index element={<HomePage />} />

View 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;

View File

@ -15,7 +15,6 @@ const ChatDetails: React.FC = () => {
useEffect(() => {
if (!chat && id) {
// Если данные не переданы через state, можно попробовать получить их с сервера
fetch(`http://localhost:5000/api/chat_history_detail?id=${encodeURIComponent(id)}`)
.then((res) => {
if (!res.ok) {

View File

@ -1,5 +1,7 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Box, Typography, Paper, IconButton } from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
interface ChatHistoryItem {
id: number;
@ -32,48 +34,124 @@ const ChatHistory: React.FC = () => {
}
}, []);
// При клике перенаправляем пользователя на /dashboard/chat/{chatId}
const handleClick = (item: ChatHistoryItem) => {
navigate(`/dashboard/chat/${item.id}`, { state: { selectedChat: item } });
};
const handleDelete = (chatId: number) => {
if (window.confirm('Are you sure that you want to delete that chat?')) {
fetch(`http://localhost:5000/api/chat_history?id=${chatId}`, {
method: 'DELETE'
})
.then((res) => res.json())
.then((data) => {
if (data.error) {
setError(data.error);
} else {
setHistory(history.filter((chat) => chat.id !== chatId));
}
})
.catch(() => setError('Error deleting chat'));
}
};
return (
<div style={{ padding: '20px' }}>
<h1>Chat History</h1>
{error && <p style={{ color: 'red' }}>{error}</p>}
{history.length === 0 && !error ? (
<p>No chat history found.</p>
<Box
sx={{
width: '100%',
height: '100vh',
overflowY: 'auto',
background: '#f5f5f5',
boxSizing: 'border-box',
p: 3,
'&::-webkit-scrollbar': {
display: 'none',
},
'-ms-overflow-style': 'none',
'scrollbarWidth': 'none',
}}
>
<Typography
variant="h4"
sx={{
mb: 3,
fontWeight: 'bold',
textAlign: 'center',
color: '#0d47a1',
}}
>
Chat History
</Typography>
{error ? (
<Typography
variant="body1"
sx={{ color: 'error.main', textAlign: 'center' }}
>
{error}
</Typography>
) : (
<ul style={{ listStyleType: 'none', padding: 0 }}>
{history.map((item) => {
// Извлекаем первую строку из сохранённого чата.
// Предполагаем, что чат хранится в формате: "User: <вопрос>\nBot: <ответ>\n..."
const lines = item.chat.split("\n");
let firstUserMessage = lines[0];
if (firstUserMessage.startsWith("User:")) {
firstUserMessage = firstUserMessage.replace("User:", "").trim();
}
return (
<li
key={item.id}
style={{
marginBottom: '15px',
borderBottom: '1px solid #ccc',
paddingBottom: '10px',
cursor: 'pointer'
}}
onClick={() => handleClick(item)}
>
<div>
<strong>{firstUserMessage}</strong>
</div>
<small>{new Date(item.created_at).toLocaleString()}</small>
</li>
);
})}
</ul>
<Box sx={{ maxWidth: '800px', mx: 'auto' }}>
{history.length === 0 ? (
<Typography
variant="body1"
sx={{ textAlign: 'center', color: '#424242' }}
>
No chat history found.
</Typography>
) : (
history.map((item) => {
const lines = item.chat.split("\n");
let firstUserMessage = lines[0];
if (firstUserMessage.startsWith("User:")) {
firstUserMessage = firstUserMessage.replace("User:", "").trim();
}
return (
<Paper
key={item.id}
sx={{
p: 2,
mb: 2,
cursor: 'pointer',
transition: 'box-shadow 0.3s ease',
'&:hover': { boxShadow: 6 },
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<Box
sx={{ flex: 1, display: 'flex', flexDirection: 'column' }}
onClick={() => handleClick(item)}
>
<Typography
variant="subtitle1"
sx={{ fontWeight: 'bold', color: '#0d47a1' }}
>
{firstUserMessage}
</Typography>
<Typography
variant="caption"
sx={{ color: '#757575' }}
>
{new Date(item.created_at).toLocaleString()}
</Typography>
</Box>
<IconButton
onClick={(e) => {
e.stopPropagation();
handleDelete(item.id);
}}
sx={{ color: '#d32f2f' }}
>
<DeleteIcon />
</IconButton>
</Paper>
);
})
)}
</Box>
)}
</div>
</Box>
);
};

View 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;

View File

@ -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;

View File

@ -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;

View File

@ -10,12 +10,11 @@ const LoginFormContent: React.FC = () => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Приведение типов для получения значений инпутов
const emailElement = document.getElementById('email') as HTMLInputElement | null;
const passwordElement = document.getElementById('password') as HTMLInputElement | null;
if (!emailElement || !passwordElement) {
console.error('Один или несколько инпутов отсутствуют');
console.error('One or many inputs are missing');
return;
}
@ -41,10 +40,10 @@ const LoginFormContent: React.FC = () => {
localStorage.setItem('user', JSON.stringify(loggedInUser));
navigate('/dashboard');
} else {
console.error('Ошибка:', data.error);
console.error('Error:', data.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));
navigate('/dashboard');
} catch (error) {
console.error('Ошибка верификации токена:', error);
console.error('Error token verification:', error);
}
};
const handleGoogleLoginError = (error: any) => {
console.error('Ошибка аутентификации через Google:', error);
const handleGoogleLoginError = () => {
console.error('Error auth through Google');
};
return (
<div
style={{

View File

@ -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;

View File

@ -2,9 +2,8 @@ import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import IconButton from '@mui/material/IconButton';
import Avatar from '@mui/material/Avatar';
import { MdAddCircleOutline, MdOutlineDarkMode } from "react-icons/md";
import { MdAddCircleOutline } from "react-icons/md";
import { GoHistory } from "react-icons/go";
import { CiLight } from "react-icons/ci";
import { CgLogIn } from "react-icons/cg";
import BackImage from '../assets/smallheadicon.png';
@ -17,7 +16,7 @@ export interface NavigationItem {
const NavigationItems: NavigationItem[] = [
{
title: 'New Chat',
link: '/dashboard/new-chat', // Перенаправляем сразу на новый чат
link: '/dashboard/new-chat',
icon: <MdAddCircleOutline size={30} />
},
{
@ -50,11 +49,8 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => {
}
}, [theme]);
const handleThemeSwitch = () => {
setTheme(theme === "dark" ? "light" : "dark");
};
// Загружаем данные пользователя из localStorage (если имеются)
const [user, setUser] = useState<any>(null);
useEffect(() => {
const storedUser = localStorage.getItem('user');
@ -106,7 +102,6 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => {
))}
</div>
</div>
{/* Блок с иконкой пользователя и переключателем темы */}
<div className="flex flex-col items-center gap-2">
<Link to={user ? '/profile' : '/login'} className="flex items-center">
<IconButton
@ -124,24 +119,24 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => {
)}
</IconButton>
</Link>
<button onClick={handleThemeSwitch} className='flex items-center gap-2'>
<IconButton
sx={{
width: 40,
height: 40,
borderRadius: 2,
background: theme === 'dark' ? 'white' : 'initial',
'&:focus-visible': {
outline: '2px solid blue',
outlineOffset: '0px',
borderRadius: '4px',
},
}}
>
{theme === 'light' ? <CiLight size={30} /> : <MdOutlineDarkMode size={30} />}
</IconButton>
{isExpanded && (theme === 'light' ? 'Light mode' : 'Dark mode')}
</button>
{/*<button onClick={handleThemeSwitch} className='flex items-center gap-2'>*/}
{/* <IconButton*/}
{/* sx={{*/}
{/* width: 40,*/}
{/* height: 40,*/}
{/* borderRadius: 2,*/}
{/* background: theme === 'dark' ? 'white' : 'initial',*/}
{/* '&:focus-visible': {*/}
{/* outline: '2px solid blue',*/}
{/* outlineOffset: '0px',*/}
{/* borderRadius: '4px',*/}
{/* },*/}
{/* }}*/}
{/* >*/}
{/* {theme === 'light' ? <CiLight size={30} /> : <MdOutlineDarkMode size={30} />}*/}
{/* </IconButton>*/}
{/* {isExpanded && (theme === 'light' ? 'Light mode' : 'Dark mode')}*/}
{/*</button>*/}
</div>
</div>
</div>

View File

@ -72,7 +72,7 @@ const NewChatPage: React.FC = () => {
console.error('Error:', error);
setChatHistory(prev => [
...prev,
{ sender: 'Assistant', text: 'Что-то пошло не так' }
{ sender: 'Assistant', text: 'Something went wrong' }
]);
}
}

View 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;

View File

@ -1,7 +1,6 @@
import React from 'react';
import { GoogleLogin, GoogleOAuthProvider } from '@react-oauth/google';
import { Link, useNavigate } from 'react-router-dom';
import gsap from 'gsap';
const CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com";
@ -11,7 +10,6 @@ const RegistrationFormContent: React.FC = () => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Приведение типов для получения значений инпутов
const nameElement = document.getElementById('name') as HTMLInputElement | null;
const emailElement = document.getElementById('email') as HTMLInputElement | null;
const passwordElement = document.getElementById('password') as HTMLInputElement | null;
@ -27,7 +25,6 @@ const RegistrationFormContent: React.FC = () => {
const password = passwordElement.value;
const confirmPassword = confirmPasswordElement.value;
// Проверка совпадения паролей
if (password !== confirmPassword) {
console.error('Passwords do not match');
alert('Passwords do not match');
@ -45,7 +42,6 @@ const RegistrationFormContent: React.FC = () => {
if (response.ok) {
console.log('User registered successfully:', data.message);
// Создаем объект пользователя для авторизации (placeholder для аватара)
const loggedInUser = {
name,
email,
@ -55,7 +51,7 @@ const RegistrationFormContent: React.FC = () => {
navigate('/dashboard');
} else {
console.error('Error:', data.error);
alert(data.error); // Показываем сообщение об ошибке, например "User already exists"
alert(data.error);
}
} catch (error) {
console.error('Error registering user:', error);
@ -79,14 +75,15 @@ const RegistrationFormContent: React.FC = () => {
localStorage.setItem('user', JSON.stringify(loggedInUser));
navigate('/dashboard');
} catch (error) {
console.error('Ошибка верификации токена:', error);
console.error('Error tiken verification:', error);
}
};
const handleGoogleLoginError = (error: any) => {
console.error('Ошибка аутентификации:', error);
const handleGoogleLoginError = () => {
console.error('Error auth: Google login failed');
};
return (
<div
style={{

View File

@ -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;

View File

@ -30,7 +30,7 @@ const HomePage: React.FC = () => {
if (!isNewChat && selectedChat && selectedChat.chat) {
const messages: ChatMessage[] = selectedChat.chat
.split(/(?=^(User:|Bot:))/m)
.map((msg) => {
.map((msg:any) => {
const trimmed = msg.trim();
const sender = trimmed.startsWith('User:') ? 'User' : 'Assistant';
return {
@ -44,10 +44,7 @@ const HomePage: React.FC = () => {
}
}, [isNewChat, selectedChat]);
/**
* Функция форматирования сообщения.
* Если в ответе отсутствуют символы перевода строки, пытаемся разбить текст по нумерованным пунктам.
*/
const formatMessage = (text: string) => {
let lines: string[] = [];

View File

@ -1,17 +1,14 @@
import React, { useState, useEffect } from 'react';
import { CgLogIn } from "react-icons/cg";
import BackImage from '../assets/smallheadicon.png';
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 { 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 RegistrationForm from "../Components/RegistrationForm";
const CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com";
// Компонент для анимации стрелки вниз
const BouncingArrow = () => {
return (
<Box
@ -57,7 +54,7 @@ const Navbar: React.FC<NavbarProps> = ({ user, setUser }) => {
</div>
<ul className="flex space-x-6 text-gray-600">
<li>
<Link to="/dashboard" className="hover:text-bright-blue transition duration-300">
<Link to="/" className="hover:text-bright-blue transition duration-300">
Home
</Link>
</li>
@ -75,19 +72,17 @@ const Navbar: React.FC<NavbarProps> = ({ user, setUser }) => {
<div className="flex items-center">
{user ? (
<div className="flex items-center gap-2">
<Avatar alt={user.name} src={user.picture} />
<Button variant="outlined" size="small" onClick={handleSignOut}>
Sign Out
</Button>
<Avatar alt={user.name} src={user.picture} onClick={() => navigate('/profile')} />
<LogoutIcon
onClick={handleSignOut}
sx={{ cursor: 'pointer', color: '#0d47a1', fontSize: '30px' }}
/>
</div>
) : (
<Button
startIcon={<CgLogIn />}
variant="outlined"
<LoginIcon
onClick={() => navigate('/register')}
>
Sign in
</Button>
sx={{ cursor: 'pointer', color: '#0d47a1', fontSize: '30px' }}
/>
)}
</div>
</nav>
@ -98,7 +93,6 @@ const Home: React.FC = () => {
const navigate = useNavigate();
const [user, setUser] = useState<any>(null);
// При загрузке страницы пытаемся загрузить данные пользователя из localStorage
useEffect(() => {
const storedUser = localStorage.getItem('user');
if (storedUser) {
@ -106,7 +100,6 @@ const Home: React.FC = () => {
}
}, []);
// Анимация GSAP для элементов страницы
useGSAP(() => {
gsap.from('#mainheading', { opacity: 0.3, ease: 'power2.inOut', duration: 0.5 });
gsap.from('#secondheading', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.3, duration: 0.5 });
@ -116,23 +109,20 @@ const Home: React.FC = () => {
gsap.to('#button', { opacity: 1, ease: 'power2.inOut', delay: 2.5, duration: 0.5 });
}, []);
// Обработчик нажатия на кнопку "Get started"
const handleGetStartedClick = () => {
if (!user) {
// Если пользователь не авторизован — переходим на страницу регистрации
navigate('/register');
} else {
// Если авторизован — переходим на страницу dashboard
navigate('/dashboard');
}
};
return (
<div style={{ backgroundColor: '#d0e7ff' }} className="min-h-screen">
<div className="h-screen flex flex-col items-center justify-center bg-gradient-to-b text-gray-800 p-4">
<div style={{ backgroundColor: '#d0e7ff' }} className="min-h-screen flex flex-col">
<div className="flex-grow flex flex-col items-center justify-center bg-gradient-to-b text-gray-800 p-4 pt-20">
<Navbar user={user} setUser={setUser} />
<div className="pt-20 flex flex-col items-center">
<div className="flex flex-col items-center">
<h1
id="mainheading"
className="text-4xl flex items-center sm:text-5xl md:text-6xl font-semibold mb-4 text-center text-dark-blue"
@ -186,11 +176,11 @@ const Home: React.FC = () => {
</div>
</div>
<footer className="mt-auto text-center text-gray-500 p-4">
<footer className="text-center text-gray-500 p-4">
<p>&copy; {new Date().getFullYear()} Health AI. All rights reserved.</p>
</footer>
</div>
);
};
export default Home;
export { Home, Navbar };

4
prepare.sh Normal file
View File

@ -0,0 +1,4 @@
#!/bin/bash
echo "Подготовка окружения: сборка Docker образов..."
docker-compose build
echo "Подготовка завершена."