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 elasticsearch import Elasticsearch
|
||||||
from langchain_huggingface import HuggingFaceEmbeddings
|
from langchain_huggingface import HuggingFaceEmbeddings
|
||||||
|
from elasticsearch.helpers import bulk
|
||||||
|
import json
|
||||||
|
|
||||||
es = Elasticsearch([{'host': 'localhost', 'port': 9200, 'scheme': 'http'}])
|
es = Elasticsearch([{'host': 'localhost', 'port': 9200, 'scheme': 'http'}])
|
||||||
|
|
||||||
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
||||||
|
|
||||||
|
def create_index():
|
||||||
|
# Определяем маппинг для индекса
|
||||||
|
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):
|
def load_drug_data(json_path):
|
||||||
with open(json_path, 'r', encoding='utf-8') as f:
|
with open(json_path, 'r', encoding='utf-8') as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def index_documents(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', '')}"
|
doc_text = f"{item['link']} {item.get('pribalovy_letak', '')} {item.get('spc', '')}"
|
||||||
|
|
||||||
vector = embeddings.embed_query(doc_text)
|
vector = embeddings.embed_query(doc_text)
|
||||||
|
|
||||||
es.index(index='drug_docs', id=i, body={
|
action = {
|
||||||
|
"_index": "drug_docs",
|
||||||
|
"_id": i,
|
||||||
|
"_source": {
|
||||||
'text': doc_text,
|
'text': doc_text,
|
||||||
'vector': vector,
|
'vector': vector,
|
||||||
'full_data': item
|
'full_data': item
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
actions.append(action)
|
||||||
|
|
||||||
|
# Отображение прогресса
|
||||||
|
print(f"Индексируется документ {i}/{total_docs}", end='\r')
|
||||||
|
|
||||||
data_path = "data/cleaned_general_info_additional.json"
|
# Опционально: индексируем пакетами по N документов
|
||||||
|
if i % 100 == 0 or i == total_docs:
|
||||||
|
bulk(es, actions)
|
||||||
|
actions = []
|
||||||
|
|
||||||
|
# Если остались неиндексированные документы
|
||||||
|
if actions:
|
||||||
|
bulk(es, actions)
|
||||||
|
|
||||||
|
print("\nИндексирование завершено.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
create_index()
|
||||||
|
data_path = "../../data_adc_databaza/cleaned_general_info_additional.json"
|
||||||
drug_data = load_drug_data(data_path)
|
drug_data = load_drug_data(data_path)
|
||||||
index_documents(drug_data)
|
index_documents(drug_data)
|
||||||
|
|
||||||
print("Индексирование завершено.")
|
|
||||||
|
227
Backend/model.py
227
Backend/model.py
@ -1,59 +1,80 @@
|
|||||||
from elasticsearch import Elasticsearch
|
|
||||||
import json
|
import json
|
||||||
import requests
|
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 SequentialChain
|
||||||
from langchain.chains import LLMChain, SequentialChain
|
from langchain.chains import LLMChain, SequentialChain
|
||||||
from langchain_huggingface import HuggingFaceEmbeddings
|
from langchain_huggingface import HuggingFaceEmbeddings
|
||||||
from langchain_elasticsearch import ElasticsearchStore
|
from langchain_elasticsearch import ElasticsearchStore
|
||||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||||
import logging
|
from langchain.docstore.document import Document
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
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"
|
mistral_api_key = "hXDC4RBJk1qy5pOlrgr01GtOlmyCBaNs"
|
||||||
if not mistral_api_key:
|
if not mistral_api_key:
|
||||||
raise ValueError("API ключ не найден.")
|
raise ValueError("API ключ Mistral не найден в конфигурации.")
|
||||||
|
|
||||||
|
|
||||||
|
# Класс для работы с моделями Mistral через OpenAI API
|
||||||
class CustomMistralLLM:
|
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.api_key = api_key
|
||||||
self.endpoint_url = endpoint_url
|
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 = {
|
headers = {
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
payload = {
|
payload = {
|
||||||
"model": "mistral-small-latest",
|
"model": self.model_name,
|
||||||
"messages": [{"role": "user", "content": prompt}],
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
"max_tokens": max_tokens,
|
"max_tokens": max_tokens,
|
||||||
"temperature": temperature
|
"temperature": temperature
|
||||||
}
|
}
|
||||||
|
attempt = 0
|
||||||
|
while attempt < retries:
|
||||||
|
try:
|
||||||
response = requests.post(self.endpoint_url, headers=headers, json=payload)
|
response = requests.post(self.endpoint_url, headers=headers, json=payload)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
result = response.json()
|
result = response.json()
|
||||||
logger.info(f"Полный ответ от модели Mistral: {result}")
|
logger.info(f"Полный ответ от модели {self.model_name}: {result}")
|
||||||
return result.get("choices", [{}])[0].get("message", {}).get("content", "No response")
|
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...")
|
logger.info("Загрузка модели HuggingFaceEmbeddings...")
|
||||||
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
||||||
|
|
||||||
|
# Определяем имя индекса
|
||||||
|
index_name = 'drug_docs'
|
||||||
|
|
||||||
config_file_path = "config.json"
|
# Подключение к Elasticsearch
|
||||||
|
|
||||||
|
|
||||||
with open(config_file_path, 'r') as config_file:
|
|
||||||
config = json.load(config_file)
|
|
||||||
|
|
||||||
# Cloud ID
|
|
||||||
if config.get("useCloud", False):
|
if config.get("useCloud", False):
|
||||||
logger.info("CLOUD ELASTIC")
|
logger.info("CLOUD ELASTIC")
|
||||||
cloud_id = "tt:dXMtZWFzdC0yLmF3cy5lbGFzdGljLWNsb3VkLmNvbTo0NDMkOGM3ODQ0ZWVhZTEyNGY3NmFjNjQyNDFhNjI4NmVhYzMkZTI3YjlkNTQ0ODdhNGViNmEyMTcxMjMxNmJhMWI0ZGU=" # Замените на ваш Cloud ID
|
cloud_id = "tt:dXMtZWFzdC0yLmF3cy5lbGFzdGljLWNsb3VkLmNvbTo0NDMkOGM3ODQ0ZWVhZTEyNGY3NmFjNjQyNDFhNjI4NmVhYzMkZTI3YjlkNTQ0ODdhNGViNmEyMTcxMjMxNmJhMWI0ZGU=" # Замените на ваш Cloud ID
|
||||||
@ -65,44 +86,172 @@ if config.get("useCloud", False):
|
|||||||
es_password="sSz2BEGv56JRNjGFwoQ191RJ",
|
es_password="sSz2BEGv56JRNjGFwoQ191RJ",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.info("LOCAL ELASTIC")
|
logger.info("LOCALlla ELASTIC")
|
||||||
vectorstore = ElasticsearchStore(
|
vectorstore = ElasticsearchStore(
|
||||||
es_url="http://localhost:9200",
|
es_url="http://host.docker.internal:9200",
|
||||||
index_name='drug_docs',
|
index_name=index_name,
|
||||||
embedding=embeddings,
|
embedding=embeddings,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Подключение установлено к {'облачному' if config.get('useCloud', False) else 'локальному'} Elasticsearch")
|
logger.info(f"Подключение установлено к {'облачному' if config.get('useCloud', False) else 'локальному'} Elasticsearch")
|
||||||
|
|
||||||
# LLM
|
# Инициализация моделей
|
||||||
llm = CustomMistralLLM(
|
llm_small = CustomMistralLLM(
|
||||||
api_key=mistral_api_key,
|
api_key=mistral_api_key,
|
||||||
endpoint_url="https://api.mistral.ai/v1/chat/completions"
|
endpoint_url="https://api.mistral.ai/v1/chat/completions",
|
||||||
|
model_name="mistral-small-latest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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):
|
def process_query_with_mistral(query, k=10):
|
||||||
logger.info("Обработка запроса началась.")
|
logger.info("Обработка запроса началась.")
|
||||||
try:
|
try:
|
||||||
# Elasticsearch LangChain
|
# --- ВЕКТОРНЫЙ ПОИСК ---
|
||||||
response = vectorstore.similarity_search(query, k=k)
|
vector_results = vectorstore.similarity_search(query, k=k)
|
||||||
if not response:
|
vector_documents = [hit.metadata.get('text', '') for hit in vector_results]
|
||||||
return {"summary": "Ничего не найдено", "links": [], "status_log": ["Ничего не найдено."]}
|
|
||||||
|
|
||||||
documents = [hit.metadata.get('text', '') for hit in response]
|
# Ограничиваем количество документов и их длину
|
||||||
links = [hit.metadata.get('link', '-') for hit in response]
|
max_docs = 5
|
||||||
structured_prompt = (
|
max_doc_length = 1000
|
||||||
f"Na základe otázky: '{query}' a nasledujúcich informácií o liekoch: {documents}. "
|
vector_documents = [doc[:max_doc_length] for doc in vector_documents[:max_docs]]
|
||||||
"Uveďte tri vhodné lieky alebo riešenia s krátkym vysvetlením pre každý z nich. "
|
|
||||||
"Odpoveď musí byť v slovenčine."
|
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)
|
||||||
|
|
||||||
summary = llm.generate_text(prompt=structured_prompt, max_tokens=512, temperature=0.7)
|
|
||||||
|
|
||||||
#TextSplitter
|
|
||||||
splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
|
splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
|
||||||
split_summary = splitter.split_text(summary)
|
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]]
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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:
|
except Exception as e:
|
||||||
logger.info(f"Ошибка: {str(e)}")
|
logger.error(f"Ошибка: {str(e)}")
|
||||||
return {"summary": "Произошла ошибка", "links": [], "status_log": [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 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
|
from model import process_query_with_mistral
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Создаем Flask приложение
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app)
|
CORS(app) # Разрешаем CORS для всех доменов
|
||||||
|
CORS(app, resources={r"/api/*": {"origins": "http://localhost:5173"}})
|
||||||
|
|
||||||
|
# Маршрут для обработки запросов от фронтенда
|
||||||
@app.route('/api/chat', methods=['POST'])
|
@app.route('/api/chat', methods=['POST'])
|
||||||
def chat():
|
def chat():
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
query = data.get('query', '')
|
query = data.get('query', '')
|
||||||
|
if not query:
|
||||||
|
return jsonify({'error': 'No query provided'}), 400
|
||||||
|
|
||||||
response = process_query_with_mistral(query)
|
response = process_query_with_mistral(query)
|
||||||
|
|
||||||
return jsonify(response)
|
return jsonify(response)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
if __name__ == "__main__":
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
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.
|
# Logs
|
||||||
|
logs
|
||||||
# dependencies
|
*.log
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.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>
|
17962
frontend/package-lock.json
generated
17962
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,54 +1,45 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "coconuts",
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.13.3",
|
"@emotion/react": "^11.13.3",
|
||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
"@mui/material": "^6.1.2",
|
"@gsap/react": "^2.1.1",
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@mui/icons-material": "^6.1.5",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@mui/material": "^6.1.5",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@reduxjs/toolkit": "^2.3.0",
|
||||||
"@types/jest": "^27.5.2",
|
"appwrite": "^16.0.2",
|
||||||
"@types/node": "^16.18.112",
|
"final-form": "^4.20.10",
|
||||||
"@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": "^3.12.5",
|
"gsap": "^3.12.5",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-final-form": "^6.5.9",
|
||||||
"three": "^0.169.0",
|
"react-icons": "^5.3.0",
|
||||||
"typescript": "^4.9.5",
|
"react-redux": "^9.1.2",
|
||||||
"web-vitals": "^2.1.4"
|
"react-router-dom": "^6.27.0"
|
||||||
},
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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 {
|
@tailwind base;
|
||||||
margin: 0;
|
@tailwind components;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
@tailwind utilities;
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
||||||
sans-serif;
|
/* Приховує скроллбар у браузерах на основі WebKit (Chrome, Safari) */
|
||||||
-webkit-font-smoothing: antialiased;
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
-moz-osx-font-smoothing: grayscale;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
/* Приховує скроллбар у Firefox */
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
.no-scrollbar {
|
||||||
monospace;
|
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": {
|
"files": [],
|
||||||
"target": "es5",
|
"references": [
|
||||||
"lib": [
|
{ "path": "./tsconfig.app.json" },
|
||||||
"dom",
|
{ "path": "./tsconfig.node.json" }
|
||||||
"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"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
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