From 677ae05159633a5e3d387aa3c0e0b492049fa16b Mon Sep 17 00:00:00 2001 From: oleh Date: Wed, 12 Feb 2025 14:40:28 +0100 Subject: [PATCH] chat history, modified database on aws, navigation modified, clearify promts, temporary closing translator, add verification of answer depending on user question --- Backend/__pycache__/model.cpython-311.pyc | Bin 13973 -> 15660 bytes Backend/model.py | 164 ++++++++----- Backend/server.py | 165 +++++++++---- frontend/src/App.tsx | 60 ++--- frontend/src/Components/ChatDetails.tsx | 58 +++++ frontend/src/Components/ChatHistory.tsx | 80 +++++++ frontend/src/Components/Navigation.tsx | 53 ++-- frontend/src/Components/NewChatPage.tsx | 186 +++++++++++++++ frontend/src/assets/call-center.png | Bin 0 -> 28198 bytes frontend/src/pages/HomePage.tsx | 279 +++++++++++++++------- 10 files changed, 791 insertions(+), 254 deletions(-) create mode 100644 frontend/src/Components/ChatDetails.tsx create mode 100644 frontend/src/Components/ChatHistory.tsx create mode 100644 frontend/src/Components/NewChatPage.tsx create mode 100644 frontend/src/assets/call-center.png diff --git a/Backend/__pycache__/model.cpython-311.pyc b/Backend/__pycache__/model.cpython-311.pyc index dcfc304c4cb6c174f5441b19d04e7663492d7b18..ea50eff0b6e0c6b805717950077c2d2e3b2c6be2 100644 GIT binary patch delta 5472 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 ZuS3NNRxIPP-b#MC($E_!TCT%@{{=2Rv;6=7 diff --git a/Backend/model.py b/Backend/model.py index 90c5091..58ca380 100644 --- a/Backend/model.py +++ b/Backend/model.py @@ -3,6 +3,7 @@ import requests import logging import time import re +import difflib from requests.exceptions import HTTPError from elasticsearch import Elasticsearch from langchain.chains import SequentialChain @@ -11,48 +12,80 @@ from langchain_huggingface import HuggingFaceEmbeddings from langchain_elasticsearch import ElasticsearchStore from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.docstore.document import Document -from googletrans import Translator # Translator for final polishing +# from googletrans import Translator logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -# Load configuration config_file_path = "config.json" with open(config_file_path, 'r') as config_file: config = json.load(config_file) -# Load Mistral API key mistral_api_key = "hXDC4RBJk1qy5pOlrgr01GtOlmyCBaNs" if not mistral_api_key: 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: """ - Translates the entire text into Slovak. - Logs the text before and after translation. + Переводит весь текст на словацкий с логированием изменений. + Сейчас функция является заглушкой и возвращает исходный текст без изменений. """ - if not text.strip(): - return text - - try: - # 1) Slovak (or any language) -> English - mid_result = translator.translate(text, src='auto', dest='en').text - - # 2) English -> Slovak - final_result = translator.translate(mid_result, src='en', dest='sk').text - - return final_result - except Exception as e: - logger.error(f"Translation error: {e}") - return text # fallback to the original text - + # 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 +############################################################################### +# Функция перевода описания лекарства с сохранением названия (до двоеточия) # +############################################################################### +def translate_preserving_medicine_names(text: str) -> str: + """ + Ищет строки вида "номер. Название лекарства: описание..." и переводит только описание, + оставляя название без изменений. + Сейчас функция является заглушкой и возвращает исходный текст без изменений. + """ + # pattern = re.compile(r'^(\d+\.\s*[^:]+:\s*)(.*)$', re.MULTILINE) + # + # 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 # @@ -83,7 +116,7 @@ class CustomMistralLLM: logger.info(f"Full response from model {self.model_name}: {result}") return result.get("choices", [{}])[0].get("message", {}).get("content", "No response") except HTTPError as e: - if response.status_code == 429: # Too Many Requests + if response.status_code == 429: logger.warning(f"Rate limit exceeded. Waiting {delay} seconds before retry.") time.sleep(delay) attempt += 1 @@ -95,7 +128,6 @@ class CustomMistralLLM: raise e raise Exception("Reached maximum number of retries for API request") - ############################################################################### # Initialize embeddings and Elasticsearch store # ############################################################################### @@ -104,7 +136,6 @@ embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase- index_name = 'drug_docs' -# Connect to Elasticsearch if config.get("useCloud", False): logger.info("Using cloud Elasticsearch.") cloud_id = "tt:dXMtZWFzdC0yLmF3cy5lbGFzdGljLWNsb3VkLmNvbTo0NDMkOGM3ODQ0ZWVhZTEyNGY3NmFjNjQyNDFhNjI4NmVhYzMkZTI3YjlkNTQ0ODdhNGViNmEyMTcxMjMxNmJhMWI0ZGU=" @@ -125,7 +156,6 @@ else: logger.info(f"Connected to {'cloud' if config.get('useCloud', False) else 'local'} Elasticsearch.") - ############################################################################### # Initialize Mistral models (small & large) # ############################################################################### @@ -141,41 +171,52 @@ llm_large = CustomMistralLLM( model_name="mistral-large-latest" ) - ############################################################################### # Helper function to evaluate model output # ############################################################################### def evaluate_results(query, summaries, model_name): - """ - Evaluates results by: - - text length, - - presence of query keywords, etc. - Returns a rating and explanation. - """ query_keywords = query.split() total_score = 0 explanation = [] - for i, summary in enumerate(summaries): - # Length-based scoring length_score = min(len(summary) / 100, 10) total_score += length_score explanation.append(f"Document {i+1}: Length score - {length_score}") - - # Keyword-based scoring keyword_matches = sum(1 for word in query_keywords if word.lower() in summary.lower()) keyword_score = min(keyword_matches * 2, 10) total_score += keyword_score explanation.append(f"Document {i+1}: Keyword match score - {keyword_score}") - final_score = total_score / len(summaries) if summaries else 0 explanation_summary = "\n".join(explanation) - logger.info(f"Evaluation for model {model_name}: {final_score}/10") logger.info(f"Explanation:\n{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) # @@ -186,29 +227,25 @@ def process_query_with_mistral(query, k=10): # --- Vector search --- vector_results = vectorstore.similarity_search(query, k=k) vector_documents = [hit.metadata.get('text', '') for hit in vector_results] - max_docs = 5 max_doc_length = 1000 vector_documents = [doc[:max_doc_length] for doc in vector_documents[:max_docs]] - if vector_documents: - # Slovak prompt vector_prompt = ( f"Otázka: '{query}'.\n" "Na základe nasledujúcich informácií o liekoch:\n" f"{vector_documents}\n\n" - "Prosím, uveďte tri najvhodnejšie lieky alebo riešenia. Pre každý liek uveďte jeho názov a stručné, jasné vysvetlenie, prečo je vhodný. " - "Odpovedajte priamo a ľudským, priateľským tónom v číslovanom zozname, bez nepotrebných úvodných fráz alebo opisu procesu. " + "Prosím, uveďte tri najvhodnejšie lieky alebo riešenia pre daný problém. " + "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." ) - 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) - splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20) split_summary_small_vector = splitter.split_text(summary_small_vector) split_summary_large_vector = splitter.split_text(summary_large_vector) - small_vector_eval = evaluate_results(query, split_summary_small_vector, 'Mistral Small') large_vector_eval = evaluate_results(query, split_summary_large_vector, 'Mistral Large') else: @@ -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 = [doc[:max_doc_length] for doc in text_documents[:max_docs]] - if text_documents: - # Slovak prompt text_prompt = ( f"Otázka: '{query}'.\n" "Na základe nasledujúcich informácií o liekoch:\n" f"{text_documents}\n\n" - "Prosím, uveďte tri najvhodnejšie lieky alebo riešenia. Pre každý liek uveďte jeho názov a stručné, jasné vysvetlenie, prečo je vhodný. " - "Odpovedajte priamo a ľudským, priateľským tónom v číslovanom zozname, bez nepotrebných úvodných fráz alebo opisu procesu. " + "Prosím, uveďte tri najvhodnejšie lieky alebo riešenia pre daný problém. " + "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." ) - summary_small_text = llm_small.generate_text(prompt=text_prompt, max_tokens=700, temperature=0.7) summary_large_text = llm_large.generate_text(prompt=text_prompt, max_tokens=700, temperature=0.7) - - split_summary_small_text = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20).split_text(summary_small_text) - split_summary_large_text = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20).split_text(summary_large_text) - + splitter_text = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20) + split_summary_small_text = splitter_text.split_text(summary_small_text) + split_summary_large_text = splitter_text.split_text(summary_large_text) small_text_eval = evaluate_results(query, split_summary_small_text, 'Mistral Small') large_text_eval = evaluate_results(query, split_summary_large_text, 'Mistral Large') else: @@ -250,30 +285,31 @@ def process_query_with_mistral(query, k=10): summary_small_text = "" summary_large_text = "" - # Combine all results and pick the best + # Porovnanie výsledkov a výber najlepšieho all_results = [ {"eval": small_vector_eval, "summary": summary_small_vector, "model": "Mistral Small Vector"}, {"eval": large_vector_eval, "summary": summary_large_vector, "model": "Mistral Large Vector"}, {"eval": small_text_eval, "summary": summary_small_text, "model": "Mistral Small Text"}, {"eval": large_text_eval, "summary": summary_large_text, "model": "Mistral Large Text"}, ] - best_result = max(all_results, key=lambda x: x["eval"]["rating"]) logger.info(f"Best result from model {best_result['model']} with score {best_result['eval']['rating']}.") - # Final translation to Slovak (with logs before/after) - polished_answer = translate_to_slovak(best_result["summary"]) + # Dodatočná kontrola logiky odpovede + validated_answer = validate_answer_logic(query, best_result["summary"]) + + polished_answer = translate_preserving_medicine_names(validated_answer) return { "best_answer": polished_answer, "model": best_result["model"], "rating": best_result["eval"]["rating"], "explanation": best_result["eval"]["explanation"] } - except Exception as e: logger.error(f"Error: {str(e)}") return { "best_answer": "An error occurred during query processing.", "error": str(e) } + diff --git a/Backend/server.py b/Backend/server.py index db5cb4a..4583644 100644 --- a/Backend/server.py +++ b/Backend/server.py @@ -1,4 +1,6 @@ import time +import re + # Сохраняем оригинальную функцию time.time _real_time = time.time # Переопределяем time.time для смещения времени на 1 секунду назад @@ -15,7 +17,7 @@ from model import process_query_with_mistral import psycopg2 from psycopg2.extras import RealDictCursor -# Параметры подключения +# Параметры подключения к базе данных DATABASE_CONFIG = { "dbname": "postgres", "user": "postgres", @@ -27,7 +29,6 @@ DATABASE_CONFIG = { # Подключение к базе данных try: conn = psycopg2.connect(**DATABASE_CONFIG) - cursor = conn.cursor(cursor_factory=RealDictCursor) print("Подключение к базе данных успешно установлено") except Exception as e: print(f"Ошибка подключения к базе данных: {e}") @@ -45,15 +46,16 @@ CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleuserconten def save_user_to_db(name, email, google_id=None, password=None): try: - cursor.execute( - """ - INSERT INTO users (name, email, google_id, password) - VALUES (%s, %s, %s, %s) - ON CONFLICT (email) DO NOTHING - """, - (name, email, google_id, password) - ) - conn.commit() + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute( + """ + INSERT INTO users (name, email, google_id, password) + VALUES (%s, %s, %s, %s) + ON CONFLICT (email) DO NOTHING + """, + (name, email, google_id, password) + ) + conn.commit() print(f"User {name} ({email}) saved successfully!") except Exception as e: print(f"Error saving user to database: {e}") @@ -63,91 +65,154 @@ def save_user_to_db(name, email, google_id=None, password=None): def verify_token(): data = request.get_json() token = data.get('token') - if not token: return jsonify({'error': 'No token provided'}), 400 - try: id_info = id_token.verify_oauth2_token(token, requests.Request(), CLIENT_ID) user_email = id_info.get('email') user_name = id_info.get('name') google_id = id_info.get('sub') # Уникальный идентификатор пользователя Google - 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})") return jsonify({'message': 'Authentication successful', 'user': {'email': user_email, 'name': user_name}}), 200 - except ValueError as e: logger.error(f"Token verification failed: {e}") return jsonify({'error': 'Invalid token'}), 400 -# Эндпоинт для регистрации пользователя +# Эндпоинт для регистрации пользователя с проверкой на дублирование @app.route('/api/register', methods=['POST']) def register(): data = request.get_json() name = data.get('name') email = data.get('email') password = data.get('password') # Рекомендуется хэшировать пароль - if not all([name, email, password]): return jsonify({'error': 'All fields are required'}), 400 - try: - # Проверка, существует ли пользователь с таким email - cursor.execute("SELECT * FROM users WHERE email = %s", (email,)) - existing_user = cursor.fetchone() - if existing_user: - return jsonify({'error': 'User already exists'}), 409 - - # Сохранение пользователя в базу данных + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute("SELECT * FROM users WHERE email = %s", (email,)) + existing_user = cur.fetchone() + if existing_user: + return jsonify({'error': 'User already exists'}), 409 save_user_to_db(name=name, email=email, password=password) - return jsonify({'message': 'User registered successfully'}), 201 except Exception as e: return jsonify({'error': str(e)}), 500 -# Эндпоинт для логина пользователя (см. предыдущий пример) +# Эндпоинт для логина пользователя @app.route('/api/login', methods=['POST']) def login(): data = request.get_json() email = data.get('email') password = data.get('password') - if not all([email, password]): return jsonify({'error': 'Email and password are required'}), 400 - try: - cursor.execute("SELECT * FROM users WHERE email = %s", (email,)) - user = cursor.fetchone() - - if not user: - return jsonify({'error': 'Invalid credentials'}), 401 - - # Сравнение простым текстом — в production используйте хэширование! - if user.get('password') != password: - return jsonify({'error': 'Invalid credentials'}), 401 - - return jsonify({ - 'message': 'Login successful', - 'user': { - 'name': user.get('name'), - 'email': user.get('email') - } - }), 200 + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute("SELECT * FROM users WHERE email = %s", (email,)) + user = cur.fetchone() + if not user: + return jsonify({'error': 'Invalid credentials'}), 401 + if user.get('password') != password: + return jsonify({'error': 'Invalid credentials'}), 401 + return jsonify({'message': 'Login successful', 'user': {'name': user.get('name'), 'email': user.get('email')}}), 200 except Exception as e: return jsonify({'error': str(e)}), 500 -# Эндпоинт для обработки запросов от фронтенда +# Объединённый эндпоинт для обработки запроса чата @app.route('/api/chat', methods=['POST']) def chat(): data = request.get_json() query = data.get('query', '') + user_email = data.get('email') # email пользователя (если передается) + chat_id = data.get('chatId') # параметр для обновления существующего чата + if not query: 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__': app.run(host='0.0.0.0', port=5000, debug=True) + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 589728b..408819a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,41 +1,41 @@ import { BrowserRouter as Router, Route, Routes, Outlet } from 'react-router-dom'; import Navigation from './Components/Navigation'; -import HomePage from './pages/HomePage'; import LandingPage from './pages/LandingPage'; -import RegistrationForm from "./Components/RegistrationForm.tsx"; -import LoginForm from "./Components/LoginForm.tsx"; - +import RegistrationForm from "./Components/RegistrationForm"; +import LoginForm from "./Components/LoginForm"; +import ChatHistory from "./Components/ChatHistory"; +import HomePage from './pages/HomePage'; +import NewChatPage from "./Components/NewChatPage"; const Layout = () => ( -
- -
-
- -
+
+ +
+
+ +
+
-
); - function App() { - return ( - - - } /> - } /> - } /> - Sorry not implemented yet} /> - Sorry not implemented yet} /> - Sorry not implemented yet} /> - }> - } /> - Sorry not implemented yet} /> - - - - ) + return ( + + + } /> + } /> + } /> + }> + {/* Новый чат */} + } /> + {/* Существующий чат (после создания нового, URL обновится) */} + } /> + } /> + } /> + + + + ); } -export default App - +export default App; diff --git a/frontend/src/Components/ChatDetails.tsx b/frontend/src/Components/ChatDetails.tsx new file mode 100644 index 0000000..26977ec --- /dev/null +++ b/frontend/src/Components/ChatDetails.tsx @@ -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(location.state?.chat || null); + const [error, setError] = useState(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
Error: {error}
; + } + + if (!chat) { + return
Loading chat details...
; + } + + return ( +
+

Chat Details

+
+ {chat.chat.split('\n').map((line, index) => ( +

{line}

+ ))} +
+ {new Date(chat.created_at).toLocaleString()} +
+ ); +}; + +export default ChatDetails; diff --git a/frontend/src/Components/ChatHistory.tsx b/frontend/src/Components/ChatHistory.tsx new file mode 100644 index 0000000..4911f01 --- /dev/null +++ b/frontend/src/Components/ChatHistory.tsx @@ -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([]); + const [error, setError] = useState(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 ( +
+

Chat History

+ {error &&

{error}

} + {history.length === 0 && !error ? ( +

No chat history found.

+ ) : ( +
    + {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 ( +
  • handleClick(item)} + > +
    + {firstUserMessage} +
    + {new Date(item.created_at).toLocaleString()} +
  • + ); + })} +
+ )} +
+ ); +}; + +export default ChatHistory; diff --git a/frontend/src/Components/Navigation.tsx b/frontend/src/Components/Navigation.tsx index ddc20f2..04a00c0 100644 --- a/frontend/src/Components/Navigation.tsx +++ b/frontend/src/Components/Navigation.tsx @@ -1,47 +1,46 @@ -import React, { useEffect, useState } from 'react' -import { IoMdHome } from "react-icons/io"; -import { GoHistory } from "react-icons/go"; +import React, { useEffect, useState } from 'react'; 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 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 BackImage from '../assets/smallheadicon.png' +import BackImage from '../assets/smallheadicon.png'; export interface NavigationItem { - icon: React.ReactNode, - title: string, - link: string + icon: React.ReactNode; + title: string; + link: string; } const NavigationItems: NavigationItem[] = [ { - title: 'Dashboard', - link: '/dashboard', - icon: + title: 'New Chat', + link: '/dashboard/new-chat', // Перенаправляем сразу на новый чат + icon: }, { title: 'History', link: '/dashboard/history', icon: } -] +]; interface NavigationProps { - isExpanded: boolean, + isExpanded: boolean; } const Navigation = ({ isExpanded = false }: NavigationProps) => { - const [theme, setTheme] = useState<'dark' | 'light'>('light') + const [theme, setTheme] = useState<'dark' | 'light'>('light'); useEffect(() => { if (window.matchMedia('(prefers-color-scheme:dark)').matches) { setTheme('dark'); } else { - setTheme('light') + setTheme('light'); } - }, []) + }, []); useEffect(() => { if (theme === "dark") { @@ -49,11 +48,11 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => { } else { document.documentElement.classList.remove("dark"); } - }, [theme]) + }, [theme]); const handleThemeSwitch = () => { - setTheme(theme === "dark" ? "light" : "dark") - } + setTheme(theme === "dark" ? "light" : "dark"); + }; // Загружаем данные пользователя из localStorage (если имеются) const [user, setUser] = useState(null); @@ -70,7 +69,7 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => {
- + Back {isExpanded && (

@@ -80,7 +79,11 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => {

{NavigationItems.map((item) => ( - + { borderRadius: 2, background: theme === 'dark' ? 'white' : 'initial', '&:focus-visible': { - outline: '2px solid blue', // Кастомный стиль фокуса + outline: '2px solid blue', outlineOffset: '0px', borderRadius: '4px', }, @@ -142,7 +145,7 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => {
- ) -} + ); +}; export default Navigation; diff --git a/frontend/src/Components/NewChatPage.tsx b/frontend/src/Components/NewChatPage.tsx new file mode 100644 index 0000000..21233bd --- /dev/null +++ b/frontend/src/Components/NewChatPage.tsx @@ -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(''); + const [chatHistory, setChatHistory] = useState([]); + const navigate = useNavigate(); + const messagesEndRef = useRef(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 ( +
+
+ {chatHistory.length > 0 ? ( + <> + {chatHistory.map((msg, index) => ( +
+ {msg.sender === 'Assistant' && ( + Call Center Icon + )} +
+ {msg.text.split('\n').map((line, i) => ( +

{line}

+ ))} +
+
+ ))} + + {(isLoading || isFetching) && ( +
+ Call Center Icon +
+
+ + + + + Assistant is typing... +
+
+
+ )} + + ) : ( +
+

+ Start a New Chat +

+
+ )} +
+
+ +
+
+ setMessage(e.target.value)} + className="w-full px-5 py-2 rounded-l-xl outline-none border border-gray-300" + /> + +
+
+
+ ); +}; + +export default NewChatPage; diff --git a/frontend/src/assets/call-center.png b/frontend/src/assets/call-center.png new file mode 100644 index 0000000000000000000000000000000000000000..85970141cee7b1033e28fb5be9c6a4b0e0beae25 GIT binary patch literal 28198 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
  • I1a`jt>s(ZtMOvO!(vORD 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 { +const HomePage: React.FC = () => { const [sendChatQuestion, { isLoading, isFetching }] = useLazySendChatQuestionQuery(); + const [message, setMessage] = useState(''); + const [chatHistory, setChatHistory] = useState([]); + const messagesEndRef = useRef(null); - type Category = 'medication' | 'supplements' | 'lifestyle'; - const [category, setCategory] = useState(null); - const [message, setMessage] = useState(''); - const [chatHistory, setChatHistory] = useState<{ sender: string; text: string, rating?: number, explanation?: string }[]>([]); + const location = useLocation(); + const navigate = useNavigate(); + const state = (location.state as any) || {}; - async function onSubmit() { + const isNewChat = state.newChat === true; + const selectedChat = state.selectedChat || null; + const selectedChatId = selectedChat ? selectedChat.id : null; + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [chatHistory, isLoading, isFetching]); + + useEffect(() => { + if (!isNewChat && selectedChat && selectedChat.chat) { + 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]); + + /** + * Функция форматирования сообщения. + * Если в ответе отсутствуют символы перевода строки, пытаемся разбить текст по нумерованным пунктам. + */ + const formatMessage = (text: string) => { + let lines: string[] = []; + + if (text.includes('\n')) { + lines = text.split('\n'); + } else { + lines = text.split(/(?=\d+\.\s+)/); + } + + lines = lines.map((line) => line.trim()).filter((line) => line !== ''); + if (lines.length === 0) return null; + + 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 ( +
    + {firstPart.trim()}: {rest.trim()} +
    + ); + } else { + return ( +
    + {line} +
    + ); + } + } + return
    {line}
    ; + }); + }; + + const onSubmit = async () => { if (!message.trim()) return; - setChatHistory([...chatHistory, { sender: 'User', text: message }]); + const userMessage = message.trim(); setMessage(''); + setChatHistory((prev) => [...prev, { sender: 'User', text: userMessage }]); - const question = { query: message }; + 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(question).unwrap(); - console.log("Response from server:", res); + const res = await sendChatQuestion(payload).unwrap(); + let bestAnswer = res.response.best_answer; + if (typeof bestAnswer !== 'string') { + bestAnswer = String(bestAnswer); + } + bestAnswer = bestAnswer.trim(); - let bestAnswer = res.best_answer.replace(/[*#]/g, ""); - const model = res.model; + if (bestAnswer) { + setChatHistory((prev) => [...prev, { sender: 'Assistant', text: bestAnswer }]); + } - bestAnswer = bestAnswer.replace(/(\d\.\s)/g, "\n\n$1").replace(/:\s-/g, ":\n-"); - - const assistantMessage = { - sender: 'Assistant', - text: `Model: ${model}:\n${bestAnswer}`, - }; - - setChatHistory((prev) => [...prev, assistantMessage]); + if (!selectedChatId && res.response.chatId) { + const updatedChatHistory = [...chatHistory, { sender: 'User', text: userMessage }]; + if (bestAnswer) { + updatedChatHistory.push({ sender: 'Assistant', text: bestAnswer }); + } + 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: "Что-то пошло не так" }]); + console.error('Error:', error); + setChatHistory((prev) => [ + ...prev, + { sender: 'Assistant', text: 'Что-то пошло не так' }, + ]); } - } - - useGSAP(() => { - gsap.from('#firstheading', { opacity: 0.3, ease: 'power2.inOut', duration: 0.5 }); - gsap.from('#secondheading', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.3, duration: 0.5 }); - gsap.from('#buttons', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.3, duration: 0.5 }); - gsap.from('#input', { opacity: 0, y: 5, ease: 'power2.inOut', duration: 0.5 }); - }, []); + }; return ( -
    -
    - {chatHistory.length > 0 ? ( - <> - {chatHistory.map((msg, index) => ( +
    +
    + {chatHistory.length === 0 ? ( +
    +

    Start a New Chat

    +
    + ) : ( + chatHistory.map((msg, index) => { + const formattedMessage = formatMessage(msg.text); + if (!formattedMessage) return null; + return (
    + {msg.sender === 'Assistant' && ( + Call Center Icon + )}
    - {msg.text.split("\n").map((line, i) => ( -

    {line}

    - ))} - {msg.rating &&

    Rating: {msg.rating}

    } - {msg.explanation &&

    Explanation: {msg.explanation}

    } + {formattedMessage}
    - ))} - {(isLoading || isFetching) && ( -
    -
    -

    I'm thinking

    -
    -
    - )} - - ) : ( -
    -

    Ask any question or advice about your health or trainings and let's see what happens

    -

    Choose a category for a better experience and make your life better with Health AI

    + ); + }) + )} + + {(isLoading || isFetching) && ( +
    + Call Center Icon +
    + + + + + Assistant is typing... +
    )} + +
    - {/*
    */} - {/*
    */} - {/* */} - {/* */} - {/* */} - {/*
    */} - {/*
    */} - -
    +
    - setMessage(e.target.value)} - className="w-full px-5 py-2 rounded-l-xl outline-none" type="text"/> -