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:
parent
771a6f8432
commit
677ae05159
Binary file not shown.
162
Backend/model.py
162
Backend/model.py
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
58
frontend/src/Components/ChatDetails.tsx
Normal file
58
frontend/src/Components/ChatDetails.tsx
Normal 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;
|
80
frontend/src/Components/ChatHistory.tsx
Normal file
80
frontend/src/Components/ChatHistory.tsx
Normal 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;
|
@ -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;
|
||||||
|
186
frontend/src/Components/NewChatPage.tsx
Normal file
186
frontend/src/Components/NewChatPage.tsx
Normal 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;
|
BIN
frontend/src/assets/call-center.png
Normal file
BIN
frontend/src/assets/call-center.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user