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