new frontend, evaluating functionality on BE, dockerFiles
This commit is contained in:
		
							parent
							
								
									8b2aad77aa
								
							
						
					
					
						commit
						4e0499ff05
					
				
							
								
								
									
										14
									
								
								Backend/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								Backend/Dockerfile
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
# Используем базовый образ Python
 | 
			
		||||
FROM python:3.12
 | 
			
		||||
 | 
			
		||||
# Устанавливаем рабочую директорию в контейнере
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
 | 
			
		||||
# Копируем все файлы проекта в контейнер
 | 
			
		||||
COPY . .
 | 
			
		||||
 | 
			
		||||
# Устанавливаем зависимости из requirements.txt
 | 
			
		||||
RUN pip install -r requirements.txt
 | 
			
		||||
 | 
			
		||||
# Запускаем сервер (замените server.py на точку входа вашего бэкенда)
 | 
			
		||||
CMD ["python", "server.py"]
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Backend/__pycache__/model.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Backend/__pycache__/model.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							@ -1,33 +1,74 @@
 | 
			
		||||
import json
 | 
			
		||||
from elasticsearch import Elasticsearch
 | 
			
		||||
from langchain_huggingface import HuggingFaceEmbeddings
 | 
			
		||||
from elasticsearch.helpers import bulk
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
es = Elasticsearch([{'host': 'localhost', 'port': 9200, 'scheme': 'http'}])
 | 
			
		||||
 | 
			
		||||
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
 | 
			
		||||
 | 
			
		||||
def create_index():
 | 
			
		||||
    # Определяем маппинг для индекса
 | 
			
		||||
    mapping = {
 | 
			
		||||
        "mappings": {
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "text": {
 | 
			
		||||
                    "type": "text",
 | 
			
		||||
                    "analyzer": "standard"
 | 
			
		||||
                },
 | 
			
		||||
                "vector": {
 | 
			
		||||
                    "type": "dense_vector",
 | 
			
		||||
                    "dims": 384  # Размерность векторного представления
 | 
			
		||||
                },
 | 
			
		||||
                "full_data": {
 | 
			
		||||
                    "type": "object",
 | 
			
		||||
                    "enabled": False  # Отключаем индексацию вложенных данных
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    es.indices.create(index='drug_docs', body=mapping, ignore=400)
 | 
			
		||||
 | 
			
		||||
def load_drug_data(json_path):
 | 
			
		||||
    with open(json_path, 'r', encoding='utf-8') as f:
 | 
			
		||||
        data = json.load(f)
 | 
			
		||||
    return data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def index_documents(data):
 | 
			
		||||
    for i, item in enumerate(data):
 | 
			
		||||
    actions = []
 | 
			
		||||
    total_docs = len(data)
 | 
			
		||||
    for i, item in enumerate(data, start=1):
 | 
			
		||||
        doc_text = f"{item['link']} {item.get('pribalovy_letak', '')} {item.get('spc', '')}"
 | 
			
		||||
 | 
			
		||||
        vector = embeddings.embed_query(doc_text)
 | 
			
		||||
 | 
			
		||||
        es.index(index='drug_docs', id=i, body={
 | 
			
		||||
            'text': doc_text,
 | 
			
		||||
            'vector': vector,
 | 
			
		||||
            'full_data': item
 | 
			
		||||
        })
 | 
			
		||||
        action = {
 | 
			
		||||
            "_index": "drug_docs",
 | 
			
		||||
            "_id": i,
 | 
			
		||||
            "_source": {
 | 
			
		||||
                'text': doc_text,
 | 
			
		||||
                'vector': vector,
 | 
			
		||||
                'full_data': item
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        actions.append(action)
 | 
			
		||||
 | 
			
		||||
        # Отображение прогресса
 | 
			
		||||
        print(f"Индексируется документ {i}/{total_docs}", end='\r')
 | 
			
		||||
 | 
			
		||||
data_path = "data/cleaned_general_info_additional.json"
 | 
			
		||||
drug_data = load_drug_data(data_path)
 | 
			
		||||
index_documents(drug_data)
 | 
			
		||||
        # Опционально: индексируем пакетами по N документов
 | 
			
		||||
        if i % 100 == 0 or i == total_docs:
 | 
			
		||||
            bulk(es, actions)
 | 
			
		||||
            actions = []
 | 
			
		||||
 | 
			
		||||
print("Индексирование завершено.")
 | 
			
		||||
    # Если остались неиндексированные документы
 | 
			
		||||
    if actions:
 | 
			
		||||
        bulk(es, actions)
 | 
			
		||||
 | 
			
		||||
    print("\nИндексирование завершено.")
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    create_index()
 | 
			
		||||
    data_path = "../../data_adc_databaza/cleaned_general_info_additional.json"
 | 
			
		||||
    drug_data = load_drug_data(data_path)
 | 
			
		||||
    index_documents(drug_data)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										239
									
								
								Backend/model.py
									
									
									
									
									
								
							
							
						
						
									
										239
									
								
								Backend/model.py
									
									
									
									
									
								
							@ -1,59 +1,80 @@
 | 
			
		||||
from elasticsearch import Elasticsearch
 | 
			
		||||
import json
 | 
			
		||||
import requests
 | 
			
		||||
import logging
 | 
			
		||||
import time
 | 
			
		||||
import re
 | 
			
		||||
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
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from langchain.docstore.document import Document
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Загрузка API ключа Mistral
 | 
			
		||||
mistral_api_key = "hXDC4RBJk1qy5pOlrgr01GtOlmyCBaNs"
 | 
			
		||||
if not mistral_api_key:
 | 
			
		||||
    raise ValueError("API ключ не найден.")
 | 
			
		||||
    raise ValueError("API ключ Mistral не найден в конфигурации.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Класс для работы с моделями Mistral через OpenAI API
 | 
			
		||||
class CustomMistralLLM:
 | 
			
		||||
    def __init__(self, api_key: str, endpoint_url: str):
 | 
			
		||||
    def __init__(self, api_key: str, endpoint_url: str, model_name: str):
 | 
			
		||||
        self.api_key = api_key
 | 
			
		||||
        self.endpoint_url = endpoint_url
 | 
			
		||||
        self.model_name = model_name
 | 
			
		||||
 | 
			
		||||
    def generate_text(self, prompt: str, max_tokens=512, temperature=0.7):
 | 
			
		||||
    def generate_text(self, prompt: str, max_tokens=512, temperature=0.7, retries=3, delay=2):
 | 
			
		||||
        headers = {
 | 
			
		||||
            "Authorization": f"Bearer {self.api_key}",
 | 
			
		||||
            "Content-Type": "application/json"
 | 
			
		||||
        }
 | 
			
		||||
        payload = {
 | 
			
		||||
            "model": "mistral-small-latest",
 | 
			
		||||
            "model": self.model_name,
 | 
			
		||||
            "messages": [{"role": "user", "content": prompt}],
 | 
			
		||||
            "max_tokens": max_tokens,
 | 
			
		||||
            "temperature": temperature
 | 
			
		||||
        }
 | 
			
		||||
        response = requests.post(self.endpoint_url, headers=headers, json=payload)
 | 
			
		||||
        response.raise_for_status()
 | 
			
		||||
        result = response.json()
 | 
			
		||||
        logger.info(f"Полный ответ от модели Mistral: {result}")
 | 
			
		||||
        return result.get("choices", [{}])[0].get("message", {}).get("content", "No response")
 | 
			
		||||
        attempt = 0
 | 
			
		||||
        while attempt < retries:
 | 
			
		||||
            try:
 | 
			
		||||
                response = requests.post(self.endpoint_url, headers=headers, json=payload)
 | 
			
		||||
                response.raise_for_status()
 | 
			
		||||
                result = response.json()
 | 
			
		||||
                logger.info(f"Полный ответ от модели {self.model_name}: {result}")
 | 
			
		||||
                return result.get("choices", [{}])[0].get("message", {}).get("content", "No response")
 | 
			
		||||
            except HTTPError as e:
 | 
			
		||||
                if response.status_code == 429:  # Too Many Requests
 | 
			
		||||
                    logger.warning(f"Превышен лимит запросов. Ожидание {delay} секунд перед повторной попыткой.")
 | 
			
		||||
                    time.sleep(delay)
 | 
			
		||||
                    attempt += 1
 | 
			
		||||
                else:
 | 
			
		||||
                    logger.error(f"HTTP Error: {e}")
 | 
			
		||||
                    raise e
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                logger.error(f"Ошибка: {str(e)}")
 | 
			
		||||
                raise e
 | 
			
		||||
        raise Exception("Превышено количество попыток запроса к API")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Инициализация эмбеддингов
 | 
			
		||||
logger.info("Загрузка модели HuggingFaceEmbeddings...")
 | 
			
		||||
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
 | 
			
		||||
 | 
			
		||||
# Определяем имя индекса
 | 
			
		||||
index_name = 'drug_docs'
 | 
			
		||||
 | 
			
		||||
config_file_path = "config.json"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
with open(config_file_path, 'r') as config_file:
 | 
			
		||||
    config = json.load(config_file)
 | 
			
		||||
 | 
			
		||||
#  Cloud ID
 | 
			
		||||
# Подключение к Elasticsearch
 | 
			
		||||
if config.get("useCloud", False):
 | 
			
		||||
    logger.info("CLOUD ELASTIC")
 | 
			
		||||
    cloud_id = "tt:dXMtZWFzdC0yLmF3cy5lbGFzdGljLWNsb3VkLmNvbTo0NDMkOGM3ODQ0ZWVhZTEyNGY3NmFjNjQyNDFhNjI4NmVhYzMkZTI3YjlkNTQ0ODdhNGViNmEyMTcxMjMxNmJhMWI0ZGU="  # Замените на ваш Cloud ID
 | 
			
		||||
@ -61,48 +82,176 @@ if config.get("useCloud", False):
 | 
			
		||||
        es_cloud_id=cloud_id,
 | 
			
		||||
        index_name='drug_docs',
 | 
			
		||||
        embedding=embeddings,
 | 
			
		||||
        es_user = "elastic",
 | 
			
		||||
        es_password = "sSz2BEGv56JRNjGFwoQ191RJ",
 | 
			
		||||
        es_user="elastic",
 | 
			
		||||
        es_password="sSz2BEGv56JRNjGFwoQ191RJ",
 | 
			
		||||
    )
 | 
			
		||||
else:
 | 
			
		||||
    logger.info("LOCAL ELASTIC")
 | 
			
		||||
    logger.info("LOCALlla ELASTIC")
 | 
			
		||||
    vectorstore = ElasticsearchStore(
 | 
			
		||||
        es_url="http://localhost:9200",
 | 
			
		||||
        index_name='drug_docs',
 | 
			
		||||
        es_url="http://host.docker.internal:9200",
 | 
			
		||||
        index_name=index_name,
 | 
			
		||||
        embedding=embeddings,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
logger.info(f"Подключение установлено к {'облачному' if config.get('useCloud', False) else 'локальному'} Elasticsearch")
 | 
			
		||||
 | 
			
		||||
# LLM
 | 
			
		||||
llm = CustomMistralLLM(
 | 
			
		||||
# Инициализация моделей
 | 
			
		||||
llm_small = CustomMistralLLM(
 | 
			
		||||
    api_key=mistral_api_key,
 | 
			
		||||
    endpoint_url="https://api.mistral.ai/v1/chat/completions"
 | 
			
		||||
    endpoint_url="https://api.mistral.ai/v1/chat/completions",
 | 
			
		||||
    model_name="mistral-small-latest"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
llm_large = CustomMistralLLM(
 | 
			
		||||
    api_key=mistral_api_key,
 | 
			
		||||
    endpoint_url="https://api.mistral.ai/v1/chat/completions",
 | 
			
		||||
    model_name="mistral-large-latest"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Функция для оценки релевантности результатов
 | 
			
		||||
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)  # Максимальная оценка за ключевые слова - 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"Оценка для модели {model_name}: {final_score}/10")
 | 
			
		||||
    logger.info(f"Пояснение оценки:\n{explanation_summary}")
 | 
			
		||||
 | 
			
		||||
    return {"rating": round(final_score, 2), "explanation": explanation_summary}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Функция для сравнения результатов двух моделей
 | 
			
		||||
# Функция для сравнения результатов двух моделей
 | 
			
		||||
# Функция для сравнения результатов двух моделей
 | 
			
		||||
def compare_models(small_model_results, large_model_results, query):
 | 
			
		||||
    logger.info("Начато сравнение моделей Mistral Small и Mistral Large")
 | 
			
		||||
 | 
			
		||||
    # Логируем результаты
 | 
			
		||||
    logger.info("Сравнение оценок моделей:")
 | 
			
		||||
    logger.info(f"Mistral Small: Оценка - {small_model_results['rating']}, Объяснение - {small_model_results['explanation']}")
 | 
			
		||||
    logger.info(f"Mistral Large: Оценка - {large_model_results['rating']}, Объяснение - {large_model_results['explanation']}")
 | 
			
		||||
 | 
			
		||||
    # Форматируем вывод для текстового и векторного поиска
 | 
			
		||||
    comparison_summary = {
 | 
			
		||||
        "query": query,
 | 
			
		||||
        "text_search": f"Текстовый поиск: Mistral Small - {small_model_results['rating']}/10, Mistral Large - {large_model_results['rating']}/10",
 | 
			
		||||
        "vector_search": f"Векторный поиск: Mistral Small - {small_model_results['rating']}/10, Mistral Large - {large_model_results['rating']}/10"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    logger.info(f"Результат сравнения: \n{comparison_summary['text_search']}\n{comparison_summary['vector_search']}")
 | 
			
		||||
 | 
			
		||||
    return comparison_summary
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Функция для обработки запроса
 | 
			
		||||
# Функция для обработки запроса
 | 
			
		||||
# Функция для обработки запроса
 | 
			
		||||
def process_query_with_mistral(query, k=10):
 | 
			
		||||
    logger.info("Обработка запроса началась.")
 | 
			
		||||
    try:
 | 
			
		||||
        # Elasticsearch LangChain
 | 
			
		||||
        response = vectorstore.similarity_search(query, k=k)
 | 
			
		||||
        if not response:
 | 
			
		||||
            return {"summary": "Ничего не найдено", "links": [], "status_log": ["Ничего не найдено."]}
 | 
			
		||||
        # --- ВЕКТОРНЫЙ ПОИСК ---
 | 
			
		||||
        vector_results = vectorstore.similarity_search(query, k=k)
 | 
			
		||||
        vector_documents = [hit.metadata.get('text', '') for hit in vector_results]
 | 
			
		||||
 | 
			
		||||
        documents = [hit.metadata.get('text', '') for hit in response]
 | 
			
		||||
        links = [hit.metadata.get('link', '-') for hit in response]
 | 
			
		||||
        structured_prompt = (
 | 
			
		||||
            f"Na základe otázky: '{query}' a nasledujúcich informácií o liekoch: {documents}. "
 | 
			
		||||
            "Uveďte tri vhodné lieky alebo riešenia s krátkym vysvetlením pre každý z nich. "
 | 
			
		||||
            "Odpoveď musí byť v slovenčine."
 | 
			
		||||
        # Ограничиваем количество документов и их длину
 | 
			
		||||
        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"Na základe otázky: '{query}' a nasledujúcich informácií o liekoch: {vector_documents}. "
 | 
			
		||||
                "Uveďte tri vhodné lieky или riešenia с кратким vysvetlením pre každý z nich. "
 | 
			
		||||
                "Odpoveď musí byť в 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"}
 | 
			
		||||
 | 
			
		||||
        # --- ТЕКСТОВЫЙ ПОИСК ---
 | 
			
		||||
        es_results = vectorstore.client.search(
 | 
			
		||||
            index=index_name,
 | 
			
		||||
            body={"size": k, "query": {"match": {"text": query}}}
 | 
			
		||||
        )
 | 
			
		||||
        text_documents = [hit['_source'].get('text', '') for hit in es_results['hits']['hits']]
 | 
			
		||||
        text_documents = [doc[:max_doc_length] for doc in text_documents[:max_docs]]
 | 
			
		||||
 | 
			
		||||
        summary = llm.generate_text(prompt=structured_prompt, max_tokens=512, temperature=0.7)
 | 
			
		||||
        if text_documents:
 | 
			
		||||
            text_prompt = (
 | 
			
		||||
                f"Na základe otázky: '{query}' a nasledujúcich informácií о liekoch: {text_documents}. "
 | 
			
		||||
                "Uveďte три vhodné lieky alebo riešenia с кратким vysvetленím pre každý з них. "
 | 
			
		||||
                "Odpoveď musí byť в 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)
 | 
			
		||||
 | 
			
		||||
        #TextSplitter
 | 
			
		||||
        splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
 | 
			
		||||
        split_summary = splitter.split_text(summary)
 | 
			
		||||
            split_summary_small_text = splitter.split_text(summary_small_text)
 | 
			
		||||
            split_summary_large_text = splitter.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"}
 | 
			
		||||
 | 
			
		||||
        # Выбираем лучший результат среди всех
 | 
			
		||||
        all_results = [
 | 
			
		||||
            {"eval": small_vector_eval, "summary": summary_small_vector, "model": "Mistral Small Vector"},
 | 
			
		||||
            {"eval": large_vector_eval, "summary": summary_large_vector, "model": "Mistral Large Vector"},
 | 
			
		||||
            {"eval": small_text_eval, "summary": summary_small_text, "model": "Mistral Small Text"},
 | 
			
		||||
            {"eval": large_text_eval, "summary": summary_large_text, "model": "Mistral Large Text"},
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        best_result = max(all_results, key=lambda x: x["eval"]["rating"])
 | 
			
		||||
 | 
			
		||||
        logger.info(f"Лучший результат от модели {best_result['model']} с оценкой {best_result['eval']['rating']}.")
 | 
			
		||||
 | 
			
		||||
        # Возвращаем только лучший ответ
 | 
			
		||||
        return {
 | 
			
		||||
            "best_answer": best_result["summary"],
 | 
			
		||||
            "model": best_result["model"],
 | 
			
		||||
            "rating": best_result["eval"]["rating"],
 | 
			
		||||
            "explanation": best_result["eval"]["explanation"]
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return {"summary": split_summary, "links": links, "status_log": ["Ответ получен от модели Mistral."]}
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.info(f"Ошибка: {str(e)}")
 | 
			
		||||
        return {"summary": "Произошла ошибка", "links": [], "status_log": [f"Ошибка: {str(e)}"]}
 | 
			
		||||
        logger.error(f"Ошибка: {str(e)}")
 | 
			
		||||
        return {
 | 
			
		||||
            "best_answer": "Произошла ошибка при обработке запроса.",
 | 
			
		||||
            "error": str(e)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										191
									
								
								Backend/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								Backend/requirements.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,191 @@
 | 
			
		||||
aiofiles==23.2.1
 | 
			
		||||
aiohttp==3.9.5
 | 
			
		||||
aiosignal==1.3.1
 | 
			
		||||
annotated-types==0.7.0
 | 
			
		||||
anyio==4.4.0
 | 
			
		||||
argcomplete==3.5.0
 | 
			
		||||
attrs==23.2.0
 | 
			
		||||
beautifulsoup4==4.12.3
 | 
			
		||||
black==22.12.0
 | 
			
		||||
blinker==1.8.2
 | 
			
		||||
blobfile==3.0.0
 | 
			
		||||
certifi==2024.7.4
 | 
			
		||||
cffi==1.16.0
 | 
			
		||||
cfgv==3.4.0
 | 
			
		||||
charset-normalizer==3.3.2
 | 
			
		||||
click==8.1.7
 | 
			
		||||
colorama==0.4.6
 | 
			
		||||
contourpy==1.2.1
 | 
			
		||||
coverage==7.6.0
 | 
			
		||||
cryptography==3.4.8
 | 
			
		||||
cycler==0.12.1
 | 
			
		||||
dataclasses-json==0.6.7
 | 
			
		||||
decorator==5.1.1
 | 
			
		||||
Deprecated==1.2.14
 | 
			
		||||
dirtyjson==1.0.8
 | 
			
		||||
distlib==0.3.8
 | 
			
		||||
distro==1.9.0
 | 
			
		||||
dnspython==2.6.1
 | 
			
		||||
docstring_parser==0.16
 | 
			
		||||
docx2txt==0.8
 | 
			
		||||
elastic-transport==8.15.0
 | 
			
		||||
elasticsearch==8.15.1
 | 
			
		||||
email_validator==2.2.0
 | 
			
		||||
eval_type_backport==0.2.0
 | 
			
		||||
fastapi==0.111.1
 | 
			
		||||
fastapi-cli==0.0.4
 | 
			
		||||
ffmpy==0.4.0
 | 
			
		||||
filelock==3.15.4
 | 
			
		||||
fire==0.6.0
 | 
			
		||||
Flask==3.0.3
 | 
			
		||||
Flask-Cors==5.0.0
 | 
			
		||||
fonttools==4.53.1
 | 
			
		||||
frozenlist==1.4.1
 | 
			
		||||
fsspec==2024.6.1
 | 
			
		||||
gradio==4.39.0
 | 
			
		||||
gradio_client==1.1.1
 | 
			
		||||
greenlet==3.0.3
 | 
			
		||||
grpcio==1.63.0
 | 
			
		||||
grpcio-tools==1.62.2
 | 
			
		||||
h11==0.14.0
 | 
			
		||||
h2==4.1.0
 | 
			
		||||
hpack==4.0.0
 | 
			
		||||
httpcore==1.0.5
 | 
			
		||||
httptools==0.6.1
 | 
			
		||||
httpx==0.27.0
 | 
			
		||||
huggingface-hub==0.24.3
 | 
			
		||||
hyperframe==6.0.1
 | 
			
		||||
identify==2.6.0
 | 
			
		||||
idna==3.7
 | 
			
		||||
importlib_metadata==8.4.0
 | 
			
		||||
importlib_resources==6.4.0
 | 
			
		||||
iniconfig==2.0.0
 | 
			
		||||
injector==0.21.0
 | 
			
		||||
itsdangerous==2.2.0
 | 
			
		||||
Jinja2==3.1.4
 | 
			
		||||
jiter==0.5.0
 | 
			
		||||
joblib==1.4.2
 | 
			
		||||
jsonpatch==1.33
 | 
			
		||||
jsonpath-python==1.0.6
 | 
			
		||||
jsonpointer==3.0.0
 | 
			
		||||
jsonschema==4.23.0
 | 
			
		||||
jsonschema-specifications==2023.12.1
 | 
			
		||||
kiwisolver==1.4.5
 | 
			
		||||
langchain==0.3.3
 | 
			
		||||
langchain-community==0.3.2
 | 
			
		||||
langchain-core==0.3.10
 | 
			
		||||
langchain-elasticsearch==0.3.0
 | 
			
		||||
langchain-huggingface==0.1.0
 | 
			
		||||
langchain-text-splitters==0.3.0
 | 
			
		||||
langsmith==0.1.132
 | 
			
		||||
llama-index-core==0.10.58
 | 
			
		||||
llama-index-embeddings-ollama==0.1.2
 | 
			
		||||
llama-index-llms-ollama==0.2.2
 | 
			
		||||
llama-index-readers-file==0.1.31
 | 
			
		||||
llama-index-vector-stores-qdrant==0.2.14
 | 
			
		||||
llama_models==0.0.19
 | 
			
		||||
llama_toolchain==0.0.17
 | 
			
		||||
lxml==5.3.0
 | 
			
		||||
markdown-it-py==3.0.0
 | 
			
		||||
MarkupSafe==2.1.5
 | 
			
		||||
marshmallow==3.21.3
 | 
			
		||||
matplotlib==3.9.1.post1
 | 
			
		||||
mdurl==0.1.2
 | 
			
		||||
minijinja==2.0.1
 | 
			
		||||
mistral_common==1.4.2
 | 
			
		||||
mistral_inference==1.4.0
 | 
			
		||||
mistralai==1.1.0
 | 
			
		||||
mpmath==1.3.0
 | 
			
		||||
multidict==6.0.5
 | 
			
		||||
mypy==1.11.0
 | 
			
		||||
mypy-extensions==1.0.0
 | 
			
		||||
nest-asyncio==1.6.0
 | 
			
		||||
networkx==3.3
 | 
			
		||||
nltk==3.8.1
 | 
			
		||||
nodeenv==1.9.1
 | 
			
		||||
numpy==1.26.4
 | 
			
		||||
ollama==0.3.0
 | 
			
		||||
openai==1.51.2
 | 
			
		||||
opencv-python-headless==4.10.0.84
 | 
			
		||||
opentelemetry-api==1.27.0
 | 
			
		||||
orjson==3.10.6
 | 
			
		||||
packaging==24.1
 | 
			
		||||
pandas==2.2.2
 | 
			
		||||
pathspec==0.12.1
 | 
			
		||||
pillow==10.4.0
 | 
			
		||||
pipx==1.7.1
 | 
			
		||||
platformdirs==4.2.2
 | 
			
		||||
pluggy==1.5.0
 | 
			
		||||
portalocker==2.10.1
 | 
			
		||||
pre-commit==2.21.0
 | 
			
		||||
protobuf==4.25.4
 | 
			
		||||
pycparser==2.22
 | 
			
		||||
pycryptodomex==3.20.0
 | 
			
		||||
pydantic==2.9.2
 | 
			
		||||
pydantic-extra-types==2.9.0
 | 
			
		||||
pydantic-settings==2.5.2
 | 
			
		||||
pydantic_core==2.23.4
 | 
			
		||||
pydub==0.25.1
 | 
			
		||||
Pygments==2.18.0
 | 
			
		||||
pyparsing==3.1.2
 | 
			
		||||
pypdf==4.3.1
 | 
			
		||||
pytest==7.4.4
 | 
			
		||||
pytest-asyncio==0.21.2
 | 
			
		||||
pytest-cov==3.0.0
 | 
			
		||||
python-dateutil==2.8.2
 | 
			
		||||
python-dotenv==1.0.1
 | 
			
		||||
python-multipart==0.0.9
 | 
			
		||||
pytz==2024.1
 | 
			
		||||
PyYAML==6.0.1
 | 
			
		||||
qdrant-client==1.10.1
 | 
			
		||||
referencing==0.35.1
 | 
			
		||||
regex==2024.7.24
 | 
			
		||||
requests==2.32.3
 | 
			
		||||
requests-toolbelt==1.0.0
 | 
			
		||||
retry-async==0.1.4
 | 
			
		||||
rich==13.7.1
 | 
			
		||||
rpds-py==0.20.0
 | 
			
		||||
ruff==0.5.5
 | 
			
		||||
safetensors==0.4.3
 | 
			
		||||
scikit-learn==1.5.2
 | 
			
		||||
scipy==1.14.1
 | 
			
		||||
semantic-version==2.10.0
 | 
			
		||||
sentence-transformers==3.1.0
 | 
			
		||||
sentencepiece==0.2.0
 | 
			
		||||
shellingham==1.5.4
 | 
			
		||||
simple-parsing==0.1.6
 | 
			
		||||
simsimd==5.6.3
 | 
			
		||||
six==1.16.0
 | 
			
		||||
sniffio==1.3.1
 | 
			
		||||
soupsieve==2.5
 | 
			
		||||
SQLAlchemy==2.0.31
 | 
			
		||||
starlette==0.37.2
 | 
			
		||||
striprtf==0.0.26
 | 
			
		||||
sympy==1.13.3
 | 
			
		||||
tenacity==8.5.0
 | 
			
		||||
termcolor==2.4.0
 | 
			
		||||
threadpoolctl==3.5.0
 | 
			
		||||
tiktoken==0.7.0
 | 
			
		||||
tokenizers==0.19.1
 | 
			
		||||
tomlkit==0.12.0
 | 
			
		||||
torch==2.4.1
 | 
			
		||||
tqdm==4.66.4
 | 
			
		||||
transformers==4.43.3
 | 
			
		||||
typer==0.12.3
 | 
			
		||||
types-PyYAML==6.0.12.20240724
 | 
			
		||||
typing-inspect==0.9.0
 | 
			
		||||
typing_extensions==4.12.2
 | 
			
		||||
tzdata==2024.1
 | 
			
		||||
ujson==5.10.0
 | 
			
		||||
urllib3==2.2.2
 | 
			
		||||
userpath==1.9.2
 | 
			
		||||
uvicorn==0.30.3
 | 
			
		||||
virtualenv==20.26.3
 | 
			
		||||
watchdog==4.0.1
 | 
			
		||||
watchfiles==0.22.0
 | 
			
		||||
websockets==11.0.3
 | 
			
		||||
Werkzeug==3.0.4
 | 
			
		||||
wrapt==1.16.0
 | 
			
		||||
xformers==0.0.28.post1
 | 
			
		||||
yarl==1.9.4
 | 
			
		||||
zipp==3.20.2
 | 
			
		||||
@ -1,21 +1,28 @@
 | 
			
		||||
from flask import Flask, request, jsonify
 | 
			
		||||
from flask_cors import CORS  # Импортируем CORS
 | 
			
		||||
from flask_cors import CORS
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
# Импортируем функцию обработки из model.py
 | 
			
		||||
from model import process_query_with_mistral
 | 
			
		||||
 | 
			
		||||
logging.basicConfig(level=logging.INFO)
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
# Создаем Flask приложение
 | 
			
		||||
app = Flask(__name__)
 | 
			
		||||
CORS(app)
 | 
			
		||||
 | 
			
		||||
CORS(app)  # Разрешаем CORS для всех доменов
 | 
			
		||||
CORS(app, resources={r"/api/*": {"origins": "http://localhost:5173"}})
 | 
			
		||||
 | 
			
		||||
# Маршрут для обработки запросов от фронтенда
 | 
			
		||||
@app.route('/api/chat', methods=['POST'])
 | 
			
		||||
def chat():
 | 
			
		||||
    data = request.get_json()
 | 
			
		||||
    query = data.get('query', '')
 | 
			
		||||
    if not query:
 | 
			
		||||
        return jsonify({'error': 'No query provided'}), 400
 | 
			
		||||
 | 
			
		||||
    response = process_query_with_mistral(query)
 | 
			
		||||
 | 
			
		||||
    return jsonify(response)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    app.run(host="0.0.0.0", port=5000, debug=True)
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    app.run(host='0.0.0.0', port=5000, debug=True)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										17
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
version: '3.8'
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
  backend:
 | 
			
		||||
    build:
 | 
			
		||||
      context: ./Backend
 | 
			
		||||
      dockerfile: Dockerfile
 | 
			
		||||
    ports:
 | 
			
		||||
      - "5000:5000"
 | 
			
		||||
    environment:
 | 
			
		||||
      - ELASTICSEARCH_HOST=http://host.docker.internal:9200
 | 
			
		||||
    networks:
 | 
			
		||||
      - app-network
 | 
			
		||||
 | 
			
		||||
networks:
 | 
			
		||||
  app-network:
 | 
			
		||||
    driver: bridge
 | 
			
		||||
							
								
								
									
										41
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										41
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1,23 +1,24 @@
 | 
			
		||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 | 
			
		||||
 | 
			
		||||
# dependencies
 | 
			
		||||
/node_modules
 | 
			
		||||
/.pnp
 | 
			
		||||
.pnp.js
 | 
			
		||||
 | 
			
		||||
# testing
 | 
			
		||||
/coverage
 | 
			
		||||
 | 
			
		||||
# production
 | 
			
		||||
/build
 | 
			
		||||
 | 
			
		||||
# misc
 | 
			
		||||
.DS_Store
 | 
			
		||||
.env.local
 | 
			
		||||
.env.development.local
 | 
			
		||||
.env.test.local
 | 
			
		||||
.env.production.local
 | 
			
		||||
 | 
			
		||||
# Logs
 | 
			
		||||
logs
 | 
			
		||||
*.log
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
pnpm-debug.log*
 | 
			
		||||
lerna-debug.log*
 | 
			
		||||
 | 
			
		||||
node_modules
 | 
			
		||||
dist
 | 
			
		||||
dist-ssr
 | 
			
		||||
*.local
 | 
			
		||||
 | 
			
		||||
# Editor directories and files
 | 
			
		||||
.vscode/*
 | 
			
		||||
!.vscode/extensions.json
 | 
			
		||||
.idea
 | 
			
		||||
.DS_Store
 | 
			
		||||
*.suo
 | 
			
		||||
*.ntvs*
 | 
			
		||||
*.njsproj
 | 
			
		||||
*.sln
 | 
			
		||||
*.sw?
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								frontend/.vite/deps/_metadata.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								frontend/.vite/deps/_metadata.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
  "hash": "c4d2d06d",
 | 
			
		||||
  "configHash": "9db50785",
 | 
			
		||||
  "lockfileHash": "e3b0c442",
 | 
			
		||||
  "browserHash": "efbdb5f3",
 | 
			
		||||
  "optimized": {},
 | 
			
		||||
  "chunks": {}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								frontend/.vite/deps/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/.vite/deps/package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
  "type": "module"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								frontend/.vite/deps_temp_530095be/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/.vite/deps_temp_530095be/package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
  "type": "module"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								frontend/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								frontend/Dockerfile
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
# Используем базовый образ Node.js
 | 
			
		||||
FROM node:18
 | 
			
		||||
 | 
			
		||||
# Устанавливаем рабочую директорию
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
 | 
			
		||||
# Копируем файлы проекта
 | 
			
		||||
COPY . .
 | 
			
		||||
 | 
			
		||||
# Устанавливаем зависимости
 | 
			
		||||
RUN npm install
 | 
			
		||||
 | 
			
		||||
# Собираем проект
 | 
			
		||||
RUN npm run dev
 | 
			
		||||
 | 
			
		||||
# Запускаем сервер
 | 
			
		||||
CMD ["npm", "run", "dev"]
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										28
									
								
								frontend/eslint.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								frontend/eslint.config.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
import js from '@eslint/js'
 | 
			
		||||
import globals from 'globals'
 | 
			
		||||
import reactHooks from 'eslint-plugin-react-hooks'
 | 
			
		||||
import reactRefresh from 'eslint-plugin-react-refresh'
 | 
			
		||||
import tseslint from 'typescript-eslint'
 | 
			
		||||
 | 
			
		||||
export default tseslint.config(
 | 
			
		||||
  { ignores: ['dist'] },
 | 
			
		||||
  {
 | 
			
		||||
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
 | 
			
		||||
    files: ['**/*.{ts,tsx}'],
 | 
			
		||||
    languageOptions: {
 | 
			
		||||
      ecmaVersion: 2020,
 | 
			
		||||
      globals: globals.browser,
 | 
			
		||||
    },
 | 
			
		||||
    plugins: {
 | 
			
		||||
      'react-hooks': reactHooks,
 | 
			
		||||
      'react-refresh': reactRefresh,
 | 
			
		||||
    },
 | 
			
		||||
    rules: {
 | 
			
		||||
      ...reactHooks.configs.recommended.rules,
 | 
			
		||||
      'react-refresh/only-export-components': [
 | 
			
		||||
        'warn',
 | 
			
		||||
        { allowConstantExport: true },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										12
									
								
								frontend/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								frontend/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
<!doctype html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <title>Health AI</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="root"></div>
 | 
			
		||||
    <script type="module" src="/src/main.tsx"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										17928
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										17928
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,54 +1,45 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "frontend",
 | 
			
		||||
  "version": "0.1.0",
 | 
			
		||||
  "name": "coconuts",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "vite",
 | 
			
		||||
    "build": "tsc -b && vite build",
 | 
			
		||||
    "lint": "eslint .",
 | 
			
		||||
    "preview": "vite preview"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@emotion/react": "^11.13.3",
 | 
			
		||||
    "@emotion/styled": "^11.13.0",
 | 
			
		||||
    "@mui/material": "^6.1.2",
 | 
			
		||||
    "@testing-library/jest-dom": "^5.17.0",
 | 
			
		||||
    "@testing-library/react": "^13.4.0",
 | 
			
		||||
    "@testing-library/user-event": "^13.5.0",
 | 
			
		||||
    "@types/jest": "^27.5.2",
 | 
			
		||||
    "@types/node": "^16.18.112",
 | 
			
		||||
    "@types/react": "^18.3.11",
 | 
			
		||||
    "@types/react-dom": "^18.3.0",
 | 
			
		||||
    "@types/three": "^0.169.0",
 | 
			
		||||
    "axios": "^1.7.7",
 | 
			
		||||
    "framer-motion": "^11.11.1",
 | 
			
		||||
    "@gsap/react": "^2.1.1",
 | 
			
		||||
    "@mui/icons-material": "^6.1.5",
 | 
			
		||||
    "@mui/material": "^6.1.5",
 | 
			
		||||
    "@reduxjs/toolkit": "^2.3.0",
 | 
			
		||||
    "appwrite": "^16.0.2",
 | 
			
		||||
    "final-form": "^4.20.10",
 | 
			
		||||
    "gsap": "^3.12.5",
 | 
			
		||||
    "react": "^18.3.1",
 | 
			
		||||
    "react-dom": "^18.3.1",
 | 
			
		||||
    "react-scripts": "5.0.1",
 | 
			
		||||
    "three": "^0.169.0",
 | 
			
		||||
    "typescript": "^4.9.5",
 | 
			
		||||
    "web-vitals": "^2.1.4"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "react-scripts start",
 | 
			
		||||
    "build": "react-scripts build",
 | 
			
		||||
    "test": "react-scripts test",
 | 
			
		||||
    "eject": "react-scripts eject"
 | 
			
		||||
  },
 | 
			
		||||
  "eslintConfig": {
 | 
			
		||||
    "extends": [
 | 
			
		||||
      "react-app",
 | 
			
		||||
      "react-app/jest"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "browserslist": {
 | 
			
		||||
    "production": [
 | 
			
		||||
      ">0.2%",
 | 
			
		||||
      "not dead",
 | 
			
		||||
      "not op_mini all"
 | 
			
		||||
    ],
 | 
			
		||||
    "development": [
 | 
			
		||||
      "last 1 chrome version",
 | 
			
		||||
      "last 1 firefox version",
 | 
			
		||||
      "last 1 safari version"
 | 
			
		||||
    ]
 | 
			
		||||
    "react-final-form": "^6.5.9",
 | 
			
		||||
    "react-icons": "^5.3.0",
 | 
			
		||||
    "react-redux": "^9.1.2",
 | 
			
		||||
    "react-router-dom": "^6.27.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "react-router-dom": "^6.26.2"
 | 
			
		||||
    "@eslint/js": "^9.13.0",
 | 
			
		||||
    "@types/react": "^18.3.11",
 | 
			
		||||
    "@types/react-dom": "^18.3.1",
 | 
			
		||||
    "@vitejs/plugin-react": "^4.3.3",
 | 
			
		||||
    "autoprefixer": "^10.4.20",
 | 
			
		||||
    "eslint": "^9.13.0",
 | 
			
		||||
    "eslint-plugin-react-hooks": "^5.0.0",
 | 
			
		||||
    "eslint-plugin-react-refresh": "^0.4.13",
 | 
			
		||||
    "globals": "^15.11.0",
 | 
			
		||||
    "postcss": "^8.4.47",
 | 
			
		||||
    "tailwindcss": "^3.4.14",
 | 
			
		||||
    "typescript": "~5.6.2",
 | 
			
		||||
    "typescript-eslint": "^8.10.0",
 | 
			
		||||
    "vite": "^5.4.9"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										6
									
								
								frontend/postcss.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								frontend/postcss.config.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
export default {
 | 
			
		||||
  plugins: {
 | 
			
		||||
    tailwindcss: {},
 | 
			
		||||
    autoprefixer: {},
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								frontend/public/vite.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/public/vite.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.5 KiB  | 
							
								
								
									
										37
									
								
								frontend/src/App.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								frontend/src/App.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
			
		||||
import { BrowserRouter as Router, Route, Routes, Outlet } from 'react-router-dom';
 | 
			
		||||
import Navigation from './components/Navigation';
 | 
			
		||||
import HomePage from './pages/HomePage';
 | 
			
		||||
import LandingPage from './pages/LandingPage';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const Layout = () => (
 | 
			
		||||
  <div className="flex w-full h-screen dark:bg-slate-200">
 | 
			
		||||
    <Navigation isExpanded={false} />
 | 
			
		||||
    <div className="flex-grow p-3 h-full">
 | 
			
		||||
      <main className="h-full w-full border rounded-xl dark:bg-slate-100 shadow-xl" >
 | 
			
		||||
        <Outlet />
 | 
			
		||||
      </main>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function App() {
 | 
			
		||||
  return (
 | 
			
		||||
    <Router>
 | 
			
		||||
      <Routes>
 | 
			
		||||
        <Route path='/' element={<LandingPage />} />
 | 
			
		||||
        <Route path="solutions" element={<>Sorry not implemented yet</>} />
 | 
			
		||||
        <Route path="contact" element={<>Sorry not implemented yet</>} />
 | 
			
		||||
        <Route path="about" element={<>Sorry not implemented yet</>} />
 | 
			
		||||
        <Route path="/dashboard" element={<Layout />}>
 | 
			
		||||
          <Route index element={<HomePage />} />
 | 
			
		||||
          <Route path="history" element={<>Sorry not implemented yet</>} />
 | 
			
		||||
        </Route>
 | 
			
		||||
      </Routes>
 | 
			
		||||
    </Router>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default App
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										100
									
								
								frontend/src/Components/EatingForm.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								frontend/src/Components/EatingForm.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,100 @@
 | 
			
		||||
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;
 | 
			
		||||
							
								
								
									
										235
									
								
								frontend/src/Components/Human.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								frontend/src/Components/Human.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,235 @@
 | 
			
		||||
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;
 | 
			
		||||
							
								
								
									
										0
									
								
								frontend/src/Components/Human2d.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								frontend/src/Components/Human2d.tsx
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										275
									
								
								frontend/src/Components/MultistepForm.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										275
									
								
								frontend/src/Components/MultistepForm.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,275 @@
 | 
			
		||||
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;
 | 
			
		||||
							
								
								
									
										121
									
								
								frontend/src/Components/Navigation.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								frontend/src/Components/Navigation.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,121 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react'
 | 
			
		||||
import { IoMdHome } from "react-icons/io";
 | 
			
		||||
import { GoHistory } from "react-icons/go";
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import { MdOutlineDarkMode } from "react-icons/md";
 | 
			
		||||
import { CiLight } from "react-icons/ci";
 | 
			
		||||
import IconButton from '@mui/material/IconButton';
 | 
			
		||||
import BackImage from '../assets/smallheadicon.png'
 | 
			
		||||
 | 
			
		||||
export interface NavigationItem {
 | 
			
		||||
    icon: React.ReactNode,
 | 
			
		||||
    title: string
 | 
			
		||||
    link: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const NavigationItems: NavigationItem[] = [
 | 
			
		||||
    {
 | 
			
		||||
        title: 'Dashboard',
 | 
			
		||||
        link: '/dashboard',
 | 
			
		||||
        icon: <IoMdHome size={30} />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        title: 'History',
 | 
			
		||||
        link: '/dashboard/history',
 | 
			
		||||
        icon: <GoHistory size={25} />
 | 
			
		||||
    }
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
interface NavigationProps {
 | 
			
		||||
    isExpanded: boolean,
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const Navigation = ({ isExpanded = false }: NavigationProps) => {
 | 
			
		||||
 | 
			
		||||
    const [theme, setTheme] = useState<'dark' | 'light'>('light')
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (window.matchMedia('(prefers-color-scheme:dark)').matches) {
 | 
			
		||||
            setTheme('dark');
 | 
			
		||||
        } else {
 | 
			
		||||
            setTheme('light')
 | 
			
		||||
        }
 | 
			
		||||
    }, [])
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (theme === "dark") {
 | 
			
		||||
            document.documentElement.classList.add("dark");
 | 
			
		||||
        } else {
 | 
			
		||||
            document.documentElement.classList.remove("dark");
 | 
			
		||||
        }
 | 
			
		||||
    }, [theme])
 | 
			
		||||
 | 
			
		||||
    const handleThemeSwitch = () => {
 | 
			
		||||
        setTheme(theme === "dark" ? "light" : "dark")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className='h-full p-3 w-fit'>
 | 
			
		||||
            <div className='h-full rounded-xl border flex flex-col px-1 justify-between py-2 items-center dark:bg-slate-300 shadow-xl'>
 | 
			
		||||
                <div className='flex flex-col items-start gap-12'>
 | 
			
		||||
                    <Link to='/' className='w-full flex items-center justify-center' >
 | 
			
		||||
                        <IconButton sx={{
 | 
			
		||||
                            width: 40,
 | 
			
		||||
                            height: 40,
 | 
			
		||||
                        }} >
 | 
			
		||||
                            <img src={BackImage} width={25} alt="" />
 | 
			
		||||
                        </IconButton>
 | 
			
		||||
                        {isExpanded && <p className='text-2xl font-semibold text-dark-blue flex items-center' >Health AI</p>}
 | 
			
		||||
                    </Link>
 | 
			
		||||
                    <div className='flex flex-col p-1 gap-5 items-center'>
 | 
			
		||||
 | 
			
		||||
                        {NavigationItems.map((item) => (
 | 
			
		||||
                            <Link key={item.link} to={item.link} className='flex gap-2 items-center w-full'>
 | 
			
		||||
                                <IconButton sx={{
 | 
			
		||||
                                    width: 40,
 | 
			
		||||
                                    height: 40,
 | 
			
		||||
                                    borderRadius: 2,
 | 
			
		||||
                                    backgroundColor: '#FFFFFF',
 | 
			
		||||
                                    ...(theme === 'dark' && {
 | 
			
		||||
                                        '&:hover': {
 | 
			
		||||
                                            backgroundColor: '#eef3f4',
 | 
			
		||||
                                            borderColor: '#0062cc',
 | 
			
		||||
                                            boxShadow: 'none',
 | 
			
		||||
                                        },
 | 
			
		||||
                                    }),
 | 
			
		||||
                                }} >
 | 
			
		||||
                                    {item.icon}
 | 
			
		||||
                                </IconButton>
 | 
			
		||||
                                {isExpanded && item.title}
 | 
			
		||||
                            </Link>
 | 
			
		||||
                        ))}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <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>
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Navigation
 | 
			
		||||
							
								
								
									
										201
									
								
								frontend/src/Components/UserMetricsForm.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								frontend/src/Components/UserMetricsForm.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,201 @@
 | 
			
		||||
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;
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								frontend/src/assets/headicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/src/assets/headicon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 34 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								frontend/src/assets/smallheadicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/src/assets/smallheadicon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 24 KiB  | 
@ -1,13 +1,37 @@
 | 
			
		||||
body {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
 | 
			
		||||
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
 | 
			
		||||
    sans-serif;
 | 
			
		||||
  -webkit-font-smoothing: antialiased;
 | 
			
		||||
  -moz-osx-font-smoothing: grayscale;
 | 
			
		||||
@tailwind base;
 | 
			
		||||
@tailwind components;
 | 
			
		||||
@tailwind utilities;
 | 
			
		||||
 | 
			
		||||
/* Приховує скроллбар у браузерах на основі WebKit (Chrome, Safari) */
 | 
			
		||||
.no-scrollbar::-webkit-scrollbar {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
code {
 | 
			
		||||
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
 | 
			
		||||
    monospace;
 | 
			
		||||
/* Приховує скроллбар у Firefox */
 | 
			
		||||
.no-scrollbar {
 | 
			
		||||
    scrollbar-width: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Приховує скроллбар в Internet Explorer та Edge */
 | 
			
		||||
.no-scrollbar {
 | 
			
		||||
    -ms-overflow-style: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* HTML: <div class="loader"></div> */
 | 
			
		||||
.loader {
 | 
			
		||||
    width: 20px;
 | 
			
		||||
    aspect-ratio: 2;
 | 
			
		||||
    --_g: no-repeat radial-gradient(circle closest-side,#000 90%,#0000);
 | 
			
		||||
    background: 
 | 
			
		||||
      var(--_g) 0%   50%,
 | 
			
		||||
      var(--_g) 50%  50%,
 | 
			
		||||
      var(--_g) 100% 50%;
 | 
			
		||||
    background-size: calc(100%/3) 50%;
 | 
			
		||||
    animation: l3 1s infinite linear;
 | 
			
		||||
  }
 | 
			
		||||
  @keyframes l3 {
 | 
			
		||||
      20%{background-position:0%   0%, 50%  50%,100%  50%}
 | 
			
		||||
      40%{background-position:0% 100%, 50%   0%,100%  50%}
 | 
			
		||||
      60%{background-position:0%  50%, 50% 100%,100%   0%}
 | 
			
		||||
      80%{background-position:0%  50%, 50%  50%,100% 100%}
 | 
			
		||||
  }
 | 
			
		||||
							
								
								
									
										15
									
								
								frontend/src/main.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/main.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
import { StrictMode } from 'react'
 | 
			
		||||
import { createRoot } from 'react-dom/client'
 | 
			
		||||
import './index.css'
 | 
			
		||||
import App from './App.tsx'
 | 
			
		||||
import { Provider } from 'react-redux'
 | 
			
		||||
import store from './store/index.ts'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
createRoot(document.getElementById('root')!).render(
 | 
			
		||||
  <StrictMode>
 | 
			
		||||
    <Provider store={store}>
 | 
			
		||||
    <App />
 | 
			
		||||
    </Provider>
 | 
			
		||||
  </StrictMode>,
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										117
									
								
								frontend/src/pages/HomePage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								frontend/src/pages/HomePage.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,117 @@
 | 
			
		||||
import { CgGym } from "react-icons/cg";
 | 
			
		||||
import { FaBed } from "react-icons/fa6";
 | 
			
		||||
import { MdFastfood } from "react-icons/md";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import gsap from "gsap";
 | 
			
		||||
import { useGSAP } from "@gsap/react";
 | 
			
		||||
 | 
			
		||||
import { useLazySendChatQuestionQuery } from "../store/api/chatApi";
 | 
			
		||||
 | 
			
		||||
const HomePage = () => {
 | 
			
		||||
    const [sendChatQuestion, { isLoading, isFetching }] = useLazySendChatQuestionQuery();
 | 
			
		||||
 | 
			
		||||
    type Category = 'sport' | 'feed' | 'sleep';
 | 
			
		||||
    const [category, setCategory] = useState<Category | null>(null);
 | 
			
		||||
    const [message, setMessage] = useState<string>('');
 | 
			
		||||
    const [chatHistory, setChatHistory] = useState<{ sender: string; text: string, rating?: number, explanation?: string }[]>([]);
 | 
			
		||||
 | 
			
		||||
    async function onSubmit() {
 | 
			
		||||
        if (!message.trim()) return;
 | 
			
		||||
        setChatHistory([...chatHistory, { sender: 'User', text: message }]);
 | 
			
		||||
        setMessage('');
 | 
			
		||||
 | 
			
		||||
        const question = { query: message };
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const res = await sendChatQuestion(question).unwrap();
 | 
			
		||||
            console.log("Response from server:", res);
 | 
			
		||||
 | 
			
		||||
            // Извлекаем лучший ответ и очищаем его от ненужных символов
 | 
			
		||||
            let bestAnswer = res.best_answer.replace(/[*#]/g, "");
 | 
			
		||||
            const model = res.model;
 | 
			
		||||
 | 
			
		||||
            // Форматируем ответ для удобства чтения
 | 
			
		||||
            bestAnswer = bestAnswer.replace(/(\d\.\s)/g, "\n\n$1").replace(/:\s-/g, ":\n-");
 | 
			
		||||
 | 
			
		||||
            // Создаем сообщение для чата с лучшим ответом
 | 
			
		||||
            const assistantMessage = {
 | 
			
		||||
                sender: 'Assistant',
 | 
			
		||||
                text: `Model: ${model}:\n${bestAnswer}`,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            setChatHistory((prev) => [...prev, assistantMessage]);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error("Error:", error);
 | 
			
		||||
            setChatHistory((prev) => [...prev, { sender: 'Assistant', text: "Что-то пошло не так" }]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    useGSAP(() => {
 | 
			
		||||
        gsap.from('#firstheading', { opacity: 0.3, ease: 'power2.inOut', duration: 0.5 });
 | 
			
		||||
        gsap.from('#secondheading', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.3, duration: 0.5 });
 | 
			
		||||
        gsap.from('#buttons', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.3, duration: 0.5 });
 | 
			
		||||
        gsap.from('#input', { opacity: 0, y: 5, ease: 'power2.inOut', duration: 0.5 });
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className='w-full h-full flex flex-col justify-end items-center p-4 gap-8'>
 | 
			
		||||
            <div className="w-full overflow-y-auto no-scrollbar h-full p-2 border-gray-200 mb-4">
 | 
			
		||||
                {chatHistory.length > 0 ? (
 | 
			
		||||
                    <>
 | 
			
		||||
                        {chatHistory.map((msg, index) => (
 | 
			
		||||
                            <div
 | 
			
		||||
                                key={index}
 | 
			
		||||
                                className={`flex ${msg.sender === 'User' ? 'justify-end' : 'justify-start'} mb-2`}
 | 
			
		||||
                            >
 | 
			
		||||
                                <div
 | 
			
		||||
                                    className={`p-2 rounded-lg max-w-md ${msg.sender === 'User' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800'}`}
 | 
			
		||||
                                >
 | 
			
		||||
                                    {msg.text.split("\n").map((line, i) => (
 | 
			
		||||
                                        <p key={i}>{line}</p>
 | 
			
		||||
                                    ))}
 | 
			
		||||
                                    {msg.rating && <p>Rating: {msg.rating}</p>}
 | 
			
		||||
                                    {msg.explanation && <p>Explanation: {msg.explanation}</p>}
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        ))}
 | 
			
		||||
                        {(isLoading || isFetching) && (
 | 
			
		||||
                            <div className="flex justify-start mb-2">
 | 
			
		||||
                                <div className="p-2 rounded-lg max-w-md bg-gray-200 text-gray-800">
 | 
			
		||||
                                    <p className="flex items-center">I'm thinking <div className="loader"></div></p>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        )}
 | 
			
		||||
                    </>
 | 
			
		||||
                ) : (
 | 
			
		||||
                    <div className="w-full h-full items-center flex flex-col gap-2 justify-center">
 | 
			
		||||
                        <h1 className="text-xl" id="firstheading">Ask any question or advice about your health or trainings and let's see what happens</h1>
 | 
			
		||||
                        <h2 className="text-gray-600" id="secondheading">Choose a category for a better experience and make your life better with Health AI</h2>
 | 
			
		||||
                    </div>
 | 
			
		||||
                )}
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div id="buttons">
 | 
			
		||||
                <div className="flex gap-6">
 | 
			
		||||
                    <button onClick={() => setCategory('sport')} className={`flex items-center shadow-lg justify-center gap-2 ${category === 'sport' ? 'bg-bright-blue' : 'bg-gray-300'}  text-white rounded-md font-medium hover:opacity-90 duration-200 h-12 w-40`}>
 | 
			
		||||
                        Training <CgGym size={30} />
 | 
			
		||||
                    </button>
 | 
			
		||||
                    <button onClick={() => setCategory('sleep')} className={`flex items-center shadow-lg justify-center gap-2  ${category === 'sleep' ? 'bg-bright-blue' : 'bg-gray-300'} text-white rounded-md font-medium hover:opacity-90 duration-200 h-12 w-40`}>
 | 
			
		||||
                        Sleep  <FaBed size={25} />
 | 
			
		||||
                    </button>
 | 
			
		||||
                    <button onClick={() => setCategory('feed')} className={`flex items-center shadow-lg justify-center gap-2  ${category === 'feed' ? 'bg-bright-blue' : 'bg-gray-300'} text-white rounded-md font-medium hover:opacity-90 duration-200 h-12 w-40`}>
 | 
			
		||||
                        Feed  <MdFastfood size={20} />
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div id="input" className="w-2/3 rounded-xl drop-shadow-2xl mb-20">
 | 
			
		||||
                <div className="flex">
 | 
			
		||||
                    <input placeholder="Waiting for your question..." value={message} onChange={(e) => setMessage(e.target.value)} className="w-full px-5 py-2 rounded-l-xl outline-none" type="text" />
 | 
			
		||||
                    <button disabled={isLoading || isFetching} onClick={onSubmit} className="bg-black rounded-r-xl px-4 py-2 text-white font-semibold hover:bg-slate-700">Send</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default HomePage;
 | 
			
		||||
							
								
								
									
										121
									
								
								frontend/src/pages/LandingPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								frontend/src/pages/LandingPage.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,121 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { CgLogIn } from "react-icons/cg";
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import BackImage from '../assets/smallheadicon.png'
 | 
			
		||||
import MultiStepForm from '../Components/MultistepForm';
 | 
			
		||||
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
 | 
			
		||||
import { Box } from '@mui/material';
 | 
			
		||||
import gsap from 'gsap';
 | 
			
		||||
import { useGSAP } from '@gsap/react';
 | 
			
		||||
 | 
			
		||||
const BouncingArrow = () => {
 | 
			
		||||
    return (
 | 
			
		||||
        <Box
 | 
			
		||||
            sx={{
 | 
			
		||||
                display: 'flex',
 | 
			
		||||
                justifyContent: 'center',
 | 
			
		||||
                alignItems: 'center',
 | 
			
		||||
                mt: 2,
 | 
			
		||||
                animation: 'bounce 1s infinite', // Додаємо анімацію
 | 
			
		||||
                '@keyframes bounce': { // Описуємо ключові кадри для анімації
 | 
			
		||||
                    '0%, 100%': {
 | 
			
		||||
                        transform: 'translateY(0)',
 | 
			
		||||
                    },
 | 
			
		||||
                    '50%': {
 | 
			
		||||
                        transform: 'translateY(-10px)',
 | 
			
		||||
                    },
 | 
			
		||||
                }
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <ArrowDownwardIcon fontSize="large" />
 | 
			
		||||
        </Box>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
const Navbar: React.FC = () => {
 | 
			
		||||
    return (
 | 
			
		||||
        <nav className="w-full bg-white shadow-md py-4 px-2 sm:px-8 flex justify-between items-center fixed top-0 left-0 right-0 z-50">
 | 
			
		||||
            <div className="text-2xl font-semibold text-dark-blue flex items-center">
 | 
			
		||||
                Health AI
 | 
			
		||||
                <img src={BackImage} width={25} alt="" />
 | 
			
		||||
            </div>
 | 
			
		||||
            <ul className="flex space-x-6 text-gray-600">
 | 
			
		||||
                <li><Link to="/dashboard" className="hover:text-bright-blue transition duration-300">Home</Link></li>
 | 
			
		||||
                <li><Link to="/solutions" className="hover:text-bright-blue transition duration-300">Solutions</Link></li>
 | 
			
		||||
                <li><Link to="/about" className="hover:text-bright-blue transition duration-300">About</Link></li>
 | 
			
		||||
                <li><Link to="/contact" className="hover:text-bright-blue transition duration-300">Contact</Link></li>
 | 
			
		||||
            </ul>
 | 
			
		||||
            <div className='flex gap-2 items-center'>
 | 
			
		||||
                Sign in <CgLogIn size={25} />
 | 
			
		||||
            </div>
 | 
			
		||||
        </nav>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const Home: React.FC = () => {
 | 
			
		||||
 | 
			
		||||
    useGSAP(() => {
 | 
			
		||||
        gsap.from('#mainheading', { opacity: 0.3, ease: 'power2.inOut', duration: 0.5 })
 | 
			
		||||
        gsap.from('#secondheading', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.3, duration: 0.5 })
 | 
			
		||||
        gsap.from('#button', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.5, duration: 0.5 })
 | 
			
		||||
        gsap.from('#features', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.7, duration: 0.5 })
 | 
			
		||||
        gsap.from('#arrow', { opacity: 0, ease: 'power2.inOut', delay: 2, duration: 0.2 })
 | 
			
		||||
    }, [])
 | 
			
		||||
 | 
			
		||||
    return (<>
 | 
			
		||||
        <div className="h-screen flex flex-col items-center justify-center bg-gradient-to-b text-gray-800 p-4">
 | 
			
		||||
            {/* Навігація */}
 | 
			
		||||
            <Navbar />
 | 
			
		||||
 | 
			
		||||
            <div className="pt-20 flex flex-col items-center">
 | 
			
		||||
                <h1 id='mainheading' className="text-4xl flex items-center sm:text-5xl md:text-6xl font-semibold mb-4 text-center text-dark-blue">
 | 
			
		||||
                    AI Assistant for Your Health
 | 
			
		||||
                </h1>
 | 
			
		||||
 | 
			
		||||
                <p id='secondheading' className="text-base sm:text-lg md:text-xl text-center max-w-2xl mb-8 text-gray-700">
 | 
			
		||||
                    The product for health improvement, trainings, and other. Care for yourself with modern technologies – be a modern human.
 | 
			
		||||
                </p>
 | 
			
		||||
 | 
			
		||||
                <Link id='button' to='/dashboard'>
 | 
			
		||||
                    <button  className="bg-bright-blue text-white font-medium py-2 px-5 rounded hover:bg-deep-blue transition duration-300 mb-10 shadow-md">
 | 
			
		||||
                        Get started
 | 
			
		||||
                    </button>
 | 
			
		||||
                </Link>
 | 
			
		||||
 | 
			
		||||
                <div className="flex flex-col sm:flex-row gap-6 mb-10" id="features">
 | 
			
		||||
                    <div className="bg-white p-6 rounded-lg max-w-xs text-center shadow-md">
 | 
			
		||||
                        <h3 className="text-xl font-medium mb-3 text-dark-blue">Personalized Training</h3>
 | 
			
		||||
                        <p className="text-gray-600">Get customized training plans designed just for you and track your progress effectively.</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="bg-white p-6 rounded-lg max-w-xs text-center shadow-md">
 | 
			
		||||
                        <h3 className="text-xl font-medium mb-3 text-dark-blue">Health Monitoring</h3>
 | 
			
		||||
                        <p className="text-gray-600">Stay informed about your health with real-time monitoring and AI-driven insights.</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="bg-white p-6 rounded-lg max-w-xs text-center shadow-md">
 | 
			
		||||
                        <h3 className="text-xl font-medium mb-3 text-dark-blue">Advanced AI Support</h3>
 | 
			
		||||
                        <p className="text-gray-600">Utilize AI support to ensure you're following the best routines for a healthier lifestyle.</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div id='arrow' className='flex flex-col items-center mt-10 z-0'>
 | 
			
		||||
                    <p className='text-gray-600'>Try it out</p>
 | 
			
		||||
                    <BouncingArrow />
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className='w-full h-screen flex flex-col justify-center items-center' >
 | 
			
		||||
 | 
			
		||||
            <MultiStepForm />
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
        <footer className=" mt-auto text-center text-gray-500 p-4">
 | 
			
		||||
            <p>© {new Date().getFullYear()} Health AI. All rights reserved.</p>
 | 
			
		||||
        </footer>
 | 
			
		||||
    </>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default Home;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										33
									
								
								frontend/src/store/api/chatApi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								frontend/src/store/api/chatApi.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
			
		||||
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// type chatQuestion = {
 | 
			
		||||
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
const chatApi = createApi({
 | 
			
		||||
    reducerPath: 'chat',
 | 
			
		||||
    baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:5000' }),
 | 
			
		||||
    endpoints: (builder) => ({
 | 
			
		||||
        sendTestVersion: builder.query<string, any>({
 | 
			
		||||
            query: (body) => ({
 | 
			
		||||
                url: '/create-answer',
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                body: body
 | 
			
		||||
            }),
 | 
			
		||||
            transformResponse: ({ response }) => response
 | 
			
		||||
        }),
 | 
			
		||||
        sendChatQuestion: builder.query<any, any>({
 | 
			
		||||
            query: (body) => ({
 | 
			
		||||
                url: '/api/chat',
 | 
			
		||||
                method: 'POST',
 | 
			
		||||
                body: body
 | 
			
		||||
            })
 | 
			
		||||
        })
 | 
			
		||||
    })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default chatApi
 | 
			
		||||
export const { useLazySendTestVersionQuery,useLazySendChatQuestionQuery } = chatApi
 | 
			
		||||
							
								
								
									
										19
									
								
								frontend/src/store/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/src/store/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
import { configureStore } from "@reduxjs/toolkit";
 | 
			
		||||
import chatApi from "./api/chatApi";
 | 
			
		||||
import { setupListeners } from '@reduxjs/toolkit/query';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const store = configureStore({
 | 
			
		||||
    reducer: {
 | 
			
		||||
        [chatApi.reducerPath]: chatApi.reducer
 | 
			
		||||
    },
 | 
			
		||||
    middleware: (getDefaultMiddleware) =>
 | 
			
		||||
        getDefaultMiddleware().concat(chatApi.middleware),
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
setupListeners(store.dispatch);
 | 
			
		||||
 | 
			
		||||
export type RootState = ReturnType<typeof store.getState>;
 | 
			
		||||
export type AppDispatch = typeof store.dispatch;
 | 
			
		||||
 | 
			
		||||
export default store;
 | 
			
		||||
							
								
								
									
										1
									
								
								frontend/src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
/// <reference types="vite/client" />
 | 
			
		||||
							
								
								
									
										22
									
								
								frontend/tailwind.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								frontend/tailwind.config.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
/** @type {import('tailwindcss').Config} */
 | 
			
		||||
export default {
 | 
			
		||||
  content: [
 | 
			
		||||
    "./index.html",
 | 
			
		||||
    "./src/**/*.{js,ts,jsx,tsx}",
 | 
			
		||||
  ],
 | 
			
		||||
  darkMode: "class",
 | 
			
		||||
  theme: {
 | 
			
		||||
    extend: {
 | 
			
		||||
      colors: {
 | 
			
		||||
        'light-blue': '#dbeafe',
 | 
			
		||||
        'soft-blue': '#bfdbfe',
 | 
			
		||||
        'light-cyan': '#e0f7fa',
 | 
			
		||||
        'dark-blue': '#1e3a8a',
 | 
			
		||||
        'bright-blue': '#2563eb',
 | 
			
		||||
        'deep-blue': '#1d4ed8',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										25
									
								
								frontend/tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								frontend/tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
			
		||||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "target": "ES2020",
 | 
			
		||||
    "useDefineForClassFields": true,
 | 
			
		||||
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
 | 
			
		||||
    "module": "ESNext",
 | 
			
		||||
    "skipLibCheck": true,
 | 
			
		||||
 | 
			
		||||
    /* Bundler mode */
 | 
			
		||||
    "moduleResolution": "Bundler",
 | 
			
		||||
    "allowImportingTsExtensions": true,
 | 
			
		||||
    "isolatedModules": true,
 | 
			
		||||
    "moduleDetection": "force",
 | 
			
		||||
    "noEmit": true,
 | 
			
		||||
    "jsx": "react-jsx",
 | 
			
		||||
 | 
			
		||||
    /* Linting */
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "noUnusedLocals": true,
 | 
			
		||||
    "noUnusedParameters": true,
 | 
			
		||||
    "noFallthroughCasesInSwitch": true,
 | 
			
		||||
    "noUncheckedSideEffectImports": true
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["src"]
 | 
			
		||||
}
 | 
			
		||||
@ -1,27 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "target": "es5",
 | 
			
		||||
    "lib": [
 | 
			
		||||
      "dom",
 | 
			
		||||
      "dom.iterable",
 | 
			
		||||
      "esnext"
 | 
			
		||||
    ],
 | 
			
		||||
    "allowJs": true,
 | 
			
		||||
    "skipLibCheck": true,
 | 
			
		||||
    "esModuleInterop": true,
 | 
			
		||||
    "allowSyntheticDefaultImports": true,
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "forceConsistentCasingInFileNames": true,
 | 
			
		||||
    "noFallthroughCasesInSwitch": true,
 | 
			
		||||
    "module": "esnext",
 | 
			
		||||
    "moduleResolution": "node",
 | 
			
		||||
    "resolveJsonModule": true,
 | 
			
		||||
    "isolatedModules": true,
 | 
			
		||||
    "noEmit": true,
 | 
			
		||||
    "jsx": "react-jsx",
 | 
			
		||||
  },
 | 
			
		||||
  "include": [
 | 
			
		||||
    "src",
 | 
			
		||||
    "declarations.d.ts"
 | 
			
		||||
  "files": [],
 | 
			
		||||
  "references": [
 | 
			
		||||
    { "path": "./tsconfig.app.json" },
 | 
			
		||||
    { "path": "./tsconfig.node.json" }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										23
									
								
								frontend/tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								frontend/tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "target": "ES2022",
 | 
			
		||||
    "lib": ["ES2023"],
 | 
			
		||||
    "module": "ESNext",
 | 
			
		||||
    "skipLibCheck": true,
 | 
			
		||||
 | 
			
		||||
    /* Bundler mode */
 | 
			
		||||
    "moduleResolution": "Bundler",
 | 
			
		||||
    "allowImportingTsExtensions": true,
 | 
			
		||||
    "isolatedModules": true,
 | 
			
		||||
    "moduleDetection": "force",
 | 
			
		||||
    "noEmit": true,
 | 
			
		||||
 | 
			
		||||
    /* Linting */
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "noUnusedLocals": true,
 | 
			
		||||
    "noUnusedParameters": true,
 | 
			
		||||
    "noFallthroughCasesInSwitch": true,
 | 
			
		||||
    "noUncheckedSideEffectImports": true
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["vite.config.ts"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								frontend/vite.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/vite.config.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
import { defineConfig } from 'vite'
 | 
			
		||||
import react from '@vitejs/plugin-react'
 | 
			
		||||
 | 
			
		||||
// https://vite.dev/config/
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  plugins: [react()],
 | 
			
		||||
})
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user