chat history, modified database on aws, navigation modified, clearify promts, temporary closing translator, add verification of answer depending on user question

This commit is contained in:
oleh 2025-02-12 14:40:28 +01:00
parent 771a6f8432
commit 677ae05159
10 changed files with 791 additions and 254 deletions

View File

@ -3,6 +3,7 @@ import requests
import logging import logging
import time import time
import re import re
import difflib
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
from langchain.chains import SequentialChain from langchain.chains import SequentialChain
@ -11,48 +12,80 @@ from langchain_huggingface import HuggingFaceEmbeddings
from langchain_elasticsearch import ElasticsearchStore from langchain_elasticsearch import ElasticsearchStore
from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.docstore.document import Document from langchain.docstore.document import Document
from googletrans import Translator # Translator for final polishing # from googletrans import Translator
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Load configuration
config_file_path = "config.json" config_file_path = "config.json"
with open(config_file_path, 'r') as config_file: with open(config_file_path, 'r') as config_file:
config = json.load(config_file) config = json.load(config_file)
# Load Mistral API key
mistral_api_key = "hXDC4RBJk1qy5pOlrgr01GtOlmyCBaNs" mistral_api_key = "hXDC4RBJk1qy5pOlrgr01GtOlmyCBaNs"
if not mistral_api_key: if not mistral_api_key:
raise ValueError("Mistral API key not found in configuration.") raise ValueError("Mistral API key not found in configuration.")
############################################################################### ###############################################################################
# Function to translate entire text to Slovak # # translate all answer to slovak(temporary closed :) ) #
############################################################################### ###############################################################################
translator = Translator() # translator = Translator()
def translate_to_slovak(text: str) -> str: def translate_to_slovak(text: str) -> str:
""" """
Translates the entire text into Slovak. Переводит весь текст на словацкий с логированием изменений.
Logs the text before and after translation. Сейчас функция является заглушкой и возвращает исходный текст без изменений.
""" """
if not text.strip(): # if not text.strip():
# return text
#
# logger.info("Translation - Before: " + text)
# try:
# mid_result = translator.translate(text, src='auto', dest='en').text
# final_result = translator.translate(mid_result, src='en', dest='sk').text
# logger.info("Translation - After: " + final_result)
# before_words = text.split()
# after_words = final_result.split()
# diff = list(difflib.ndiff(before_words, after_words))
# changed_words = [word[2:] for word in diff if word.startswith('+ ')]
# if changed_words:
# logger.info("Changed words: " + ", ".join(changed_words))
# else:
# logger.info("No changed words detected.")
# return final_result
# except Exception as e:
# logger.error(f"Translation error: {e}")
# return text
return text return text
try: ###############################################################################
# 1) Slovak (or any language) -> English # Функция перевода описания лекарства с сохранением названия (до двоеточия) #
mid_result = translator.translate(text, src='auto', dest='en').text ###############################################################################
def translate_preserving_medicine_names(text: str) -> str:
# 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}") # pattern = re.compile(r'^(\d+\.\s*[^:]+:\s*)(.*)$', re.MULTILINE)
return text # fallback to the original text #
# def replacer(match):
# prefix = match.group(1)
# description = match.group(2)
# logger.info("Translating description: " + description)
# translated_description = translate_to_slovak(description)
# logger.info("Translated description: " + translated_description)
# diff = list(difflib.ndiff(description.split(), translated_description.split()))
# changed_words = [word[2:] for word in diff if word.startswith('+ ')]
# if changed_words:
# logger.info("Changed words in description: " + ", ".join(changed_words))
# else:
# logger.info("No changed words in description detected.")
# return prefix + translated_description
#
# if pattern.search(text):
# return pattern.sub(replacer, text)
# else:
# return translate_to_slovak(text)
return text
############################################################################### ###############################################################################
# Custom Mistral LLM # # Custom Mistral LLM #
@ -83,7 +116,7 @@ class CustomMistralLLM:
logger.info(f"Full response from model {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") return result.get("choices", [{}])[0].get("message", {}).get("content", "No response")
except HTTPError as e: except HTTPError as e:
if response.status_code == 429: # Too Many Requests if response.status_code == 429:
logger.warning(f"Rate limit exceeded. Waiting {delay} seconds before retry.") logger.warning(f"Rate limit exceeded. Waiting {delay} seconds before retry.")
time.sleep(delay) time.sleep(delay)
attempt += 1 attempt += 1
@ -95,7 +128,6 @@ class CustomMistralLLM:
raise e raise e
raise Exception("Reached maximum number of retries for API request") raise Exception("Reached maximum number of retries for API request")
############################################################################### ###############################################################################
# Initialize embeddings and Elasticsearch store # # Initialize embeddings and Elasticsearch store #
############################################################################### ###############################################################################
@ -104,7 +136,6 @@ embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-
index_name = 'drug_docs' index_name = 'drug_docs'
# Connect to Elasticsearch
if config.get("useCloud", False): if config.get("useCloud", False):
logger.info("Using cloud Elasticsearch.") logger.info("Using cloud Elasticsearch.")
cloud_id = "tt:dXMtZWFzdC0yLmF3cy5lbGFzdGljLWNsb3VkLmNvbTo0NDMkOGM3ODQ0ZWVhZTEyNGY3NmFjNjQyNDFhNjI4NmVhYzMkZTI3YjlkNTQ0ODdhNGViNmEyMTcxMjMxNmJhMWI0ZGU=" cloud_id = "tt:dXMtZWFzdC0yLmF3cy5lbGFzdGljLWNsb3VkLmNvbTo0NDMkOGM3ODQ0ZWVhZTEyNGY3NmFjNjQyNDFhNjI4NmVhYzMkZTI3YjlkNTQ0ODdhNGViNmEyMTcxMjMxNmJhMWI0ZGU="
@ -125,7 +156,6 @@ else:
logger.info(f"Connected to {'cloud' if config.get('useCloud', False) else 'local'} Elasticsearch.") logger.info(f"Connected to {'cloud' if config.get('useCloud', False) else 'local'} Elasticsearch.")
############################################################################### ###############################################################################
# Initialize Mistral models (small & large) # # Initialize Mistral models (small & large) #
############################################################################### ###############################################################################
@ -141,41 +171,52 @@ llm_large = CustomMistralLLM(
model_name="mistral-large-latest" model_name="mistral-large-latest"
) )
############################################################################### ###############################################################################
# Helper function to evaluate model output # # Helper function to evaluate model output #
############################################################################### ###############################################################################
def evaluate_results(query, summaries, model_name): 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 total_score = 0
explanation = [] explanation = []
for i, summary in enumerate(summaries): for i, summary in enumerate(summaries):
# Length-based scoring
length_score = min(len(summary) / 100, 10) length_score = min(len(summary) / 100, 10)
total_score += length_score total_score += length_score
explanation.append(f"Document {i+1}: Length 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_matches = sum(1 for word in query_keywords if word.lower() in summary.lower())
keyword_score = min(keyword_matches * 2, 10) keyword_score = min(keyword_matches * 2, 10)
total_score += keyword_score total_score += keyword_score
explanation.append(f"Document {i+1}: Keyword match score - {keyword_score}") explanation.append(f"Document {i+1}: Keyword match score - {keyword_score}")
final_score = total_score / len(summaries) if summaries else 0 final_score = total_score / len(summaries) if summaries else 0
explanation_summary = "\n".join(explanation) explanation_summary = "\n".join(explanation)
logger.info(f"Evaluation for model {model_name}: {final_score}/10") logger.info(f"Evaluation for model {model_name}: {final_score}/10")
logger.info(f"Explanation:\n{explanation_summary}") logger.info(f"Explanation:\n{explanation_summary}")
return {"rating": round(final_score, 2), "explanation": explanation_summary} return {"rating": round(final_score, 2), "explanation": explanation_summary}
###############################################################################
# validation of recieved answer is it correct for user question #
###############################################################################
def validate_answer_logic(query: str, answer: str) -> str:
"""
Проверяет, соответствует ли ответ логике вопроса.
Если, например, вопрос относится к ľudským liekom a obsahuje otázku na dávkovanie,
odpoveď musí obsahovať iba lieky vhodné pre ľudí s uvedením správneho dávkovania.
"""
validation_prompt = (
f"Otázka: '{query}'\n"
f"Odpoveď: '{answer}'\n\n"
"Analyzuj prosím túto odpoveď. Ak odpoveď obsahuje odporúčania liekov, ktoré nie sú vhodné pre ľudí, "
"alebo ak neobsahuje správne informácie o dávkovaní, oprav ju tak, aby bola logicky konzistentná s otázkou. "
"Odpoveď musí obsahovať iba lieky určené pre ľudí a pri potrebe aj presné informácie o dávkovaní (napr. v gramoch). "
"Ak je odpoveď logická a korektná, vráť pôvodnú odpoveď bez zmien. "
"Odpovedz v slovenčine a iba čistou, konečnou odpoveďou bez ďalších komentárov."
)
try:
validated_answer = llm_small.generate_text(prompt=validation_prompt, max_tokens=500, temperature=0.5)
logger.info(f"Validated answer: {validated_answer}")
return validated_answer
except Exception as e:
logger.error(f"Error during answer validation: {e}")
return answer
############################################################################### ###############################################################################
# Main function: process_query_with_mistral (Slovak prompt) # # Main function: process_query_with_mistral (Slovak prompt) #
@ -186,29 +227,25 @@ def process_query_with_mistral(query, k=10):
# --- Vector search --- # --- Vector search ---
vector_results = vectorstore.similarity_search(query, k=k) vector_results = vectorstore.similarity_search(query, k=k)
vector_documents = [hit.metadata.get('text', '') for hit in vector_results] vector_documents = [hit.metadata.get('text', '') for hit in vector_results]
max_docs = 5 max_docs = 5
max_doc_length = 1000 max_doc_length = 1000
vector_documents = [doc[:max_doc_length] for doc in vector_documents[:max_docs]] vector_documents = [doc[:max_doc_length] for doc in vector_documents[:max_docs]]
if vector_documents: if vector_documents:
# Slovak prompt
vector_prompt = ( vector_prompt = (
f"Otázka: '{query}'.\n" f"Otázka: '{query}'.\n"
"Na základe nasledujúcich informácií o liekoch:\n" "Na základe nasledujúcich informácií o liekoch:\n"
f"{vector_documents}\n\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ý. " "Prosím, uveďte tri najvhodnejšie lieky alebo riešenia pre daný problém. "
"Odpovedajte priamo a ľudským, priateľským tónom v číslovanom zozname, bez nepotrebných úvodných fráz alebo opisu procesu. " "Pre každý liek uveďte jeho názov, stručné a jasné vysvetlenie, prečo je vhodný, a ak je to relevantné, "
"aj odporúčané dávkovanie (napr. v gramoch alebo v iných vhodných jednotkách). "
"Odpovedajte priamo a ľudským, priateľským tónom v číslovanom zozname, bez nepotrebných úvodných fráz. "
"Odpoveď musí byť v slovenčine." "Odpoveď musí byť v slovenčine."
) )
summary_small_vector = llm_small.generate_text(prompt=vector_prompt, max_tokens=700, temperature=0.7) 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_large_vector = llm_large.generate_text(prompt=vector_prompt, max_tokens=700, temperature=0.7)
splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20) splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
split_summary_small_vector = splitter.split_text(summary_small_vector) split_summary_small_vector = splitter.split_text(summary_small_vector)
split_summary_large_vector = splitter.split_text(summary_large_vector) split_summary_large_vector = splitter.split_text(summary_large_vector)
small_vector_eval = evaluate_results(query, split_summary_small_vector, 'Mistral Small') small_vector_eval = evaluate_results(query, split_summary_small_vector, 'Mistral Small')
large_vector_eval = evaluate_results(query, split_summary_large_vector, 'Mistral Large') large_vector_eval = evaluate_results(query, split_summary_large_vector, 'Mistral Large')
else: else:
@ -224,24 +261,22 @@ def process_query_with_mistral(query, k=10):
) )
text_documents = [hit['_source'].get('text', '') for hit in es_results['hits']['hits']] 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]] text_documents = [doc[:max_doc_length] for doc in text_documents[:max_docs]]
if text_documents: if text_documents:
# Slovak prompt
text_prompt = ( text_prompt = (
f"Otázka: '{query}'.\n" f"Otázka: '{query}'.\n"
"Na základe nasledujúcich informácií o liekoch:\n" "Na základe nasledujúcich informácií o liekoch:\n"
f"{text_documents}\n\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ý. " "Prosím, uveďte tri najvhodnejšie lieky alebo riešenia pre daný problém. "
"Odpovedajte priamo a ľudským, priateľským tónom v číslovanom zozname, bez nepotrebných úvodných fráz alebo opisu procesu. " "Pre každý liek uveďte jeho názov, stručné a jasné vysvetlenie, prečo je vhodný, a ak je to relevantné, "
"aj odporúčané dávkovanie (napr. v gramoch alebo v iných vhodných jednotkách). "
"Odpovedajte priamo a ľudským, priateľským tónom v číslovanom zozname, bez nepotrebných úvodných fráz. "
"Odpoveď musí byť v slovenčine." "Odpoveď musí byť v slovenčine."
) )
summary_small_text = llm_small.generate_text(prompt=text_prompt, max_tokens=700, temperature=0.7) 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) summary_large_text = llm_large.generate_text(prompt=text_prompt, max_tokens=700, temperature=0.7)
splitter_text = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
split_summary_small_text = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20).split_text(summary_small_text) split_summary_small_text = splitter_text.split_text(summary_small_text)
split_summary_large_text = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20).split_text(summary_large_text) split_summary_large_text = splitter_text.split_text(summary_large_text)
small_text_eval = evaluate_results(query, split_summary_small_text, 'Mistral Small') small_text_eval = evaluate_results(query, split_summary_small_text, 'Mistral Small')
large_text_eval = evaluate_results(query, split_summary_large_text, 'Mistral Large') large_text_eval = evaluate_results(query, split_summary_large_text, 'Mistral Large')
else: else:
@ -250,30 +285,31 @@ def process_query_with_mistral(query, k=10):
summary_small_text = "" summary_small_text = ""
summary_large_text = "" summary_large_text = ""
# Combine all results and pick the best # Porovnanie výsledkov a výber najlepšieho
all_results = [ all_results = [
{"eval": small_vector_eval, "summary": summary_small_vector, "model": "Mistral Small Vector"}, {"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": large_vector_eval, "summary": summary_large_vector, "model": "Mistral Large Vector"},
{"eval": small_text_eval, "summary": summary_small_text, "model": "Mistral Small Text"}, {"eval": small_text_eval, "summary": summary_small_text, "model": "Mistral Small Text"},
{"eval": large_text_eval, "summary": summary_large_text, "model": "Mistral Large Text"}, {"eval": large_text_eval, "summary": summary_large_text, "model": "Mistral Large Text"},
] ]
best_result = max(all_results, key=lambda x: x["eval"]["rating"]) 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 from model {best_result['model']} with score {best_result['eval']['rating']}.")
# Final translation to Slovak (with logs before/after) # Dodatočná kontrola logiky odpovede
polished_answer = translate_to_slovak(best_result["summary"]) validated_answer = validate_answer_logic(query, best_result["summary"])
polished_answer = translate_preserving_medicine_names(validated_answer)
return { return {
"best_answer": polished_answer, "best_answer": polished_answer,
"model": best_result["model"], "model": best_result["model"],
"rating": best_result["eval"]["rating"], "rating": best_result["eval"]["rating"],
"explanation": best_result["eval"]["explanation"] "explanation": best_result["eval"]["explanation"]
} }
except Exception as e: except Exception as e:
logger.error(f"Error: {str(e)}") logger.error(f"Error: {str(e)}")
return { return {
"best_answer": "An error occurred during query processing.", "best_answer": "An error occurred during query processing.",
"error": str(e) "error": str(e)
} }

View File

@ -1,4 +1,6 @@
import time import time
import re
# Сохраняем оригинальную функцию time.time # Сохраняем оригинальную функцию time.time
_real_time = time.time _real_time = time.time
# Переопределяем time.time для смещения времени на 1 секунду назад # Переопределяем time.time для смещения времени на 1 секунду назад
@ -15,7 +17,7 @@ from model import process_query_with_mistral
import psycopg2 import psycopg2
from psycopg2.extras import RealDictCursor from psycopg2.extras import RealDictCursor
# Параметры подключения # Параметры подключения к базе данных
DATABASE_CONFIG = { DATABASE_CONFIG = {
"dbname": "postgres", "dbname": "postgres",
"user": "postgres", "user": "postgres",
@ -27,7 +29,6 @@ DATABASE_CONFIG = {
# Подключение к базе данных # Подключение к базе данных
try: try:
conn = psycopg2.connect(**DATABASE_CONFIG) conn = psycopg2.connect(**DATABASE_CONFIG)
cursor = conn.cursor(cursor_factory=RealDictCursor)
print("Подключение к базе данных успешно установлено") print("Подключение к базе данных успешно установлено")
except Exception as e: except Exception as e:
print(f"Ошибка подключения к базе данных: {e}") print(f"Ошибка подключения к базе данных: {e}")
@ -45,7 +46,8 @@ CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleuserconten
def save_user_to_db(name, email, google_id=None, password=None): def save_user_to_db(name, email, google_id=None, password=None):
try: try:
cursor.execute( with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(
""" """
INSERT INTO users (name, email, google_id, password) INSERT INTO users (name, email, google_id, password)
VALUES (%s, %s, %s, %s) VALUES (%s, %s, %s, %s)
@ -63,91 +65,154 @@ def save_user_to_db(name, email, google_id=None, password=None):
def verify_token(): def verify_token():
data = request.get_json() data = request.get_json()
token = data.get('token') token = data.get('token')
if not token: if not token:
return jsonify({'error': 'No token provided'}), 400 return jsonify({'error': 'No token provided'}), 400
try: try:
id_info = id_token.verify_oauth2_token(token, requests.Request(), CLIENT_ID) id_info = id_token.verify_oauth2_token(token, requests.Request(), CLIENT_ID)
user_email = id_info.get('email') user_email = id_info.get('email')
user_name = id_info.get('name') user_name = id_info.get('name')
google_id = id_info.get('sub') # Уникальный идентификатор пользователя Google google_id = id_info.get('sub') # Уникальный идентификатор пользователя Google
save_user_to_db(name=user_name, email=user_email, google_id=google_id) save_user_to_db(name=user_name, email=user_email, google_id=google_id)
logger.info(f"User authenticated and saved: {user_name} ({user_email})") logger.info(f"User authenticated and saved: {user_name} ({user_email})")
return jsonify({'message': 'Authentication successful', 'user': {'email': user_email, 'name': user_name}}), 200 return jsonify({'message': 'Authentication successful', 'user': {'email': user_email, 'name': user_name}}), 200
except ValueError as e: except ValueError as e:
logger.error(f"Token verification failed: {e}") logger.error(f"Token verification failed: {e}")
return jsonify({'error': 'Invalid token'}), 400 return jsonify({'error': 'Invalid token'}), 400
# Эндпоинт для регистрации пользователя # Эндпоинт для регистрации пользователя с проверкой на дублирование
@app.route('/api/register', methods=['POST']) @app.route('/api/register', methods=['POST'])
def register(): def register():
data = request.get_json() data = request.get_json()
name = data.get('name') name = data.get('name')
email = data.get('email') email = data.get('email')
password = data.get('password') # Рекомендуется хэшировать пароль password = data.get('password') # Рекомендуется хэшировать пароль
if not all([name, email, password]): if not all([name, email, password]):
return jsonify({'error': 'All fields are required'}), 400 return jsonify({'error': 'All fields are required'}), 400
try: try:
# Проверка, существует ли пользователь с таким email with conn.cursor(cursor_factory=RealDictCursor) as cur:
cursor.execute("SELECT * FROM users WHERE email = %s", (email,)) cur.execute("SELECT * FROM users WHERE email = %s", (email,))
existing_user = cursor.fetchone() existing_user = cur.fetchone()
if existing_user: if existing_user:
return jsonify({'error': 'User already exists'}), 409 return jsonify({'error': 'User already exists'}), 409
# Сохранение пользователя в базу данных
save_user_to_db(name=name, email=email, password=password) save_user_to_db(name=name, email=email, password=password)
return jsonify({'message': 'User registered successfully'}), 201 return jsonify({'message': 'User registered successfully'}), 201
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
# Эндпоинт для логина пользователя (см. предыдущий пример) # Эндпоинт для логина пользователя
@app.route('/api/login', methods=['POST']) @app.route('/api/login', methods=['POST'])
def login(): def login():
data = request.get_json() data = request.get_json()
email = data.get('email') email = data.get('email')
password = data.get('password') password = data.get('password')
if not all([email, password]): if not all([email, password]):
return jsonify({'error': 'Email and password are required'}), 400 return jsonify({'error': 'Email and password are required'}), 400
try: try:
cursor.execute("SELECT * FROM users WHERE email = %s", (email,)) with conn.cursor(cursor_factory=RealDictCursor) as cur:
user = cursor.fetchone() cur.execute("SELECT * FROM users WHERE email = %s", (email,))
user = cur.fetchone()
if not user: if not user:
return jsonify({'error': 'Invalid credentials'}), 401 return jsonify({'error': 'Invalid credentials'}), 401
# Сравнение простым текстом — в production используйте хэширование!
if user.get('password') != password: if user.get('password') != password:
return jsonify({'error': 'Invalid credentials'}), 401 return jsonify({'error': 'Invalid credentials'}), 401
return jsonify({'message': 'Login successful', 'user': {'name': user.get('name'), 'email': user.get('email')}}), 200
return jsonify({
'message': 'Login successful',
'user': {
'name': user.get('name'),
'email': user.get('email')
}
}), 200
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
# Эндпоинт для обработки запросов от фронтенда # Объединённый эндпоинт для обработки запроса чата
@app.route('/api/chat', methods=['POST']) @app.route('/api/chat', methods=['POST'])
def chat(): def chat():
data = request.get_json() data = request.get_json()
query = data.get('query', '') query = data.get('query', '')
user_email = data.get('email') # email пользователя (если передается)
chat_id = data.get('chatId') # параметр для обновления существующего чата
if not query: if not query:
return jsonify({'error': 'No query provided'}), 400 return jsonify({'error': 'No query provided'}), 400
response = process_query_with_mistral(query) # Вызов функции для обработки запроса (например, чат-бота)
return jsonify(response) response_obj = process_query_with_mistral(query)
best_answer = ""
if isinstance(response_obj, dict):
best_answer = response_obj.get("best_answer", "")
else:
best_answer = str(response_obj)
# Форматирование ответа с использованием re.sub
best_answer = re.sub(r'[*#]', '', best_answer)
best_answer = re.sub(r'(\d\.\s)', r'\n\n\1', best_answer)
best_answer = re.sub(r':\s-', r':\n-', best_answer)
# Если chatId передан, обновляем существующий чат, иначе создаем новый чат
if chat_id:
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("SELECT chat FROM chat_history WHERE id = %s", (chat_id,))
existing_chat = cur.fetchone()
if existing_chat:
updated_chat = existing_chat['chat'] + f"\nUser: {query}\nBot: {best_answer}"
cur.execute("UPDATE chat_history SET chat = %s WHERE id = %s", (updated_chat, chat_id))
conn.commit()
else:
with conn.cursor(cursor_factory=RealDictCursor) as cur2:
cur2.execute(
"INSERT INTO chat_history (user_email, chat) VALUES (%s, %s) RETURNING id",
(user_email, f"User: {query}\nBot: {best_answer}")
)
new_chat_id = cur2.fetchone()['id']
conn.commit()
chat_id = new_chat_id
except Exception as e:
return jsonify({'error': str(e)}), 500
else:
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(
"INSERT INTO chat_history (user_email, chat) VALUES (%s, %s) RETURNING id",
(user_email, f"User: {query}\nBot: {best_answer}")
)
new_chat_id = cur.fetchone()['id']
conn.commit()
chat_id = new_chat_id
except Exception as e:
return jsonify({'error': str(e)}), 500
# Возвращаем текстовый ответ и новый chatId, если чат был создан
return jsonify({'response': {'best_answer': best_answer, 'model': 'Mistral Small Vector', 'chatId': chat_id}}), 200
# Эндпоинт для получения истории чатов конкретного пользователя
@app.route('/api/chat_history', methods=['GET'])
def get_chat_history():
user_email = request.args.get('email')
if not user_email:
return jsonify({'error': 'User email is required'}), 400
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(
"SELECT id, chat, created_at FROM chat_history WHERE user_email = %s ORDER BY created_at DESC",
(user_email,)
)
history = cur.fetchall()
return jsonify({'history': history}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
# Эндпоинт для получения деталей чата по ID
@app.route('/api/chat_history_detail', methods=['GET'])
def chat_history_detail():
chat_id = request.args.get('id')
if not chat_id:
return jsonify({'error': 'Chat id is required'}), 400
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("SELECT id, chat, created_at FROM chat_history WHERE id = %s", (chat_id,))
chat = cur.fetchone()
if not chat:
return jsonify({'error': 'Chat not found'}), 404
return jsonify({'chat': chat}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True) app.run(host='0.0.0.0', port=5000, debug=True)

View File

@ -1,10 +1,11 @@
import { BrowserRouter as Router, Route, Routes, Outlet } from 'react-router-dom'; 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'; import LandingPage from './pages/LandingPage';
import RegistrationForm from "./Components/RegistrationForm.tsx"; import RegistrationForm from "./Components/RegistrationForm";
import LoginForm from "./Components/LoginForm.tsx"; import LoginForm from "./Components/LoginForm";
import ChatHistory from "./Components/ChatHistory";
import HomePage from './pages/HomePage';
import NewChatPage from "./Components/NewChatPage";
const Layout = () => ( const Layout = () => (
<div className="flex w-full h-screen dark:bg-slate-200"> <div className="flex w-full h-screen dark:bg-slate-200">
@ -17,25 +18,24 @@ const Layout = () => (
</div> </div>
); );
function App() { function App() {
return ( return (
<Router> <Router>
<Routes> <Routes>
<Route path='/' element={<LandingPage />} /> <Route path="/" element={<LandingPage />} />
<Route path="/register" element={<RegistrationForm />} /> <Route path="/register" element={<RegistrationForm />} />
<Route path="/login" element={<LoginForm />} /> <Route path="/login" element={<LoginForm />} />
<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 path="/dashboard" element={<Layout />}>
{/* Новый чат */}
<Route path="new-chat" element={<NewChatPage />} />
{/* Существующий чат (после создания нового, URL обновится) */}
<Route path="chat/:id" element={<HomePage />} />
<Route path="history" element={<ChatHistory />} />
<Route index element={<HomePage />} /> <Route index element={<HomePage />} />
<Route path="history" element={<>Sorry not implemented yet</>} />
</Route> </Route>
</Routes> </Routes>
</Router> </Router>
) );
} }
export default App export default App;

View File

@ -0,0 +1,58 @@
import React, { useEffect, useState } from 'react';
import { useParams, useLocation } from 'react-router-dom';
interface ChatHistoryItem {
id: number;
chat: string;
created_at: string;
}
const ChatDetails: React.FC = () => {
const { id } = useParams<{ id: string }>();
const location = useLocation();
const [chat, setChat] = useState<ChatHistoryItem | null>(location.state?.chat || null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!chat && id) {
// Если данные не переданы через state, можно попробовать получить их с сервера
fetch(`http://localhost:5000/api/chat_history_detail?id=${encodeURIComponent(id)}`)
.then((res) => {
if (!res.ok) {
throw new Error('Chat not found');
}
return res.json();
})
.then((data) => {
if (data.error) {
setError(data.error);
} else {
setChat(data.chat);
}
})
.catch((err) => setError(err.message));
}
}, [id, chat]);
if (error) {
return <div>Error: {error}</div>;
}
if (!chat) {
return <div>Loading chat details...</div>;
}
return (
<div style={{ padding: '20px' }}>
<h1>Chat Details</h1>
<div style={{ border: '1px solid #ccc', padding: '10px' }}>
{chat.chat.split('\n').map((line, index) => (
<p key={index}>{line}</p>
))}
</div>
<small>{new Date(chat.created_at).toLocaleString()}</small>
</div>
);
};
export default ChatDetails;

View File

@ -0,0 +1,80 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
interface ChatHistoryItem {
id: number;
chat: string;
created_at: string;
}
const ChatHistory: React.FC = () => {
const [history, setHistory] = useState<ChatHistoryItem[]>([]);
const [error, setError] = useState<string | null>(null);
const navigate = useNavigate();
useEffect(() => {
const storedUser = localStorage.getItem('user');
if (storedUser) {
const user = JSON.parse(storedUser);
const email = user.email;
fetch(`http://localhost:5000/api/chat_history?email=${encodeURIComponent(email)}`)
.then((res) => res.json())
.then((data) => {
if (data.error) {
setError(data.error);
} else {
setHistory(data.history);
}
})
.catch(() => setError('Error fetching chat history'));
} else {
setError('User not logged in');
}
}, []);
// При клике перенаправляем пользователя на /dashboard/chat/{chatId}
const handleClick = (item: ChatHistoryItem) => {
navigate(`/dashboard/chat/${item.id}`, { state: { selectedChat: item } });
};
return (
<div style={{ padding: '20px' }}>
<h1>Chat History</h1>
{error && <p style={{ color: 'red' }}>{error}</p>}
{history.length === 0 && !error ? (
<p>No chat history found.</p>
) : (
<ul style={{ listStyleType: 'none', padding: 0 }}>
{history.map((item) => {
// Извлекаем первую строку из сохранённого чата.
// Предполагаем, что чат хранится в формате: "User: <вопрос>\nBot: <ответ>\n..."
const lines = item.chat.split("\n");
let firstUserMessage = lines[0];
if (firstUserMessage.startsWith("User:")) {
firstUserMessage = firstUserMessage.replace("User:", "").trim();
}
return (
<li
key={item.id}
style={{
marginBottom: '15px',
borderBottom: '1px solid #ccc',
paddingBottom: '10px',
cursor: 'pointer'
}}
onClick={() => handleClick(item)}
>
<div>
<strong>{firstUserMessage}</strong>
</div>
<small>{new Date(item.created_at).toLocaleString()}</small>
</li>
);
})}
</ul>
)}
</div>
);
};
export default ChatHistory;

View File

@ -1,47 +1,46 @@
import React, { useEffect, useState } from 'react' 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 { Link } from 'react-router-dom';
import { MdOutlineDarkMode } from "react-icons/md";
import { CiLight } from "react-icons/ci";
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
import { MdAddCircleOutline, MdOutlineDarkMode } from "react-icons/md";
import { GoHistory } from "react-icons/go";
import { CiLight } from "react-icons/ci";
import { CgLogIn } from "react-icons/cg"; import { CgLogIn } from "react-icons/cg";
import BackImage from '../assets/smallheadicon.png' import BackImage from '../assets/smallheadicon.png';
export interface NavigationItem { export interface NavigationItem {
icon: React.ReactNode, icon: React.ReactNode;
title: string, title: string;
link: string link: string;
} }
const NavigationItems: NavigationItem[] = [ const NavigationItems: NavigationItem[] = [
{ {
title: 'Dashboard', title: 'New Chat',
link: '/dashboard', link: '/dashboard/new-chat', // Перенаправляем сразу на новый чат
icon: <IoMdHome size={30} /> icon: <MdAddCircleOutline size={30} />
}, },
{ {
title: 'History', title: 'History',
link: '/dashboard/history', link: '/dashboard/history',
icon: <GoHistory size={25} /> icon: <GoHistory size={25} />
} }
] ];
interface NavigationProps { interface NavigationProps {
isExpanded: boolean, isExpanded: boolean;
} }
const Navigation = ({ isExpanded = false }: NavigationProps) => { const Navigation = ({ isExpanded = false }: NavigationProps) => {
const [theme, setTheme] = useState<'dark' | 'light'>('light') const [theme, setTheme] = useState<'dark' | 'light'>('light');
useEffect(() => { useEffect(() => {
if (window.matchMedia('(prefers-color-scheme:dark)').matches) { if (window.matchMedia('(prefers-color-scheme:dark)').matches) {
setTheme('dark'); setTheme('dark');
} else { } else {
setTheme('light') setTheme('light');
} }
}, []) }, []);
useEffect(() => { useEffect(() => {
if (theme === "dark") { if (theme === "dark") {
@ -49,11 +48,11 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => {
} else { } else {
document.documentElement.classList.remove("dark"); document.documentElement.classList.remove("dark");
} }
}, [theme]) }, [theme]);
const handleThemeSwitch = () => { const handleThemeSwitch = () => {
setTheme(theme === "dark" ? "light" : "dark") setTheme(theme === "dark" ? "light" : "dark");
} };
// Загружаем данные пользователя из localStorage (если имеются) // Загружаем данные пользователя из localStorage (если имеются)
const [user, setUser] = useState<any>(null); const [user, setUser] = useState<any>(null);
@ -70,7 +69,7 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => {
<div className='flex flex-col items-start gap-12'> <div className='flex flex-col items-start gap-12'>
<Link to='/' className='w-full flex items-center justify-center'> <Link to='/' className='w-full flex items-center justify-center'>
<IconButton sx={{ width: 40, height: 40 }}> <IconButton sx={{ width: 40, height: 40 }}>
<img src={BackImage} width={25} alt="" /> <img src={BackImage} width={25} alt="Back" />
</IconButton> </IconButton>
{isExpanded && ( {isExpanded && (
<p className='text-2xl font-semibold text-dark-blue flex items-center'> <p className='text-2xl font-semibold text-dark-blue flex items-center'>
@ -80,7 +79,11 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => {
</Link> </Link>
<div className='flex flex-col p-1 gap-5 items-center'> <div className='flex flex-col p-1 gap-5 items-center'>
{NavigationItems.map((item) => ( {NavigationItems.map((item) => (
<Link key={item.link} to={item.link} className='flex gap-2 items-center w-full'> <Link
key={item.link}
to={item.link}
className='flex gap-2 items-center w-full'
>
<IconButton <IconButton
sx={{ sx={{
width: 40, width: 40,
@ -129,7 +132,7 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => {
borderRadius: 2, borderRadius: 2,
background: theme === 'dark' ? 'white' : 'initial', background: theme === 'dark' ? 'white' : 'initial',
'&:focus-visible': { '&:focus-visible': {
outline: '2px solid blue', // Кастомный стиль фокуса outline: '2px solid blue',
outlineOffset: '0px', outlineOffset: '0px',
borderRadius: '4px', borderRadius: '4px',
}, },
@ -142,7 +145,7 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => {
</div> </div>
</div> </div>
</div> </div>
) );
} };
export default Navigation; export default Navigation;

View File

@ -0,0 +1,186 @@
import React, { useEffect, useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';
import { useLazySendChatQuestionQuery } from '../store/api/chatApi';
import callCenterIcon from '../assets/call-center.png';
interface ChatMessage {
sender: string;
text: string;
}
const NewChatPage: React.FC = () => {
const [sendChatQuestion, { isLoading, isFetching }] = useLazySendChatQuestionQuery();
const [message, setMessage] = useState<string>('');
const [chatHistory, setChatHistory] = useState<ChatMessage[]>([]);
const navigate = useNavigate();
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [chatHistory, isLoading, isFetching]);
useEffect(() => {
setChatHistory([]);
}, []);
async function onSubmit() {
if (!message.trim()) return;
const newUserMessage: ChatMessage = { sender: 'User', text: message };
const updatedHistory = [...chatHistory, newUserMessage];
setChatHistory(updatedHistory);
const userMessage = message;
setMessage('');
const storedUser = localStorage.getItem('user');
const email = storedUser ? JSON.parse(storedUser).email : '';
const question = { query: userMessage, email };
try {
const res = await sendChatQuestion(question).unwrap();
console.log('Response from server:', res);
let bestAnswer = res.response.best_answer;
if (typeof bestAnswer !== 'string') {
bestAnswer = String(bestAnswer);
}
bestAnswer = bestAnswer.trim();
const newAssistantMessage: ChatMessage = { sender: 'Assistant', text: bestAnswer };
const newUpdatedHistory = [...updatedHistory, newAssistantMessage];
setChatHistory(newUpdatedHistory);
if (res.response.chatId) {
const chatString = newUpdatedHistory
.map(msg => (msg.sender === 'User' ? 'User: ' : 'Bot: ') + msg.text)
.join('\n');
navigate(`/dashboard/chat/${res.response.chatId}`, {
replace: true,
state: {
selectedChat: {
id: res.response.chatId,
chat: chatString,
created_at: new Date().toISOString()
}
}
});
}
} catch (error) {
console.error('Error:', error);
setChatHistory(prev => [
...prev,
{ sender: 'Assistant', text: 'Что-то пошло не так' }
]);
}
}
useGSAP(() => {
gsap.from('#input', { opacity: 0, y: 5, ease: 'power2.inOut', duration: 0.5 });
}, []);
return (
<div className="flex flex-col justify-end items-center p-4 gap-8 h-full w-full">
<div className="w-full p-2 rounded overflow-y-auto h-full mb-4">
{chatHistory.length > 0 ? (
<>
{chatHistory.map((msg, index) => (
<div
key={index}
className={`flex mb-2 ${msg.sender === 'User' ? 'justify-end' : 'justify-start items-start'}`}
>
{msg.sender === 'Assistant' && (
<img
src={callCenterIcon}
alt="Call Center Icon"
className="w-6 h-6 mr-2"
/>
)}
<div
className={`p-3 rounded-lg max-w-md flex ${
msg.sender === 'User'
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-800'
}`}
style={{ whiteSpace: 'normal' }}
>
{msg.text.split('\n').map((line, i) => (
<p key={i}>{line}</p>
))}
</div>
</div>
))}
{(isLoading || isFetching) && (
<div className="flex mb-2 justify-start items-start">
<img
src={callCenterIcon}
alt="Call Center Icon"
className="w-6 h-6 mr-2"
/>
<div
className="p-3 rounded-lg max-w-md flex bg-gray-200 text-gray-800"
style={{ whiteSpace: 'normal' }}
>
<div className="flex items-center">
<svg
className="animate-spin h-5 w-5 mr-3 text-gray-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
></path>
</svg>
<span>Assistant is typing...</span>
</div>
</div>
</div>
)}
</>
) : (
<div className="w-full h-full flex flex-col gap-2 items-center justify-center">
<h1 className="text-xl" id="firstheading">
Start a New Chat
</h1>
</div>
)}
<div ref={messagesEndRef} />
</div>
<div id="input" className="w-2/3 mb-20">
<div className="flex">
<input
type="text"
placeholder="Type your message..."
value={message}
onChange={(e) => setMessage(e.target.value)}
className="w-full px-5 py-2 rounded-l-xl outline-none border border-gray-300"
/>
<button
disabled={isLoading || isFetching}
onClick={onSubmit}
className="bg-black text-white font-semibold px-4 py-2 rounded-r-xl hover:bg-slate-700"
>
Send
</button>
</div>
</div>
</div>
);
};
export default NewChatPage;

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -1,116 +1,225 @@
import React, { useEffect, useState, useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useLazySendChatQuestionQuery } from '../store/api/chatApi';
import callCenterIcon from '../assets/call-center.png';
import { MdLocalPharmacy, MdSelfImprovement } from "react-icons/md"; interface ChatMessage {
import { GiPill } from "react-icons/gi"; sender: string;
import { useState } from "react"; text: string;
import gsap from "gsap"; }
import { useGSAP } from "@gsap/react";
import { useLazySendChatQuestionQuery } from "../store/api/chatApi"; const HomePage: React.FC = () => {
const HomePage = () => {
const [sendChatQuestion, { isLoading, isFetching }] = useLazySendChatQuestionQuery(); const [sendChatQuestion, { isLoading, isFetching }] = useLazySendChatQuestionQuery();
const [message, setMessage] = useState('');
const [chatHistory, setChatHistory] = useState<ChatMessage[]>([]);
const messagesEndRef = useRef<HTMLDivElement>(null);
type Category = 'medication' | 'supplements' | 'lifestyle'; const location = useLocation();
const [category, setCategory] = useState<Category | null>(null); const navigate = useNavigate();
const [message, setMessage] = useState<string>(''); const state = (location.state as any) || {};
const [chatHistory, setChatHistory] = useState<{ sender: string; text: string, rating?: number, explanation?: string }[]>([]);
async function onSubmit() { const isNewChat = state.newChat === true;
if (!message.trim()) return; const selectedChat = state.selectedChat || null;
setChatHistory([...chatHistory, { sender: 'User', text: message }]); const selectedChatId = selectedChat ? selectedChat.id : null;
setMessage('');
const question = { query: message }; useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [chatHistory, isLoading, isFetching]);
try { useEffect(() => {
const res = await sendChatQuestion(question).unwrap(); if (!isNewChat && selectedChat && selectedChat.chat) {
console.log("Response from server:", res); const messages: ChatMessage[] = selectedChat.chat
.split(/(?=^(User:|Bot:))/m)
.map((msg) => {
const trimmed = msg.trim();
const sender = trimmed.startsWith('User:') ? 'User' : 'Assistant';
return {
sender,
text: trimmed.replace(/^User:|^Bot:/, '').trim(),
};
});
setChatHistory(messages);
} else {
setChatHistory([]);
}
}, [isNewChat, selectedChat]);
let bestAnswer = res.best_answer.replace(/[*#]/g, ""); /**
const model = res.model; * Функция форматирования сообщения.
* Если в ответе отсутствуют символы перевода строки, пытаемся разбить текст по нумерованным пунктам.
*/
const formatMessage = (text: string) => {
let lines: string[] = [];
bestAnswer = bestAnswer.replace(/(\d\.\s)/g, "\n\n$1").replace(/:\s-/g, ":\n-"); if (text.includes('\n')) {
lines = text.split('\n');
} else {
lines = text.split(/(?=\d+\.\s+)/);
}
const assistantMessage = { lines = lines.map((line) => line.trim()).filter((line) => line !== '');
sender: 'Assistant', if (lines.length === 0) return null;
text: `Model: ${model}:\n${bestAnswer}`,
return lines.map((line, index) => {
if (/^\d+\.\s*/.test(line)) {
const colonIndex = line.indexOf(':');
if (colonIndex !== -1) {
const firstPart = line.substring(0, colonIndex);
const rest = line.substring(colonIndex + 1);
return (
<div key={index} className="mb-1">
<strong>{firstPart.trim()}</strong>: {rest.trim()}
</div>
);
} else {
return (
<div key={index} className="mb-1">
<strong>{line}</strong>
</div>
);
}
}
return <div key={index}>{line}</div>;
});
}; };
setChatHistory((prev) => [...prev, assistantMessage]); const onSubmit = async () => {
} catch (error) { if (!message.trim()) return;
console.error("Error:", error); const userMessage = message.trim();
setChatHistory((prev) => [...prev, { sender: 'Assistant', text: "Что-то пошло не так" }]); setMessage('');
setChatHistory((prev) => [...prev, { sender: 'User', text: userMessage }]);
const storedUser = localStorage.getItem('user');
const email = storedUser ? JSON.parse(storedUser).email : '';
const payload = selectedChatId
? { query: userMessage, chatId: selectedChatId, email }
: { query: userMessage, email };
try {
const res = await sendChatQuestion(payload).unwrap();
let bestAnswer = res.response.best_answer;
if (typeof bestAnswer !== 'string') {
bestAnswer = String(bestAnswer);
} }
bestAnswer = bestAnswer.trim();
if (bestAnswer) {
setChatHistory((prev) => [...prev, { sender: 'Assistant', text: bestAnswer }]);
} }
useGSAP(() => { if (!selectedChatId && res.response.chatId) {
gsap.from('#firstheading', { opacity: 0.3, ease: 'power2.inOut', duration: 0.5 }); const updatedChatHistory = [...chatHistory, { sender: 'User', text: userMessage }];
gsap.from('#secondheading', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.3, duration: 0.5 }); if (bestAnswer) {
gsap.from('#buttons', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.3, duration: 0.5 }); updatedChatHistory.push({ sender: 'Assistant', text: bestAnswer });
gsap.from('#input', { opacity: 0, y: 5, ease: 'power2.inOut', duration: 0.5 }); }
}, []); const chatString = updatedChatHistory
.map((msg) => (msg.sender === 'User' ? 'User: ' : 'Bot: ') + msg.text)
.join('\n');
navigate(`/dashboard/chat/${res.response.chatId}`, {
replace: true,
state: {
selectedChat: {
id: res.response.chatId,
chat: chatString,
created_at: new Date().toISOString(),
},
},
});
}
} catch (error) {
console.error('Error:', error);
setChatHistory((prev) => [
...prev,
{ sender: 'Assistant', text: 'Что-то пошло не так' },
]);
}
};
return ( return (
<div className='w-full h-full flex flex-col justify-end items-center p-4 gap-8'> <div className="flex flex-col justify-end items-center p-4 gap-8 h-full w-full">
<div className="w-full overflow-y-auto no-scrollbar h-full p-2 border-gray-200 mb-4"> <div className="w-full p-2 rounded overflow-y-auto h-full mb-4">
{chatHistory.length > 0 ? ( {chatHistory.length === 0 ? (
<> <div className="flex flex-col items-center justify-center h-full">
{chatHistory.map((msg, index) => ( <h1 className="text-xl">Start a New Chat</h1>
</div>
) : (
chatHistory.map((msg, index) => {
const formattedMessage = formatMessage(msg.text);
if (!formattedMessage) return null;
return (
<div <div
key={index} key={index}
className={`flex ${msg.sender === 'User' ? 'justify-end' : 'justify-start'} mb-2`} className={`flex mb-2 ${msg.sender === 'User' ? 'justify-end' : 'justify-start items-start'}`}
> >
{msg.sender === 'Assistant' && (
<img
src={callCenterIcon}
alt="Call Center Icon"
className="w-6 h-6 mr-2"
/>
)}
<div <div
className={`p-2 rounded-lg max-w-md ${msg.sender === 'User' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800'}`} className={`p-3 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) => ( {formattedMessage}
<p key={i}>{line}</p>
))}
{msg.rating && <p>Rating: {msg.rating}</p>}
{msg.explanation && <p>Explanation: {msg.explanation}</p>}
</div> </div>
</div> </div>
))} );
})
)}
{(isLoading || isFetching) && ( {(isLoading || isFetching) && (
<div className="flex justify-start mb-2"> <div className="flex mb-2 justify-start items-start">
<div className="p-2 rounded-lg max-w-md bg-gray-200 text-gray-800"> <img src={callCenterIcon} alt="Call Center Icon" className="w-6 h-6 mr-2" />
<p className="flex items-center">I'm thinking <div className="loader"></div></p> <div className="p-3 rounded-lg max-w-md bg-gray-200 text-gray-800 flex items-center">
<svg
className="animate-spin h-5 w-5 mr-3 text-gray-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
></path>
</svg>
<span>Assistant is typing...</span>
</div> </div>
</div> </div>
)} )}
</>
) : ( <div ref={messagesEndRef} />
<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>
{/*<div id="buttons">*/} <div className="w-2/3 mb-20">
{/* <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"> <div className="flex">
<input placeholder="Waiting for your question..." value={message} <input
type="text"
placeholder="Waiting for your question..."
value={message}
onChange={(e) => setMessage(e.target.value)} onChange={(e) => setMessage(e.target.value)}
className="w-full px-5 py-2 rounded-l-xl outline-none" type="text"/> disabled={isLoading || isFetching}
<button disabled={isLoading || isFetching} onClick={onSubmit} className="w-full px-5 py-2 rounded-l-xl outline-none border border-gray-300"
className="bg-black rounded-r-xl px-4 py-2 text-white font-semibold hover:bg-slate-700">Send />
<button
onClick={onSubmit}
disabled={isLoading || isFetching}
className="bg-black text-white font-semibold px-4 py-2 rounded-r-xl hover:bg-gray-800 disabled:opacity-50"
>
Send
</button> </button>
</div> </div>
</div> </div>