translator, upd frontend, upd promts
36
.gitignore
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
elasticsearch/data/
|
||||
|
||||
|
||||
|
||||
.idea/
|
||||
.vs/
|
||||
|
||||
|
||||
*.log
|
||||
*.tmp
|
||||
*.swp
|
||||
|
||||
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
|
||||
|
||||
*.venv/
|
||||
Backend/venv/
|
||||
Backend/__pycache__/
|
||||
|
||||
|
||||
|
||||
|
||||
*.dockerignore
|
||||
*.env
|
||||
docker-compose.override.yml
|
||||
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
|
||||
*.lock
|
||||
package-lock.json
|
||||
yarn.lock
|
8
.idea/.gitignore
vendored
@ -1,8 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
@ -1,6 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.12" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
|
||||
</project>
|
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/AI.iml" filepath="$PROJECT_DIR$/.idea/AI.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
@ -10,5 +10,8 @@ COPY . .
|
||||
# Устанавливаем зависимости из requirements.txt
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
# Запускаем сервер (замените server.py на точку входа вашего бэкенда)
|
||||
CMD ["python", "server.py"]
|
||||
# Делаем скрипт ожидания исполняемым
|
||||
RUN chmod +x wait-for-elasticsearch.sh
|
||||
|
||||
# Запускаем скрипт ожидания перед запуском бэкенда
|
||||
CMD ["./wait-for-elasticsearch.sh", "python", "server.py"]
|
||||
|
80
Backend/index-server-es
Normal file
@ -0,0 +1,80 @@
|
||||
from elasticsearch import Elasticsearch
|
||||
from langchain.embeddings import HuggingFaceEmbeddings
|
||||
from elasticsearch.helpers import bulk
|
||||
import json
|
||||
|
||||
# Настройка подключения к Elasticsearch с аутентификацией и HTTPS
|
||||
es = Elasticsearch(
|
||||
[{'host': 'localhost', 'port': 9200, 'scheme': 'https'}],
|
||||
http_auth=('elastic', '3lvFhvVYrazLsj=M-R_g'), # замените на ваш пароль
|
||||
verify_certs=False # Отключить проверку SSL-сертификата, если используется самоподписанный сертификат
|
||||
)
|
||||
|
||||
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):
|
||||
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)
|
||||
|
||||
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')
|
||||
|
||||
# Опционально: индексируем пакетами по 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 = "../../esDB/cleaned_general_info_additional.json"
|
||||
drug_data = load_drug_data(data_path)
|
||||
index_documents(drug_data)
|
||||
|
180
Backend/model.py
@ -11,23 +11,52 @@ from langchain_huggingface import HuggingFaceEmbeddings
|
||||
from langchain_elasticsearch import ElasticsearchStore
|
||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
from langchain.docstore.document import Document
|
||||
from googletrans import Translator # Translator for final polishing
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Загрузка конфигурации
|
||||
# Load configuration
|
||||
config_file_path = "config.json"
|
||||
|
||||
with open(config_file_path, 'r') as config_file:
|
||||
config = json.load(config_file)
|
||||
|
||||
# Загрузка API ключа Mistral
|
||||
# Load Mistral API key
|
||||
mistral_api_key = "hXDC4RBJk1qy5pOlrgr01GtOlmyCBaNs"
|
||||
if not mistral_api_key:
|
||||
raise ValueError("API ключ Mistral не найден в конфигурации.")
|
||||
raise ValueError("Mistral API key not found in configuration.")
|
||||
|
||||
|
||||
# Класс для работы с моделями Mistral через OpenAI API
|
||||
###############################################################################
|
||||
# Function to translate entire text to Slovak #
|
||||
###############################################################################
|
||||
translator = Translator()
|
||||
|
||||
def translate_to_slovak(text: str) -> str:
|
||||
"""
|
||||
Translates the entire text into Slovak.
|
||||
Logs the text before and after translation.
|
||||
"""
|
||||
if not text.strip():
|
||||
return text
|
||||
|
||||
try:
|
||||
# 1) Slovak (or any language) -> English
|
||||
mid_result = translator.translate(text, src='auto', dest='en').text
|
||||
|
||||
# 2) English -> Slovak
|
||||
final_result = translator.translate(mid_result, src='en', dest='sk').text
|
||||
|
||||
return final_result
|
||||
except Exception as e:
|
||||
logger.error(f"Translation error: {e}")
|
||||
return text # fallback to the original text
|
||||
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Custom Mistral LLM #
|
||||
###############################################################################
|
||||
class CustomMistralLLM:
|
||||
def __init__(self, api_key: str, endpoint_url: str, model_name: str):
|
||||
self.api_key = api_key
|
||||
@ -51,51 +80,55 @@ class CustomMistralLLM:
|
||||
response = requests.post(self.endpoint_url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
logger.info(f"Полный ответ от модели {self.model_name}: {result}")
|
||||
logger.info(f"Full response from model {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} секунд перед повторной попыткой.")
|
||||
logger.warning(f"Rate limit exceeded. Waiting {delay} seconds before retry.")
|
||||
time.sleep(delay)
|
||||
attempt += 1
|
||||
else:
|
||||
logger.error(f"HTTP Error: {e}")
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка: {str(e)}")
|
||||
logger.error(f"Error: {str(e)}")
|
||||
raise e
|
||||
raise Exception("Превышено количество попыток запроса к API")
|
||||
raise Exception("Reached maximum number of retries for API request")
|
||||
|
||||
|
||||
# Инициализация эмбеддингов
|
||||
logger.info("Загрузка модели HuggingFaceEmbeddings...")
|
||||
###############################################################################
|
||||
# Initialize embeddings and Elasticsearch store #
|
||||
###############################################################################
|
||||
logger.info("Loading HuggingFaceEmbeddings model...")
|
||||
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
||||
|
||||
# Определяем имя индекса
|
||||
index_name = 'drug_docs'
|
||||
|
||||
# Подключение к Elasticsearch
|
||||
# Connect to Elasticsearch
|
||||
if config.get("useCloud", False):
|
||||
logger.info("CLOUD ELASTIC")
|
||||
cloud_id = "tt:dXMtZWFzdC0yLmF3cy5lbGFzdGljLWNsb3VkLmNvbTo0NDMkOGM3ODQ0ZWVhZTEyNGY3NmFjNjQyNDFhNjI4NmVhYzMkZTI3YjlkNTQ0ODdhNGViNmEyMTcxMjMxNmJhMWI0ZGU=" # Замените на ваш Cloud ID
|
||||
logger.info("Using cloud Elasticsearch.")
|
||||
cloud_id = "tt:dXMtZWFzdC0yLmF3cy5lbGFzdGljLWNsb3VkLmNvbTo0NDMkOGM3ODQ0ZWVhZTEyNGY3NmFjNjQyNDFhNjI4NmVhYzMkZTI3YjlkNTQ0ODdhNGViNmEyMTcxMjMxNmJhMWI0ZGU="
|
||||
vectorstore = ElasticsearchStore(
|
||||
es_cloud_id=cloud_id,
|
||||
index_name='drug_docs',
|
||||
embedding=embeddings,
|
||||
es_user="elastic",
|
||||
es_password="sSz2BEGv56JRNjGFwoQ191RJ",
|
||||
es_password="sSz2BEGv56JRNjGFwoQ191RJ"
|
||||
)
|
||||
else:
|
||||
logger.info("LOCALlla ELASTIC")
|
||||
logger.info("Using local Elasticsearch.")
|
||||
vectorstore = ElasticsearchStore(
|
||||
es_url="http://host.docker.internal:9200",
|
||||
es_url="http://localhost:9200",
|
||||
index_name=index_name,
|
||||
embedding=embeddings,
|
||||
)
|
||||
|
||||
logger.info(f"Подключение установлено к {'облачному' if config.get('useCloud', False) else 'локальному'} Elasticsearch")
|
||||
logger.info(f"Connected to {'cloud' if config.get('useCloud', False) else 'local'} Elasticsearch.")
|
||||
|
||||
# Инициализация моделей
|
||||
|
||||
###############################################################################
|
||||
# Initialize Mistral models (small & large) #
|
||||
###############################################################################
|
||||
llm_small = CustomMistralLLM(
|
||||
api_key=mistral_api_key,
|
||||
endpoint_url="https://api.mistral.ai/v1/chat/completions",
|
||||
@ -109,85 +142,66 @@ llm_large = CustomMistralLLM(
|
||||
)
|
||||
|
||||
|
||||
# Функция для оценки релевантности результатов
|
||||
###############################################################################
|
||||
# Helper function to evaluate model output #
|
||||
###############################################################################
|
||||
def evaluate_results(query, summaries, model_name):
|
||||
"""
|
||||
Оценивает результаты на основе длины текста, наличия ключевых слов из запроса
|
||||
и других подходящих критериев. Используется для определения качества вывода от модели.
|
||||
Evaluates results by:
|
||||
- text length,
|
||||
- presence of query keywords, etc.
|
||||
Returns a rating and explanation.
|
||||
"""
|
||||
query_keywords = query.split() # Получаем ключевые слова из запроса
|
||||
query_keywords = query.split()
|
||||
total_score = 0
|
||||
explanation = []
|
||||
|
||||
for i, summary in enumerate(summaries):
|
||||
# Оценка по длине ответа
|
||||
# Length-based scoring
|
||||
length_score = min(len(summary) / 100, 10)
|
||||
total_score += length_score
|
||||
explanation.append(f"Document {i+1}: Length score - {length_score}")
|
||||
|
||||
# Оценка по количеству совпадений ключевых слов
|
||||
# Keyword-based scoring
|
||||
keyword_matches = sum(1 for word in query_keywords if word.lower() in summary.lower())
|
||||
keyword_score = min(keyword_matches * 2, 10) # Максимальная оценка за ключевые слова - 10
|
||||
keyword_score = min(keyword_matches * 2, 10)
|
||||
total_score += keyword_score
|
||||
explanation.append(f"Document {i+1}: Keyword match score - {keyword_score}")
|
||||
|
||||
# Средняя оценка по количеству документов
|
||||
final_score = total_score / len(summaries) if summaries else 0
|
||||
explanation_summary = "\n".join(explanation)
|
||||
|
||||
logger.info(f"Оценка для модели {model_name}: {final_score}/10")
|
||||
logger.info(f"Пояснение оценки:\n{explanation_summary}")
|
||||
logger.info(f"Evaluation for model {model_name}: {final_score}/10")
|
||||
logger.info(f"Explanation:\n{explanation_summary}")
|
||||
|
||||
return {"rating": round(final_score, 2), "explanation": explanation_summary}
|
||||
|
||||
|
||||
|
||||
# Функция для сравнения результатов двух моделей
|
||||
# Функция для сравнения результатов двух моделей
|
||||
# Функция для сравнения результатов двух моделей
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
# Функция для обработки запроса
|
||||
# Функция для обработки запроса
|
||||
# Функция для обработки запроса
|
||||
###############################################################################
|
||||
# Main function: process_query_with_mistral (Slovak prompt) #
|
||||
###############################################################################
|
||||
def process_query_with_mistral(query, k=10):
|
||||
logger.info("Обработка запроса началась.")
|
||||
logger.info("Processing query started.")
|
||||
try:
|
||||
# --- ВЕКТОРНЫЙ ПОИСК ---
|
||||
# --- Vector search ---
|
||||
vector_results = vectorstore.similarity_search(query, k=k)
|
||||
vector_documents = [hit.metadata.get('text', '') for hit in vector_results]
|
||||
|
||||
# Ограничиваем количество документов и их длину
|
||||
max_docs = 5
|
||||
max_doc_length = 1000
|
||||
vector_documents = [doc[:max_doc_length] for doc in vector_documents[:max_docs]]
|
||||
|
||||
if vector_documents:
|
||||
# Slovak prompt
|
||||
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."
|
||||
f"Otázka: '{query}'.\n"
|
||||
"Na základe nasledujúcich informácií o liekoch:\n"
|
||||
f"{vector_documents}\n\n"
|
||||
"Prosím, uveďte tri najvhodnejšie lieky alebo riešenia. Pre každý liek uveďte jeho názov a stručné, jasné vysvetlenie, prečo je vhodný. "
|
||||
"Odpovedajte priamo a ľudským, priateľským tónom v číslovanom zozname, bez nepotrebných úvodných fráz alebo opisu procesu. "
|
||||
"Odpoveď musí byť v slovenčine."
|
||||
)
|
||||
|
||||
summary_small_vector = llm_small.generate_text(prompt=vector_prompt, max_tokens=700, temperature=0.7)
|
||||
summary_large_vector = llm_large.generate_text(prompt=vector_prompt, max_tokens=700, temperature=0.7)
|
||||
|
||||
@ -195,14 +209,15 @@ def process_query_with_mistral(query, k=10):
|
||||
split_summary_small_vector = splitter.split_text(summary_small_vector)
|
||||
split_summary_large_vector = splitter.split_text(summary_large_vector)
|
||||
|
||||
# Оценка векторных результатов
|
||||
small_vector_eval = evaluate_results(query, split_summary_small_vector, 'Mistral Small')
|
||||
large_vector_eval = evaluate_results(query, split_summary_large_vector, 'Mistral Large')
|
||||
else:
|
||||
small_vector_eval = {"rating": 0, "explanation": "No results"}
|
||||
large_vector_eval = {"rating": 0, "explanation": "No results"}
|
||||
summary_small_vector = ""
|
||||
summary_large_vector = ""
|
||||
|
||||
# --- ТЕКСТОВЫЙ ПОИСК ---
|
||||
# --- Text search ---
|
||||
es_results = vectorstore.client.search(
|
||||
index=index_name,
|
||||
body={"size": k, "query": {"match": {"text": query}}}
|
||||
@ -211,25 +226,31 @@ def process_query_with_mistral(query, k=10):
|
||||
text_documents = [doc[:max_doc_length] for doc in text_documents[:max_docs]]
|
||||
|
||||
if text_documents:
|
||||
# Slovak prompt
|
||||
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."
|
||||
f"Otázka: '{query}'.\n"
|
||||
"Na základe nasledujúcich informácií o liekoch:\n"
|
||||
f"{text_documents}\n\n"
|
||||
"Prosím, uveďte tri najvhodnejšie lieky alebo riešenia. Pre každý liek uveďte jeho názov a stručné, jasné vysvetlenie, prečo je vhodný. "
|
||||
"Odpovedajte priamo a ľudským, priateľským tónom v číslovanom zozname, bez nepotrebných úvodných fráz alebo opisu procesu. "
|
||||
"Odpoveď musí byť v slovenčine."
|
||||
)
|
||||
|
||||
summary_small_text = llm_small.generate_text(prompt=text_prompt, max_tokens=700, temperature=0.7)
|
||||
summary_large_text = llm_large.generate_text(prompt=text_prompt, max_tokens=700, temperature=0.7)
|
||||
|
||||
split_summary_small_text = splitter.split_text(summary_small_text)
|
||||
split_summary_large_text = splitter.split_text(summary_large_text)
|
||||
split_summary_small_text = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20).split_text(summary_small_text)
|
||||
split_summary_large_text = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20).split_text(summary_large_text)
|
||||
|
||||
# Оценка текстовых результатов
|
||||
small_text_eval = evaluate_results(query, split_summary_small_text, 'Mistral Small')
|
||||
large_text_eval = evaluate_results(query, split_summary_large_text, 'Mistral Large')
|
||||
else:
|
||||
small_text_eval = {"rating": 0, "explanation": "No results"}
|
||||
large_text_eval = {"rating": 0, "explanation": "No results"}
|
||||
summary_small_text = ""
|
||||
summary_large_text = ""
|
||||
|
||||
# Выбираем лучший результат среди всех
|
||||
# Combine all results and pick the best
|
||||
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"},
|
||||
@ -238,20 +259,21 @@ def process_query_with_mistral(query, k=10):
|
||||
]
|
||||
|
||||
best_result = max(all_results, key=lambda x: x["eval"]["rating"])
|
||||
logger.info(f"Best result from model {best_result['model']} with score {best_result['eval']['rating']}.")
|
||||
|
||||
logger.info(f"Лучший результат от модели {best_result['model']} с оценкой {best_result['eval']['rating']}.")
|
||||
# Final translation to Slovak (with logs before/after)
|
||||
polished_answer = translate_to_slovak(best_result["summary"])
|
||||
|
||||
# Возвращаем только лучший ответ
|
||||
return {
|
||||
"best_answer": best_result["summary"],
|
||||
"best_answer": polished_answer,
|
||||
"model": best_result["model"],
|
||||
"rating": best_result["eval"]["rating"],
|
||||
"explanation": best_result["eval"]["explanation"]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка: {str(e)}")
|
||||
logger.error(f"Error: {str(e)}")
|
||||
return {
|
||||
"best_answer": "Произошла ошибка при обработке запроса.",
|
||||
"best_answer": "An error occurred during query processing.",
|
||||
"error": str(e)
|
||||
}
|
||||
|
13
Backend/wait-for-elasticsearch.sh
Normal file
@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Ожидание готовности Elasticsearch..."
|
||||
|
||||
# Проверяем доступность Elasticsearch
|
||||
while ! curl -s http://elasticsearch:9200 > /dev/null; do
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo "Elasticsearch готов. Запуск бэкенда..."
|
||||
|
||||
# Запускаем бэкенд
|
||||
exec "$@"
|
@ -1,14 +1,50 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
elasticsearch:
|
||||
build:
|
||||
context: ./elasticsearch
|
||||
dockerfile: Dockerfile
|
||||
container_name: elasticsearch
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- xpack.security.enabled=false
|
||||
ports:
|
||||
- "9200:9200"
|
||||
- "9300:9300"
|
||||
networks:
|
||||
- app-network
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-f", "http://localhost:9200/_cluster/health" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 40s
|
||||
|
||||
backend:
|
||||
container_name: backend_container
|
||||
build:
|
||||
context: ./Backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- ELASTICSEARCH_HOST=http://host.docker.internal:9200
|
||||
- ELASTICSEARCH_HOST=http://elasticsearch:9200
|
||||
depends_on:
|
||||
elasticsearch:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
frontend:
|
||||
container_name: frontend_container
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
|
15
elasticsearch/Dockerfile
Normal file
@ -0,0 +1,15 @@
|
||||
FROM docker.elastic.co/elasticsearch/elasticsearch:8.14.3
|
||||
|
||||
# Отключаем безопасность для упрощения доступа
|
||||
ENV discovery.type=single-node
|
||||
ENV xpack.security.enabled=false
|
||||
|
||||
# Копируем проиндексированные данные в директорию данных Elasticsearch
|
||||
COPY --chown=elasticsearch:elasticsearch data/ /usr/share/elasticsearch/data
|
||||
|
||||
# Устанавливаем права доступа
|
||||
RUN chmod -R 0775 /usr/share/elasticsearch/data
|
||||
|
||||
# Удаляем файлы блокировок (добавьте эти команды)
|
||||
RUN find /usr/share/elasticsearch/data -type f -name "*.lock" -delete
|
||||
RUN rm -f /usr/share/elasticsearch/data/nodes/0/node.lock
|
5
frontend/.idea/.gitignore
vendored
@ -1,5 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
@ -1,6 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/frontend.iml" filepath="$PROJECT_DIR$/.idea/frontend.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
@ -1,17 +1,26 @@
|
||||
# Используем базовый образ Node.js
|
||||
FROM node:18
|
||||
FROM node:18-alpine
|
||||
|
||||
# Устанавливаем рабочую директорию
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем файлы проекта
|
||||
COPY . .
|
||||
# Копируем package.json и package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# Устанавливаем зависимости
|
||||
RUN npm install
|
||||
|
||||
# Собираем проект
|
||||
RUN npm run dev
|
||||
# Копируем файлы проекта
|
||||
COPY . .
|
||||
|
||||
# Запускаем сервер
|
||||
CMD ["npm", "run", "dev"]
|
||||
# Сборка приложения
|
||||
RUN npm run build
|
||||
|
||||
# Устанавливаем сервер для обслуживания статических файлов
|
||||
RUN npm install -g serve
|
||||
|
||||
# Открываем порт
|
||||
EXPOSE 3000
|
||||
|
||||
# Запуск фронтенда
|
||||
CMD ["serve", "-s", "dist", "-l", "3000"]
|
||||
|
14
frontend/declarations.d.ts
vendored
@ -1,14 +0,0 @@
|
||||
declare module "*.png" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module "*.jpg" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module "*.gif" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
23
frontend/my-app/.gitignore
vendored
@ -1,23 +0,0 @@
|
||||
# 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
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
@ -1,46 +0,0 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
17960
frontend/my-app/package-lock.json
generated
@ -1,43 +0,0 @@
|
||||
{
|
||||
"name": "my-app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@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.113",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 3.8 KiB |
@ -1,43 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 9.4 KiB |
@ -1,25 +0,0 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
@ -1,38 +0,0 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
@ -1,13 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
Before Width: | Height: | Size: 2.6 KiB |
1
frontend/my-app/src/react-app-env.d.ts
vendored
@ -1 +0,0 @@
|
||||
/// <reference types="react-scripts" />
|
@ -1,15 +0,0 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
@ -1,5 +0,0 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
@ -1,26 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
23
frontend/package-lock.json
generated
@ -11,6 +11,7 @@
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@gsap/react": "^2.1.1",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@mui/icons-material": "^6.1.5",
|
||||
"@mui/material": "^6.1.5",
|
||||
"@reduxjs/toolkit": "^2.3.0",
|
||||
@ -1157,6 +1158,28 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@material-ui/icons": {
|
||||
"version": "4.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.3.tgz",
|
||||
"integrity": "sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.4.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@material-ui/core": "^4.0.0",
|
||||
"@types/react": "^16.8.6 || ^17.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/core-downloads-tracker": {
|
||||
"version": "6.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.5.tgz",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@gsap/react": "^2.1.1",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@mui/icons-material": "^6.1.5",
|
||||
"@mui/material": "^6.1.5",
|
||||
"@reduxjs/toolkit": "^2.3.0",
|
||||
|
Before Width: | Height: | Size: 3.8 KiB |
@ -1,3 +0,0 @@
|
||||
{
|
||||
"isChatEnabled": true
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 9.4 KiB |
@ -1,25 +0,0 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
@ -1,5 +1,5 @@
|
||||
import { BrowserRouter as Router, Route, Routes, Outlet } from 'react-router-dom';
|
||||
import Navigation from './components/Navigation';
|
||||
import Navigation from './Components/Navigation';
|
||||
import HomePage from './pages/HomePage';
|
||||
import LandingPage from './pages/LandingPage';
|
||||
|
||||
|
@ -1,109 +0,0 @@
|
||||
.chat-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.chat-sidebar {
|
||||
width: 15%;
|
||||
padding: 20px;
|
||||
background-color: #0077b6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-sidebar-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-sidebar-title {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.new-chat-img {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-divider {
|
||||
margin: 20px 0;
|
||||
border: none;
|
||||
height: 1px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.chat-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.chat-list-item:hover {
|
||||
background-color: #005f8f;
|
||||
}
|
||||
|
||||
.chat-main {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #e0f7fa;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
max-width: 99%;
|
||||
flex-grow: 1;
|
||||
padding: 20px;
|
||||
background-color: #e0f7fa;
|
||||
overflow-y: auto;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
.chat-messages::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb {
|
||||
background-color: #0077b6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 50px 0;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
width: 50%;
|
||||
height: 40px;
|
||||
padding: 10px;
|
||||
border: 1px solid white;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.chat-send-button {
|
||||
padding: 10px 20px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.send-img {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
}
|
||||
.send-img:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
// @ts-ignore
|
||||
import React, { useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { motion } from 'framer-motion';
|
||||
import Message from '../Message/Message';
|
||||
import './Chat.css';
|
||||
import img from "../Images/send-message.png";
|
||||
import newChatImg from "../Images/new-message.png";
|
||||
import ChatItem from "../ChatItem/ChatItem";
|
||||
|
||||
interface Message {
|
||||
content: string;
|
||||
role: 'user' | 'assistant';
|
||||
}
|
||||
interface ChatItemProps {
|
||||
index: number;
|
||||
}
|
||||
|
||||
const Chat: React.FC = () => {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
const handleSend = async (messageContent: string) => {
|
||||
const newMessage: Message = { content: messageContent, role: 'user' };
|
||||
setMessages([...messages, newMessage]);
|
||||
|
||||
try {
|
||||
const response = await axios.post('http://localhost:5000/api/chat', {
|
||||
query: messageContent,
|
||||
});
|
||||
|
||||
const assistantMessage: Message = {
|
||||
content: response.data.summary,
|
||||
role: 'assistant',
|
||||
};
|
||||
setMessages((prevMessages) => [...prevMessages, assistantMessage]);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при отправке запроса:', error);
|
||||
}
|
||||
};
|
||||
const fullfillChatItems = () => {
|
||||
let countOfChats:number = 10;
|
||||
const chatItems: JSX.Element[] = [];
|
||||
for (let i = 1; i < countOfChats; i++){
|
||||
chatItems.push(<ChatItem index={i} key={i} onMouseDownEvent={handleMouseDown} onMouseMoveEvent={handleMouseMove} onMouseUpEvent={handleMouseUp} />)
|
||||
}
|
||||
return chatItems;
|
||||
}
|
||||
const addEventListenerChatItem = (elements: HTMLElement[]) => {
|
||||
elements.forEach(item => {
|
||||
item.addEventListener('mousedown', () => {
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
const handleMouseDown = () => {
|
||||
console.log('mouseDown');
|
||||
}
|
||||
const handleMouseMove = () => {
|
||||
console.log('mouseMove');
|
||||
}
|
||||
const handleMouseUp = () => {
|
||||
console.log('mouseUp');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chat-container">
|
||||
<div className="chat-sidebar">
|
||||
<div className="chat-sidebar-header">
|
||||
<h3 className="chat-sidebar-title">Your Chats</h3>
|
||||
<img src={newChatImg} alt="New Chat" className="new-chat-img" />
|
||||
</div>
|
||||
<hr className="chat-divider" />
|
||||
<ul className="chat-list">
|
||||
{fullfillChatItems()}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="chat-main">
|
||||
<div className="chat-messages">
|
||||
{messages.map((message, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Message content={message.content} role={message.role} />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Поле ввода сообщения */}
|
||||
<form
|
||||
className="chat-input-container"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSend(inputValue);
|
||||
setInputValue('');
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
className="chat-input"
|
||||
placeholder="Write your message"
|
||||
/>
|
||||
|
||||
<button type="submit" className="chat-send-button">
|
||||
<img src={img} alt="Send" className="send-img" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chat;
|
@ -1,6 +0,0 @@
|
||||
.chat-list-item {
|
||||
padding: 10px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import './ChatItem.css';
|
||||
|
||||
interface ChatItemProps {
|
||||
index: number;
|
||||
onMouseDownEvent: (index: number, e: React.MouseEvent) => void;
|
||||
onMouseUpEvent: (index: number, e: React.MouseEvent) => void;
|
||||
onMouseMoveEvent: (index: number, e: React.MouseEvent) => void;
|
||||
}
|
||||
const ChatItem: React.FC<ChatItemProps> = ({index, onMouseDownEvent, onMouseMoveEvent, onMouseUpEvent}) => {
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div className={"chat-list-item"}
|
||||
onMouseDown={(event) => onMouseDownEvent(index, event)}
|
||||
onMouseMove={(event) => onMouseMoveEvent(index, event)}
|
||||
onMouseUp={(event ) => onMouseUpEvent(index, event)}
|
||||
>
|
||||
Chat {index}
|
||||
</div>
|
||||
|
||||
|
||||
);
|
||||
};
|
||||
export default ChatItem;
|
@ -13,9 +13,9 @@ const MuscleDiagram: React.FC = () => {
|
||||
setHighlightedMuscle(null);
|
||||
};
|
||||
|
||||
const handleMuscleClick = (muscle: Muscle) => {
|
||||
|
||||
}
|
||||
// const handleMuscleClick = (muscle: Muscle) => {
|
||||
//
|
||||
// }
|
||||
|
||||
const getDarkerColor = (baseColor: string) => {
|
||||
const colorValue = parseInt(baseColor.slice(1), 16);
|
||||
|
@ -1,39 +0,0 @@
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 50px;
|
||||
background-color: #0077b6;
|
||||
color: white;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.logo h2 {
|
||||
margin: 0;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.nav-header ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nav-header ul li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nav-header ul li a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 1.1rem;
|
||||
padding: 10px 20px;
|
||||
border-radius: 25px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-header ul li a:hover {
|
||||
background-color: #90e0ef;
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { gsap } from "gsap";
|
||||
import './Header.css';
|
||||
|
||||
export default function Header() {
|
||||
const headerRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (headerRef.current) {
|
||||
gsap.fromTo(headerRef.current, { opacity: 0, y: -50 }, { opacity: 1, y: 0, duration: 1, ease: "power4.out" });
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header ref={headerRef} className="header">
|
||||
<div className="logo">
|
||||
<h2>Medical AI Assistant</h2>
|
||||
</div>
|
||||
<nav className="nav-header">
|
||||
<ul>
|
||||
<li><Link to="/login">Log In</Link></li>
|
||||
<li><Link to="/signup">Sign Up</Link></li>
|
||||
<li><Link to="/about">About Us</Link></li>
|
||||
<li><Link to="/services">Services</Link></li>
|
||||
<li><Link to="/contact">Contact</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
Before Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 2.9 MiB |
Before Width: | Height: | Size: 145 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 15 KiB |
@ -1,31 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface InputFieldProps {
|
||||
onSend: (message: string) => void;
|
||||
}
|
||||
|
||||
const InputField: React.FC<InputFieldProps> = ({ onSend }) => {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
const handleSend = () => {
|
||||
if (inputValue.trim() !== '') {
|
||||
onSend(inputValue);
|
||||
setInputValue('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="input-field">
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
placeholder="Введите сообщение..."
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
|
||||
/>
|
||||
<button onClick={handleSend}>Отправить</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputField;
|
@ -1,82 +0,0 @@
|
||||
header {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-top: 0;
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
background-color: #e0f7fa;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.wave-text-container {
|
||||
font-size: 3rem;
|
||||
font-weight: bold;
|
||||
color: #0077b6;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wave-text {
|
||||
display: inline-block;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
font-size: 1.5rem;
|
||||
color: #005f73;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.try-chat-container {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.smiley-container {
|
||||
position: relative;
|
||||
margin-top: 5%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: yellow;
|
||||
border-radius: 50%;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.smiley {
|
||||
width: 90%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.eye {
|
||||
position: absolute;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-color: black;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
|
||||
.left-eye {
|
||||
top: 40%;
|
||||
left: 35%;
|
||||
}
|
||||
|
||||
.right-eye {
|
||||
top: 40%;
|
||||
right: 35%;
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
// @ts-ignore
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { gsap } from "gsap";
|
||||
import Header from "../Header/Header";
|
||||
// @ts-ignore
|
||||
import smiley from '../Images/clipart1366776.png';
|
||||
import { Snackbar, Button } from '@mui/material';
|
||||
import './MainPage.css';
|
||||
|
||||
export default function MainPage() {
|
||||
const titleRef = useRef<HTMLHeadingElement | null>(null);
|
||||
const textRef = useRef<HTMLParagraphElement | null>(null);
|
||||
const buttonRef = useRef<HTMLDivElement | null>(null);
|
||||
const leftEyeRef = useRef<HTMLDivElement | null>(null);
|
||||
const rightEyeRef = useRef<HTMLDivElement | null>(null);
|
||||
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
|
||||
const [openSnackbar, setOpenSnackbar] = useState(false);
|
||||
const [chatEnabled, setChatEnabled] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await fetch("/featureFlags.json");
|
||||
const data = await response.json();
|
||||
setChatEnabled(data.isChatEnabled);
|
||||
} catch (error) {
|
||||
console.error("Error featureFlags.json:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
if (titleRef.current) {
|
||||
gsap.fromTo(titleRef.current, { opacity: 0, y: -50 }, { opacity: 1, y: 0, duration: 1.5, ease: "power4.out" });
|
||||
}
|
||||
|
||||
if (textRef.current) {
|
||||
gsap.fromTo(textRef.current, { opacity: 0, y: 50 }, { opacity: 1, y: 0, duration: 2, delay: 0.5, ease: "power4.out" });
|
||||
}
|
||||
|
||||
if (buttonRef.current) {
|
||||
gsap.fromTo(buttonRef.current, { opacity: 0, y: 50 }, { opacity: 1, y: 0, duration: 2, delay: 0.8, ease: "power4.out" });
|
||||
}
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
setMousePos({ x: event.clientX, y: event.clientY });
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const calculateEyeMovement = (eyeRef: React.RefObject<HTMLDivElement>, offsetX: number, offsetY: number) => {
|
||||
if (eyeRef.current) {
|
||||
const eye = eyeRef.current;
|
||||
const eyeX = eye.getBoundingClientRect().left + eye.offsetWidth / 2;
|
||||
const eyeY = eye.getBoundingClientRect().top + eye.offsetHeight / 2;
|
||||
|
||||
const angle = Math.atan2(mousePos.y - eyeY, mousePos.x - eyeX);
|
||||
const distance = 10;
|
||||
|
||||
const moveX = Math.cos(angle) * distance;
|
||||
const moveY = Math.sin(angle) * distance;
|
||||
|
||||
eye.style.transform = `translate(${moveX + offsetX}px, ${moveY + offsetY}px)`;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
calculateEyeMovement(leftEyeRef, -10, 0);
|
||||
calculateEyeMovement(rightEyeRef, 10, 0);
|
||||
}, [mousePos]);
|
||||
|
||||
const handleTryChatClick = () => {
|
||||
chatEnabled ? navigate('/Chat'): setOpenSnackbar(true);
|
||||
};
|
||||
|
||||
const handleCloseSnackbar = (event?: React.SyntheticEvent | Event, reason?: string) => {
|
||||
if (reason === 'clickaway') {
|
||||
return;
|
||||
}
|
||||
setOpenSnackbar(false);
|
||||
};
|
||||
|
||||
const waveText = "Welcome to the Medical AI Assistant!".split("").map((char, index) => (
|
||||
<span key={index} className="wave-text" style={{ animationDelay: `${index * 0.1}s` }}>
|
||||
{char === " " ? "\u00A0" : char}
|
||||
</span>
|
||||
));
|
||||
|
||||
useEffect(() => {
|
||||
const startWaveAnimation = () => {
|
||||
const letters = document.querySelectorAll('.wave-text');
|
||||
letters.forEach((letter, index) => {
|
||||
(letter as HTMLElement).style.animation = 'none';
|
||||
|
||||
// Перезапускаем анимацию
|
||||
setTimeout(() => {
|
||||
(letter as HTMLElement).style.animation = `wave 1s ease-in-out ${index * 0.1}s`;
|
||||
}, 10);
|
||||
});
|
||||
};
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
startWaveAnimation();
|
||||
}, 10000);
|
||||
|
||||
startWaveAnimation();
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<div className="main-content">
|
||||
<h1 ref={titleRef} className="wave-text-container">{waveText}</h1>
|
||||
<p ref={textRef} className="intro-text">
|
||||
Your personal assistant in the world of healthcare and pharmaceuticals. We are here to help you find reliable information about medicines, healthcare, and more.
|
||||
</p>
|
||||
<div className="try-chat-container" ref={buttonRef}>
|
||||
<Button variant="contained" color="primary" onClick={handleTryChatClick}>
|
||||
TRY CHAT
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="smiley-container">
|
||||
<img src={smiley} alt="Smiley" className="smiley" />
|
||||
<div className="eye left-eye" ref={leftEyeRef}></div>
|
||||
<div className="eye right-eye" ref={rightEyeRef}></div>
|
||||
</div>
|
||||
|
||||
<Snackbar
|
||||
open={openSnackbar}
|
||||
autoHideDuration={3000}
|
||||
onClose={handleCloseSnackbar}
|
||||
message="Chat is not available right now"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
interface MessageProps {
|
||||
content: string;
|
||||
role: 'user' | 'assistant';
|
||||
}
|
||||
|
||||
const Message: React.FC<MessageProps> = ({ content, role }) => {
|
||||
return (
|
||||
<div className={`message ${role}`}>
|
||||
<p>{content}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Message;
|
@ -1,15 +0,0 @@
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import Chat from "../Chat/Chat";
|
||||
import MainPage from "../MainPage/MainPage";
|
||||
|
||||
export default function Router(){
|
||||
return(
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<MainPage />} />
|
||||
<Route path="Chat" element={<Chat></Chat>}></Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import Router from '../src/Router/Router';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Router />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
reportWebVitals();
|
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
Before Width: | Height: | Size: 2.6 KiB |
@ -1,6 +1,6 @@
|
||||
import { CgGym } from "react-icons/cg";
|
||||
import { FaBed } from "react-icons/fa6";
|
||||
import { MdFastfood } from "react-icons/md";
|
||||
|
||||
import { MdLocalPharmacy, MdSelfImprovement } from "react-icons/md";
|
||||
import { GiPill } from "react-icons/gi";
|
||||
import { useState } from "react";
|
||||
import gsap from "gsap";
|
||||
import { useGSAP } from "@gsap/react";
|
||||
@ -10,7 +10,7 @@ import { useLazySendChatQuestionQuery } from "../store/api/chatApi";
|
||||
const HomePage = () => {
|
||||
const [sendChatQuestion, { isLoading, isFetching }] = useLazySendChatQuestionQuery();
|
||||
|
||||
type Category = 'sport' | 'feed' | 'sleep';
|
||||
type Category = 'medication' | 'supplements' | 'lifestyle';
|
||||
const [category, setCategory] = useState<Category | null>(null);
|
||||
const [message, setMessage] = useState<string>('');
|
||||
const [chatHistory, setChatHistory] = useState<{ sender: string; text: string, rating?: number, explanation?: string }[]>([]);
|
||||
@ -26,14 +26,11 @@ const HomePage = () => {
|
||||
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}`,
|
||||
@ -90,24 +87,31 @@ const HomePage = () => {
|
||||
)}
|
||||
</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="buttons">*/}
|
||||
{/* <div className="flex gap-6">*/}
|
||||
{/* <button onClick={() => setCategory('medication')}*/}
|
||||
{/* className={`flex items-center shadow-lg justify-center gap-2 ${category === 'medication' ? 'bg-bright-blue' : 'bg-gray-300'} text-white rounded-md font-medium hover:opacity-90 duration-200 h-12 w-40`}>*/}
|
||||
{/* Medications <MdLocalPharmacy size={30}/>*/}
|
||||
{/* </button>*/}
|
||||
{/* <button onClick={() => setCategory('supplements')}*/}
|
||||
{/* className={`flex items-center shadow-lg justify-center gap-2 ${category === 'supplements' ? 'bg-bright-blue' : 'bg-gray-300'} text-white rounded-md font-medium hover:opacity-90 duration-200 h-12 w-40`}>*/}
|
||||
{/* Supplements <GiPill size={25}/>*/}
|
||||
{/* </button>*/}
|
||||
{/* <button onClick={() => setCategory('lifestyle')}*/}
|
||||
{/* className={`flex items-center shadow-lg justify-center gap-2 ${category === 'lifestyle' ? 'bg-bright-blue' : 'bg-gray-300'} text-white rounded-md font-medium hover:opacity-90 duration-200 h-12 w-40`}>*/}
|
||||
{/* Lifestyle <MdSelfImprovement size={25}/>*/}
|
||||
{/* </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>
|
||||
<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>
|
||||
|
@ -40,7 +40,6 @@ const Navbar: React.FC = () => {
|
||||
</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>
|
||||
@ -52,70 +51,78 @@ const Navbar: React.FC = () => {
|
||||
};
|
||||
|
||||
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 })
|
||||
gsap.to('#button-wrapper', { opacity: 1, ease: 'power2.inOut', delay: 2.5, duration: 0.5 });
|
||||
}, [])
|
||||
|
||||
return (<>
|
||||
<div className="h-screen flex flex-col items-center justify-center bg-gradient-to-b text-gray-800 p-4">
|
||||
{/* Навігація */}
|
||||
<Navbar />
|
||||
return (
|
||||
<div style={{backgroundColor: '#d0e7ff'}} className="min-h-screen">
|
||||
<div className="h-screen flex flex-col items-center justify-center bg-gradient-to-b text-gray-800 p-4">
|
||||
<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>
|
||||
<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>
|
||||
<p id='secondheading'
|
||||
className="text-base sm:text-lg md:text-xl text-center max-w-2xl mb-8 text-gray-700">
|
||||
A solution for personalized health support, including tailored medication guidance and wellness
|
||||
insights. Take care of yourself with the power of modern technology.
|
||||
</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 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 Prescription</h3>
|
||||
<p className="text-gray-600">Receive tailored medication recommendations specifically
|
||||
designed for your needs.</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 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 id='arrow' className='flex flex-col items-center mt-10 z-0'>
|
||||
<p className='text-gray-600'>Try it out</p>
|
||||
<BouncingArrow/>
|
||||
</div>
|
||||
|
||||
<div id="button-wrapper" className="flex justify-center opacity-0 mt-6">
|
||||
<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 shadow-md">
|
||||
Get started
|
||||
</button>
|
||||
</Link>
|
||||
</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>
|
||||
</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;
|
||||
|
||||
|
||||
|
@ -1,15 +0,0 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
@ -1,5 +0,0 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
@ -1,50 +0,0 @@
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.messages {
|
||||
flex-grow: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
text-align: right;
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.message.assistant {
|
||||
text-align: left;
|
||||
color: green;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
input {
|
||||
flex-grow: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 10px;
|
||||
padding: 10px 20px;
|
||||
background-color: blue;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|