Please check that ChatHXApp class and see if there is anything wrong with it: import sys
import os
import json
import re
import time
import traceback
import logging
import threading
import psutil  # For system resource usage
from PyQt6.QtWidgets import QGraphicsOpacityEffect
from PyQt6.QtCore import QPropertyAnimation, Qt, QSize, QThread, pyqtSignal, QTimer
from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QLineEdit, QPushButton, QDialog, QLabel, QFrame,
    QFileDialog, QMessageBox, QScrollArea, QGraphicsDropShadowEffect,
    QMenu, QProgressDialog, QSlider, QDoubleSpinBox, QSpinBox,
    QSystemTrayIcon, QStyle, QComboBox, QWidgetAction, QGroupBox, QCheckBox,
    QTabWidget
)
from PyQt6.QtGui import QFont, QColor, QIcon, QFontDatabase, QAction, QMovie, QPixmap, QPainter, QLinearGradient, QBrush
from llama_cpp import Llama
import openrag_S

class LLMInferenceThread(QThread):
    token_ready = pyqtSignal(str)
    inference_finished = pyqtSignal(float, int)  # Pass latency and token count

    def __init__(self, rag_llm, query, context_length, temperature, top_p, top_k, use_rag=True):
        super().__init__()
        self.rag_llm = rag_llm
        self.query = query
        self.context_length = int(context_length)
        self.temperature = temperature
        self.top_p = top_p
        self.top_k = top_k
        self.use_rag = use_rag
        self.stop_flag = False  # Flag to stop inference
        logging.debug("LLMInferenceThread initialized with query: %s", self.query)

    def run(self):
        logging.info("LLMInferenceThread started.")
        try:
            if not isinstance(self.context_length, int):
                raise TypeError(
                    f"context_length should be an integer, got {type(self.context_length)} instead."
                )
            logging.debug("Context length verified: %d", self.context_length)

            start_time = time.time()
            tokens_generated = 0

            if self.use_rag:
                logging.debug("Using RAG for response generation.")
                response_generator = self.rag_llm.generate_response(
                    self.query, context_length=self.context_length,
                    temperature=self.temperature, top_p=self.top_p, top_k=self.top_k)
            else:
                logging.debug("Not using RAG for response generation.")
                response_generator = self.rag_llm.generate_response_without_rag(
                    self.query, context_length=self.context_length,
                    temperature=self.temperature, top_p=self.top_p, top_k=self.top_k)

            for token in response_generator:
                if self.stop_flag:
                    logging.info("LLMInferenceThread received stop flag.")
                    break
                self.token_ready.emit(token)
                tokens_generated += 1
                logging.debug("Token generated: %s", token)

            latency = time.time() - start_time
            logging.info("LLMInferenceThread finished. Latency: %.2f sec, Tokens: %d", latency, tokens_generated)
            self.inference_finished.emit(latency, tokens_generated)

        except Exception as e:
            logging.error("Exception in LLMInferenceThread: %s", str(e), exc_info=True)
            self.token_ready.emit(f"Error: {str(e)}")
            self.inference_finished.emit(0, 0)
            logging.debug("Traceback: %s", traceback.format_exc())

    def stop(self):
        logging.info("LLMInferenceThread stop requested.")
        self.stop_flag = True

class StatisticsWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        logging.debug("Initializing StatisticsWindow.")
        self.setWindowTitle("Performance Statistics")
        self.setGeometry(150, 150, 400, 300)
        self.init_ui()
        self.update_timer = QTimer()
        self.update_timer.timeout.connect(self.update_resource_usage)
        self.update_timer.start(1000)  # Update every 1 second
        logging.debug("StatisticsWindow initialized.")

    def init_ui(self):
        logging.debug("StatisticsWindow UI setup started.")
        self.central_widget = QWidget()
        self.layout = QVBoxLayout()

        self.token_speed_label = QLabel("Token Speed: N/A")
        self.memory_usage_label = QLabel("Memory Usage: N/A")
        self.cpu_usage_label = QLabel("CPU Usage: N/A")
        self.latency_label = QLabel("Inference Latency: N/A")
        self.tokens_generated_label = QLabel("Tokens Generated: N/A")

        labels = [
            self.token_speed_label,
            self.memory_usage_label,
            self.cpu_usage_label,
            self.latency_label,
            self.tokens_generated_label
        ]

        for label in labels:
            label.setFont(QFont("CustomFont", 12))
            self.layout.addWidget(label)

        self.central_widget.setLayout(self.layout)
        self.setCentralWidget(self.central_widget)
        logging.debug("StatisticsWindow UI setup completed.")

    def update_metrics(self, token_speed, memory_usage, cpu_usage, latency, tokens_generated):
        logging.debug("Updating metrics: Speed=%.2f, Memory=%.2f, CPU=%.2f, Latency=%.2f, Tokens=%d",
                      token_speed, memory_usage, cpu_usage, latency, tokens_generated)
        self.token_speed_label.setText(f"Token Speed: {token_speed:.2f} tokens/sec")
        self.latency_label.setText(f"Inference Latency: {latency:.2f} sec")
        self.tokens_generated_label.setText(f"Tokens Generated: {tokens_generated}")

    def update_resource_usage(self):
        memory_usage = psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2  # In MB
        cpu_usage = psutil.cpu_percent()
        logging.debug("Resource usage - Memory: %.2f MB, CPU: %.2f%%", memory_usage, cpu_usage)
        self.memory_usage_label.setText(f"Memory Usage: {int(memory_usage)} MB")
        self.cpu_usage_label.setText(f"CPU Usage: {cpu_usage}%")

class DatasetLoadingThread(QThread):
    progress_update = pyqtSignal(int)
    finished = pyqtSignal()
    error = pyqtSignal(str)

    def __init__(self, dataset_path, search_engine):
        super().__init__()
        self.dataset_path = dataset_path
        self.search_engine = search_engine
        logging.debug("DatasetLoadingThread initialized with path: %s", self.dataset_path)

    def run(self):
        logging.info("DatasetLoadingThread started.")
        try:
            logging.debug("Loading documents from dataset path.")
            self.search_engine.load_documents(self.dataset_path)
            logging.info("DatasetLoadingThread completed successfully.")
        except Exception as e:
            logging.error("Exception in DatasetLoadingThread: %s", str(e), exc_info=True)
            self.error.emit(str(e))
        finally:
            self.finished.emit()
            logging.debug("DatasetLoadingThread signals finished.")

# Custom Gradient Button with Dynamic Animation
class GradientButton(QPushButton):
    def __init__(self, text, parent=None):
        super().__init__(text, parent)
        self.setMinimumHeight(40)
        self.gradient_direction = 0
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update_gradient)
        self.timer.start(100)  # Update gradient every 100 ms

    def update_gradient(self):
        self.gradient_direction = (self.gradient_direction + 5) % 360
        self.update()

    def paintEvent(self, event):
        painter = QPainter(self)
        gradient = QLinearGradient(0, 0, self.width(), self.height())
        gradient.setColorAt(0, QColor(255, 165, 0))  # Orange
        gradient.setColorAt(1, QColor(0, 191, 255))  # Deep Sky Blue
        gradient.setAngle(self.gradient_direction)
        painter.setBrush(QBrush(gradient))
        painter.setPen(Qt.PenStyle.NoPen)
        painter.drawRect(self.rect())
        painter.setPen(QColor(255, 255, 255))  # White text
        painter.setFont(self.font())
        painter.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, self.text())

# Existing Threads and Windows (LLMInferenceThread, DatasetLoadingThread, StatisticsWindow, SettingsWindow, ChatBubble)
# [Include the classes provided in the previous response without modification]

# Adding the TextToolsTab Class
class TextToolsTab(QWidget):
    def __init__(self, parent_app):
        super().__init__()
        self.parent_app = parent_app
        self.init_ui()
        self.current_loaded_file = None
        self.model = None

    def init_ui(self):
        self.layout = QVBoxLayout()

        # Top Layout with Load Button and Model Selection
        top_layout = QHBoxLayout()

        # Load File Button
        self.load_file_button = QPushButton("Load File")
        self.load_file_button.setStyleSheet("background-color: #28a745; color: white;")
        self.load_file_button.clicked.connect(self.load_file)
        top_layout.addWidget(self.load_file_button)

        # Spacer
        top_layout.addStretch()

        # Model Selection Dropdown
        self.model_combo = QComboBox()
        self.model_combo.addItem("Select Model")
        for model in self.parent_app.model_history:
            self.model_combo.addItem(model.get("model_name", "Unknown Model"))
        self.model_combo.currentIndexChanged.connect(self.select_model)
        top_layout.addWidget(self.model_combo)

        self.layout.addLayout(top_layout)

        # Central Area
        self.central_widget = QWidget()
        self.central_layout = QHBoxLayout()
        self.central_widget.setLayout(self.central_layout)

        # Initial Empty Text Box
        self.text_input = QLineEdit()
        self.text_input.setPlaceholderText("Paste or type your text here...")
        self.text_input.setFont(QFont("Arial", 12))
        self.text_input.setMinimumHeight(100)
        self.text_input.setAlignment(Qt.AlignmentFlag.AlignTop)
        self.central_layout.addWidget(self.text_input)

        self.layout.addWidget(self.central_widget)

        # Text Tools Buttons
        tools_layout = QHBoxLayout()

        # First Category Tools
        self.modify_tools_group = QGroupBox("Modify and Save")
        modify_layout = QVBoxLayout()
        modify_actions = ["Summarize", "Shorten", "Expand", "Proofread"]

        for action in modify_actions:
            btn = GradientButton(action)
            btn.clicked.connect(lambda checked, a=action: self.modify_and_save(a))
            modify_layout.addWidget(btn)

        self.modify_tools_group.setLayout(modify_layout)
        tools_layout.addWidget(self.modify_tools_group)

        # Second Category Tools
        self.generate_tools_group = QGroupBox("Generate New Text")
        generate_layout = QVBoxLayout()
        generate_actions = ["Explain More", "Translate To"]

        for action in generate_actions:
            btn = GradientButton(action)
            btn.clicked.connect(lambda checked, a=action: self.generate_new_text(a))
            generate_layout.addWidget(btn)

        self.generate_tools_group.setLayout(generate_layout)
        tools_layout.addWidget(self.generate_tools_group)

        self.layout.addLayout(tools_layout)

        # Dynamic Area for File Thumbnail and Messages
        self.dynamic_area = QHBoxLayout()
        self.layout.addLayout(self.dynamic_area)

        self.setLayout(self.layout)

    def load_file(self):
        logging.info("TextToolsTab: Load File button clicked.")
        file_path, _ = QFileDialog.getOpenFileName(
            self, "Select File", "", 
            "Text Files (*.txt);;PDF Files (*.pdf);;Word Documents (*.docx);;PowerPoint Presentations (*.pptx);;Images (*.png *.xpm *.jpg)"
        )
        if file_path:
            self.current_loaded_file = file_path
            self.display_file_info(file_path)

    def display_file_info(self, file_path):
        logging.info(f"TextToolsTab: Displaying file info for {file_path}.")
        # Clear previous dynamic widgets
        while self.dynamic_area.count():
            child = self.dynamic_area.takeAt(0).widget()
            if child:
                child.deleteLater()

        # Thumbnail/Icon
        file_ext = os.path.splitext(file_path)[1].lower()
        if file_ext in [".png", ".jpg", ".jpeg", ".bmp", ".gif"]:
            pixmap = QPixmap(file_path).scaled(100, 100, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
        else:
            # Use generic icons for non-image files
            icon_path = os.path.join(os.path.dirname(__file__), "assets", "file_icon.png")
            if not os.path.exists(icon_path):
                icon_path = ""
            pixmap = QPixmap(icon_path if os.path.exists(icon_path) else "").scaled(100, 100, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
            if pixmap.isNull():
                # Create a placeholder pixmap
                pixmap = QPixmap(100, 100)
                pixmap.fill(QColor(200, 200, 200))
                painter = QPainter(pixmap)
                painter.setPen(QColor(0, 0, 0))
                painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, file_ext.upper())
                painter.end()

        self.thumbnail_label = QLabel()
        self.thumbnail_label.setPixmap(pixmap)
        self.thumbnail_label.setFixedSize(100, 100)
        self.dynamic_area.addWidget(self.thumbnail_label)

        # File Name
        self.file_name_label = QLabel(os.path.basename(file_path))
        self.file_name_label.setFont(QFont("Arial", 10))
        self.dynamic_area.addWidget(self.file_name_label)

        # Spacer
        self.dynamic_area.addStretch()

        # Message Label
        self.message_label = QLabel()
        self.dynamic_area.addWidget(self.message_label)

    def select_model(self, index):
        if index == 0:
            logging.info("TextToolsTab: No model selected.")
            return
        model_name = self.model_combo.currentText()
        model_entry = next((m for m in self.parent_app.model_history if m.get("model_name") == model_name), None)
        if model_entry:
            model_path = model_entry.get("model_path")
            if model_path:
                logging.info(f"TextToolsTab: Loading model '{model_name}' from {model_path}.")
                try:
                    self.model = Llama(model_path=model_path, n_ctx=1000000)  # Setting a very high context length
                    logging.info(f"TextToolsTab: Model '{model_name}' loaded successfully with unlimited context length.")
                except Exception as e:
                    logging.error(f"TextToolsTab: Failed to load model '{model_name}': {str(e)}", exc_info=True)
                    QMessageBox.critical(self, "Error", f"Failed to load model '{model_name}': {str(e)}")
            else:
                logging.warning("TextToolsTab: Selected model does not have a valid path.")
                QMessageBox.warning(self, "Error", "Selected model does not have a valid path.")
        else:
            logging.warning(f"TextToolsTab: Model '{model_name}' not found in history.")
            QMessageBox.warning(self, "Error", f"Model '{model_name}' not found.")

    def modify_and_save(self, action):
        if not self.model:
            logging.warning(f"TextToolsTab: Model not loaded. Cannot perform action '{action}'.")
            QMessageBox.warning(self, "Error", "Please select and load a model first.")
            return
        if not self.current_loaded_file:
            logging.warning(f"TextToolsTab: No file loaded. Cannot perform action '{action}'.")
            QMessageBox.warning(self, "Error", "Please load a file first.")
            return
        logging.info(f"TextToolsTab: Performing '{action}' on the loaded file.")
        try:
            with open(self.current_loaded_file, 'r', encoding='utf-8', errors='ignore') as f:
                content = f.read()
            prompt = self.generate_prompt(action, content)
            response = self.model.generate(prompt)
            # Here, assuming response is a single string. Adjust if model returns differently.
            modified_content = response.strip()

            with open(self.current_loaded_file, 'w', encoding='utf-8', errors='ignore') as f:
                f.write(modified_content)

            # Display message
            self.message_label.setText(f"{action} has been affected to the content of this file.")
            logging.info(f"TextToolsTab: Action '{action}' performed and file '{self.current_loaded_file}' updated.")
        except Exception as e:
            logging.error(f"TextToolsTab: Error performing '{action}': {str(e)}", exc_info=True)
            QMessageBox.critical(self, "Error", f"An error occurred while performing '{action}': {str(e)}")

    def generate_new_text_prompt(self, action, content):
        return f"Action: {action}\n\nContent:\n{content}\n\nPlease perform the action above on the content."

    def modify_new_text_prompt(self, action, content):
        return f"Action: {action}\n\nContent:\n{content}\n\nPlease perform the action above on the content."

    def generate_prompt(self, action, content):
        # Customize the prompt based on action
        prompts = {
            "Summarize": f"Please provide a concise summary of the following text:\n\n{content}",
            "Shorten": f"Please shorten the following text while retaining its original meaning:\n\n{content}",
            "Expand": f"Please expand on the following text, providing more details and explanations:\n\n{content}",
            "Proofread": f"Please proofread and correct any grammatical errors in the following text:\n\n{content}",
            "Explain More": f"Please provide a more detailed explanation of the following text:\n\n{content}",
            "Translate To": f"Please translate the following text to English:\n\n{content}"
        }
        return prompts.get(action, f"Please perform the action '{action}' on the following text:\n\n{content}")

    def generate_new_text(self, action):
        if not self.model:
            logging.warning(f"TextToolsTab: Model not loaded. Cannot perform action '{action}'.")
            QMessageBox.warning(self, "Error", "Please select and load a model first.")
            return
        if not self.current_loaded_file and not self.text_input.text().strip():
            logging.warning(f"TextToolsTab: No text provided. Cannot perform action '{action}'.")
            QMessageBox.warning(self, "Error", "Please load a file or enter text to perform actions.")
            return
        logging.info(f"TextToolsTab: Performing '{action}' to generate new text.")
        try:
            if self.current_loaded_file:
                with open(self.current_loaded_file, 'r', encoding='utf-8', errors='ignore') as f:
                    content = f.read()
            else:
                content = self.text_input.text().strip()

            prompt = self.generate_new_text_prompt(action, content)
            response = self.model.generate(prompt)
            generated_text = response.strip()

            # Create a new widget to display the generated text
            generated_widget = QWidget()
            generated_layout = QVBoxLayout()
            generated_label = QLabel(generated_text)
            generated_label.setWordWrap(True)
            generated_label.setFont(QFont("Arial", 12))
            generated_layout.addWidget(generated_label)
            generated_widget.setLayout(generated_layout)

            # Add the generated widget to the dynamic area
            self.dynamic_area.addWidget(generated_widget)
            logging.info(f"TextToolsTab: Generated text from action '{action}' displayed.")
        except Exception as e:
            logging.error(f"TextToolsTab: Error performing '{action}': {str(e)}", exc_info=True)
            QMessageBox.critical(self, "Error", f"An error occurred while performing '{action}': {str(e)}")

# Main ChatHXApp Class with Tabs
class ChatHXApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("ChatHX")
        self.setGeometry(100, 100, 1200, 800)

        # Load settings first
        self.load_settings()

        # Logging
        self.logging_enabled = self.settings.get('logging_enabled', False)
        self.setup_logging()
        logging.debug("Logging setup completed with enabled=%s", self.logging_enabled)

        # Default GUI settings
        self.gui_scale = self.settings.get('gui_scale', 1.15)
        self.dark_mode = True

        # Custom Font
        font_path = os.path.join(os.path.dirname(__file__), "assets", "HX_Sans.ttf")
        self.custom_font = self.load_custom_font(font_path)

        self.setStyleSheet(self.get_stylesheet())
        logging.debug("Custom font set to: %s", self.custom_font)

        self.has_gpu_support = False
        self.n_gpu_layers = 0

        self.llm_model = None
        self.search_engine = openrag_S.LocalSemanticSearchEngine()
        self.model_config = {}
        self.loading_thread = None
        self.loading_progress = None
        self.is_inferencing = False
        self.max_token_length = 1024
        self.context_length = 2048
        self.max_context_length = self.context_length
        self.loaded_model_name = None
        self.last_model_path = self.settings.get('last_model_path', "")
        self.last_dataset_path = self.settings.get('last_dataset_path', "")
        self.current_response_box = None
        self.use_rag = self.settings.get('use_rag', True)  # Flag to indicate whether to use RAG or not

        self.model_history = []
        self.load_history()

        # Create Tabs
        self.tabs = QTabWidget()
        self.chat_tab = QWidget()
        self.text_tools_tab = TextToolsTab(self)
        self.tabs.addTab(self.chat_tab, "Chat")
        self.tabs.addTab(self.text_tools_tab, "Text Tools")

        self.create_chat_tab()
        self.setCentralWidget(self.tabs)

        # Performance metrics
        self.latency = 0
        self.tokens_generated = 0
        self.token_speed = 0
        self.statistics_window = StatisticsWindow()

        # Defaults
        self.temperature = 0.7
        self.top_p = 0.9
        self.top_k = 40

        self.stop_flag = False

        # File context
        self.file_context = None
        self.file_name = None

        # Auto-load the last used model
        if self.last_model_path:
            logging.info("Auto-loading last used model from: %s", self.last_model_path)
            self.model_path.setText(self.last_model_path)
            self.load_model()

        logging.info("ChatHXApp initialized successfully.")

    def load_history(self):
        logging.debug("Loading model history from history.json.")
        history_file = os.path.join(os.path.dirname(__file__), "history.json")
        if os.path.exists(history_file) and os.stat(history_file).st_size > 0:
            try:
                with open(history_file, "r") as f:
                    data = json.load(f)
                    if data:
                        self.model_history = [
                            {"model_name": item.get('model_name'), "model_path": item.get('model_path')} for item in data
                        ]
                        logging.info("Model history loaded from history.json.")
            except json.JSONDecodeError:
                logging.error("Error decoding history.json. File might be corrupted.", exc_info=True)
            except Exception as e:
                logging.error("Unexpected error loading history.json: %s", str(e), exc_info=True)
        else:
            logging.info("No previous history.json found or file is empty.")

    def setup_logging(self):
        if self.logging_enabled:
            logging.basicConfig(
                filename='chathx.log',
                level=logging.DEBUG,  # Set to DEBUG to capture all levels of logs
                format='%(asctime)s - %(levelname)s - %(threadName)s - %(message)s',
                datefmt='%Y-%m-%d %H:%M:%S'
            )
            logging.info("Logging has been enabled.")
        else:
            logging.disable(logging.CRITICAL)
            logging.info("Logging has been disabled.")

    def create_chat_tab(self):
        logging.debug("Creating Chat tab UI.")
        layout = QVBoxLayout()

        # Info widget at the top
        self.info_widget = QLabel()
        self.info_widget.setFont(QFont(self.custom_font, 10))
        self.update_info_widget()
        layout.addWidget(self.info_widget)

        # Title label
        self.title_label = QLabel("ChatHX")
        self.title_label.setFont(QFont(self.custom_font, 24, QFont.Weight.ExtraBold))
        self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        layout.addWidget(self.title_label)

        # Make chat history scrollable
        self.scroll_area = QScrollArea()
        self.scroll_area.setWidgetResizable(True)

        self.chat_history_frame = QWidget()
        self.chat_history_layout = QVBoxLayout()
        self.chat_history_layout.addStretch(1)
        self.chat_history_frame.setLayout(self.chat_history_layout)

        self.scroll_area.setWidget(self.chat_history_frame)
        layout.addWidget(self.scroll_area)

        # Suggested questions
        self.questions_layout = QHBoxLayout()
        self.suggested_questions = [
            "How to get the most out of AI?",
            "How many cores does the new Core Ultra 7 have?",
            "How many Xe2 cores are in the new Core Ultra 7?",
            "What are the benefits of AI for gaming?"
        ]
        self.update_suggested_questions()
        layout.addLayout(self.questions_layout)

        # Input area
        self.input_layout = QHBoxLayout()
        self.input_area = QLineEdit()
        self.input_area.setFont(QFont(self.custom_font, 12))
        self.input_area.setPlaceholderText("Type your message here...")
        self.input_area.returnPressed.connect(self.send_message)
        self.input_area.setFixedHeight(40)
        self.input_layout.addWidget(self.input_area)

        # Add File Button
        self.add_file_button = QPushButton("Add File")
        self.add_file_button.clicked.connect(self.add_file_to_context)
        self.add_file_button.setFixedHeight(40)
        self.input_layout.addWidget(self.add_file_button)

        self.send_button = QPushButton("Send")
        self.send_button.clicked.connect(self.send_message)
        self.send_button.setFixedHeight(40)
        self.input_layout.addWidget(self.send_button)

        layout.addLayout(self.input_layout)

        self.chat_tab.setLayout(layout)
        logging.debug("Chat tab UI created.")

    def update_info_widget(self):
        model_name = self.loaded_model_name or "No model loaded"
        dataset_path = os.path.basename(self.last_dataset_path) if self.last_dataset_path else "No dataset"
        rag_status = "On" if self.use_rag else "Off"
        info_text = f"Model: {model_name} | Dataset: {dataset_path} | RAG: {rag_status}"
        self.info_widget.setText(info_text)
        logging.debug("Info widget updated with: %s", info_text)

    def update_suggested_questions(self):
        logging.debug("Updating suggested questions.")
        # Clear existing buttons
        for i in reversed(range(self.questions_layout.count())):
            widget = self.questions_layout.itemAt(i).widget()
            if widget is not None:
                widget.setParent(None)
                widget.deleteLater()

        # Add new buttons
        for question in self.suggested_questions:
            question_button = QPushButton(question)
            question_button.setFont(QFont(self.custom_font, 10))
            question_button.clicked.connect(lambda _, q=question: self.set_question(q))
            self.questions_layout.addWidget(question_button)
        logging.info("Suggested questions updated.")

    def set_question(self, question):
        logging.info("Setting input area with suggested question: %s", question)
        self.input_area.setText(question)

    def send_message(self):
        user_message = self.input_area.text().strip()
        if not user_message:
            logging.debug("Chat tab: Send message called with empty input.")
            return

        logging.info("User message sent: %s", user_message)
        if not self.llm_model:
            logging.warning("Chat tab: Send message failed - No LLM model loaded.")
            QMessageBox.warning(self, "Error", "Please load an LLM model first.")
            return

        try:
            # Hide the title label when a message is sent
            if self.title_label.isVisible():
                self.title_label.hide()

            # Display user message
            user_message_box = ChatBubble(user_message, is_user=True, file_indicator=self.file_name, custom_font=self.custom_font)
            self.chat_history_layout.addWidget(user_message_box)

            # Create a new message box for the AI response
            self.current_response_box = ChatBubble("", is_user=False, custom_font=self.custom_font)
            self.chat_history_layout.addWidget(self.current_response_box)

            self.input_area.clear()
            self.send_button.setEnabled(False)
            self.is_inferencing = True

            # Start response processing
            self.full_response = ""
            prompt = self.create_prompt(user_message) if self.use_rag else self.create_prompt_without_rag(user_message)

            self.llm_thread = LLMInferenceThread(
                self.rag_llm, prompt, self.context_length,
                self.temperature, self.top_p, self.top_k,
                use_rag=self.use_rag)
            self.llm_thread.token_ready.connect(self.handle_token)
            self.llm_thread.inference_finished.connect(self.process_full_response)
            self.llm_thread.start()

            logging.debug("LLMInferenceThread started.")

        except Exception as e:
            logging.error("Error during processing send_message: %s", str(e), exc_info=True)
            QMessageBox.critical(self, "Error", "An error occurred during processing. Please check the log file for details.")
            logging.debug("Detailed error: %s", e)

    def stop_inference(self):
        if self.is_inferencing and self.llm_thread:
            logging.info("Stopping inference as per user request.")
            self.llm_thread.stop()
            self.is_inferencing = False
            self.send_button.setEnabled(True)
            QMessageBox.information(self, "Stopped", "Inference has been stopped.")
        else:
            logging.debug("Stop inference called, but no inference was ongoing.")

    def handle_token(self, token):
        logging.debug("Handling token: %s", token)
        self.full_response += token
        self.update_chat_history(token, from_user=False)

    def process_full_response(self, latency, tokens_generated):
        logging.info("Processing full response. Latency: %.2f sec, Tokens: %d", latency, tokens_generated)
        self.is_inferencing = False
        self.send_button.setEnabled(True)
        self.llm_thread.inference_finished.disconnect(self.process_full_response)
        self.llm_thread = None

        # Update statistics
        elapsed_time = latency
        self.tokens_generated = tokens_generated
        self.token_speed = tokens_generated / elapsed_time if elapsed_time > 0 else 0

        self.statistics_window.update_metrics(
            self.token_speed, 0, 0, elapsed_time, self.tokens_generated)
        logging.debug("Statistics updated with new metrics.")

        # Extract answer
        try:
            answer, _ = self.extract_answer_and_questions(self.full_response)
            self.current_response_box.setText(answer)
            logging.debug("AI response set in chat history.")
        except Exception as e:
            logging.error("Error processing response: %s", str(e), exc_info=True)
            self.current_response_box.setText(self.full_response)
            QMessageBox.critical(self, "Error", "Error processing response. Please check the log file for details.")

        self.full_response = ""
        self.current_response_box = None

        # Clear file context after processing
        self.file_context = None
        self.file_name = None
        logging.debug("File context cleared after processing response.")

    def update_chat_history(self, text, from_user=False):
        if not self.current_response_box or from_user:
            message_box = ChatBubble(text, is_user=from_user, custom_font=self.custom_font)
            self.chat_history_layout.addWidget(message_box)
            logging.debug("Chat history updated with new message.")
        else:
            current_text = self.current_response_box.text
            self.current_response_box.setText(current_text + text)
            logging.debug("Chat history updated with token.")

        # Scroll to the bottom
        self.scroll_area.verticalScrollBar().setValue(
            self.scroll_area.verticalScrollBar().maximum())
        logging.debug("Scrolled chat history to the bottom.")

    def get_conversation_history(self, max_turns=5):
        history = []
        logging.debug("Retrieving conversation history.")
        # Iterate in reverse to get the latest messages
        for i in reversed(range(self.chat_history_layout.count())):
            widget = self.chat_history_layout.itemAt(i).widget()
            if isinstance(widget, ChatBubble):
                role = "User" if widget.is_user else "Assistant"
                history.append(f"{role}: {widget.text}")
                if len(history) >= max_turns * 2:  # Assuming each turn has a user and assistant message
                    break
        # Reverse to get the correct order
        history.reverse()
        conversation = '\n'.join(history)
        logging.debug("Conversation history retrieved.")
        return conversation

    def create_prompt(self, query):
        logging.debug("Creating prompt with RAG.")
        # Get conversation history
        conversation = self.get_conversation_history()

        # Get context
        context = ""
        if self.file_context:
            context = self.file_context
            logging.debug("Using file context for prompt.")
        else:
            most_similar_file, similarity_score = self.search_engine.search(query)
            logging.debug("Most similar file: %s with similarity: %.2f", most_similar_file, similarity_score)
            if most_similar_file and similarity_score > 0.4:
                with open(most_similar_file, 'r', encoding='utf-8', errors='ignore') as file:
                    context = file.read()
                context = context[:min(int(self.context_length), self.max_context_length - len(query) - 100)]
                self.rag_llm.last_source_file = most_similar_file
                logging.debug("Context loaded from file: %s", most_similar_file)

        prompt = f"""You are ChatHX, an AI assistant. Below is a conversation between a user and you. Use the provided context to answer the user's question.

## Conversation
{conversation}

## Context
{context}

## Question
{query}

## Instructions
- Provide a clear and accurate answer.

## Response Format
Answer: <Your answer here>
"""
        logging.debug("Prompt created with RAG.")
        return prompt

    def create_prompt_without_rag(self, query):
        logging.debug("Creating prompt without RAG.")
        prompt = f"""You are a helpful and informative AI assistant.

Question: {query}

Provide a clear and accurate answer based on your knowledge.

## Response Format
Answer: <Your answer here>
"""
        logging.debug("Prompt created without RAG.")
        return prompt

    def extract_answer_and_questions(self, response):
        logging.debug("Extracting answer from response.")
        answer = "Error: Could not process model output. Please try rephrasing."
        follow_up_questions = []

        try:
            # Use regex to find the JSON block
            json_match = re.search(r"\s*({[\s\S]*?})\s*", response)
            if not json_match:
                # Try to find JSON even without code blocks
                json_match = re.search(r"({[\s\S]*?})", response)
            if json_match:
                json_str = json_match.group(1).strip()
                # Clean up the JSON string
                json_str = json_str.replace('\n', ' ')
                json_str = json_str.replace('“', '"').replace('”', '"')
                json_str = json_str.replace("'", '"')
                logging.debug("JSON string extracted: %s", json_str)
                # Use json.loads with error handling
                try:
                    response_json = json.loads(json_str)
                except json.JSONDecodeError:
                    # Attempt to fix common issues
                    json_str = re.sub(r',\s*}', '}', json_str)
                    json_str = re.sub(r',\s*]', ']', json_str)
                    response_json = json.loads(json_str)
                answer = response_json.get("answer", answer)
                follow_up_questions = response_json.get("follow_up_questions", [])
                logging.debug("Answer extracted: %s", answer)
                logging.debug("Follow-up questions extracted: %s", follow_up_questions)
            else:
                # If no JSON found, display the entire response
                answer = response.strip()
                logging.debug("No JSON found. Using entire response as answer.")
        except Exception as e:
            logging.error("Error extracting information from response: %s", str(e), exc_info=True)
            answer = response.strip()

        return answer, follow_up_questions

    def browse_model(self):
        logging.info("Chat tab: Browse Model button clicked.")
        model_path, _ = QFileDialog.getOpenFileName(self, "Select Model File")
        if model_path:
            self.model_path.setText(model_path)
            logging.info("Model file selected: %s", model_path)

    def load_model(self):
        model_path = self.model_path.text()
        logging.info("Attempting to load model from: %s", model_path)
        if model_path:
            try:
                # Show progress dialog
                self.model_loading_progress = QProgressDialog("Loading Model...", None, 0, 0, self)
                self.model_loading_progress.setWindowModality(Qt.WindowModality.ApplicationModal)
                self.model_loading_progress.setCancelButton(None)
                self.model_loading_progress.setMinimumDuration(0)
                self.model_loading_progress.show()
                logging.debug("Model loading progress dialog shown.")

                # Load the model asynchronously to avoid freezing the UI
                QTimer.singleShot(100, lambda: self._load_model_async(model_path))

            except Exception as e:
                logging.error("Failed to load model: %s", str(e), exc_info=True)
                QMessageBox.critical(self, "Error", f"Failed to load model: {str(e)}")
        else:
            logging.warning("Load model called without specifying a model path.")

        self.update_info_widget()

    def _load_model_async(self, model_path):
        logging.debug("Asynchronous model loading started.")
        try:
            # Model loading code
            model_params = {
                'model_path': model_path,
                'n_ctx': self.context_length
            }
            if self.has_gpu_support:
                model_params['n_gpu_layers'] = self.n_gpu_layers

            self.llm_model = Llama(**model_params)
            self.loaded_model_name = os.path.basename(model_path)
            self.last_model_path = model_path
            logging.info(f"LLM model '{self.loaded_model_name}' loaded successfully.")

            # Update model history
            model_entry = {
                'model_name': self.loaded_model_name,
                'model_path': model_path
            }
            if model_entry not in self.model_history:
                self.model_history.append(model_entry)
                self.model_combo.addItem(self.loaded_model_name)
                logging.debug(f"Model history updated with: {self.loaded_model_name}")
            self.save_history()

            QMessageBox.information(self, "Success", f"LLM model '{self.loaded_model_name}' loaded successfully.")

            # Update the info widget and save settings
            self.update_info_widget()
            self.save_settings()

        except Exception as e:
            logging.error("Exception during model loading: %s", str(e), exc_info=True)
            QMessageBox.critical(self, "Error", f"Failed to load model: {str(e)}")
        finally:
            # Close the progress dialog
            if hasattr(self, 'model_loading_progress'):
                self.model_loading_progress.close()
                logging.debug("Model loading progress dialog closed.")

    def eject_model(self):
        logging.info("Ejecting current model.")
        if self.llm_model:
            self.llm_model = None
            self.loaded_model_name = None
            self.model_path.clear()
            QMessageBox.information(self, "Success", "Model ejected successfully.")
            logging.info("Model ejected successfully.")
        else:
            logging.warning("Eject model called, but no model was loaded.")
            QMessageBox.warning(self, "Info", "No model loaded to eject.")

    def browse_dataset(self):
        logging.info("Chat tab: Browse Dataset button clicked.")
        folder_path = QFileDialog.getExistingDirectory(self, "Select Dataset Folder")
        if folder_path:
            self.dataset_path.setText(folder_path)
            logging.info("Dataset folder selected: %s", folder_path)

    def load_dataset(self):
        dataset_path = self.dataset_path.text()
        logging.info("Attempting to load dataset from: %s", dataset_path)
        if not dataset_path:
            logging.warning("Load dataset called without specifying a dataset path.")
            QMessageBox.warning(self, "Error", "Please select a dataset folder.")
            return

        self.loading_thread = DatasetLoadingThread(dataset_path, self.search_engine)
        self.loading_thread.finished.connect(self.loading_finished)
        self.loading_thread.progress_update.connect(self.update_loading_progress)
        self.loading_thread.error.connect(lambda err: QMessageBox.critical(self, "Error", f"Failed to load dataset: {err}"))
        self.loading_thread.start()
        logging.debug("DatasetLoadingThread started.")

        self.loading_progress = QProgressDialog("Loading Dataset...", "Cancel", 0, 100, self)
        self.loading_progress.setWindowModality(Qt.WindowModality.WindowModal)
        self.loading_progress.show()
        logging.debug("Dataset loading progress dialog shown.")

        self.last_dataset_path = self.dataset_path.text()
        self.update_info_widget()

    def update_loading_progress(self, value):
        if self.loading_progress:
            self.loading_progress.setValue(value)
            logging.debug("Dataset loading progress updated to: %d%%", value)

    def loading_finished(self):
        if self.loading_progress:
            self.loading_progress.close()
            logging.debug("Dataset loading progress dialog closed.")
        self.last_dataset_path = self.dataset_path.text()
        self.save_history()
        QMessageBox.information(self, "Success", "Dataset loaded successfully.")
        logging.info("Dataset loaded successfully.")

    def toggle_rag(self):
        logging.info("Toggling RAG usage to: %s", not self.use_rag)
        self.use_rag = not self.use_rag
        button_text = "Switch to Model Knowledge" if self.use_rag else "Switch to Dataset"
        if hasattr(self, 'settings_window') and self.settings_window.use_model_knowledge_button:
            self.settings_window.use_model_knowledge_button.setText(button_text)
        if self.use_rag and not self.search_engine.corpus:
            logging.warning("RAG enabled but no dataset loaded.")
            QMessageBox.warning(self, "Info", "No dataset loaded. Please load a dataset or switch back to 'Use Model Knowledge'.")
        self.update_info_widget()
        self.save_settings()

    def save_settings(self):
        logging.debug("Saving settings to settings.json.")
        settings = {
            'gui_scale': self.gui_scale,
            'logging_enabled': self.logging_enabled,
            'last_model_path': self.last_model_path,
            'last_dataset_path': self.last_dataset_path,
            'use_rag': self.use_rag,
            # Include other settings as needed
        }
        settings_file = os.path.join(os.path.dirname(__file__), "settings.json")
        try:
            with open(settings_file, "w") as f:
                json.dump(settings, f, indent=4)
            logging.info("Settings saved successfully.")
        except Exception as e:
            logging.error("Error saving settings: %s", str(e), exc_info=True)

    def load_settings(self):
        logging.debug("Loading settings from settings.json.")
        settings_file = os.path.join(os.path.dirname(__file__), "settings.json")
        self.settings = {}
        if os.path.exists(settings_file):
            try:
                with open(settings_file, "r") as f:
                    self.settings = json.load(f)
                logging.info("Settings loaded successfully.")
            except Exception as e:
                logging.error("Error loading settings: %s", str(e), exc_info=True)
        else:
            logging.info("No settings.json found. Using default settings.")

        # Default settings if not loaded
        self.gui_scale = self.settings.get('gui_scale', 1.15)
        self.logging_enabled = self.settings.get('logging_enabled', False)
        self.last_model_path = self.settings.get('last_model_path', "")
        self.last_dataset_path = self.settings.get('last_dataset_path', "")
        self.use_rag = self.settings.get('use_rag', True)

    def toggle_gpu(self, state):
        logging.info("GPU acceleration toggled to: %s", "Enabled" if state else "Disabled")
        self.has_gpu_support = state

    def set_gpu_layers(self, value):
        logging.info("Setting number of GPU layers to: %d", value)
        self.n_gpu_layers = value

    def load_custom_font(self, font_path):
        logging.debug("Loading custom font from: %s", font_path)
        if not os.path.exists(font_path):
            logging.warning("Custom font file not found at: %s. Falling back to Arial.", font_path)
            return "Arial"
        font_id = QFontDatabase.addApplicationFont(font_path)
        if font_id == -1:
            logging.error("Failed to load custom font from: %s. Falling back to Arial.", font_path)
            return "Arial"
        font_family = QFontDatabase.applicationFontFamilies(font_id)[0]
        logging.debug("Custom font loaded: %s", font_family)
        return font_family

    def get_stylesheet(self):
        base_style = """
            QMainWindow, QWidget {
                background-color: #1e1e1e;
                color: #ffffff;
            }
            QLineEdit, QPushButton, QComboBox {
                background-color: #2d2d2d;
                color: #ffffff;
                border: 1px solid #3d3d3d;
                padding: 5px;
            }
            QPushButton {
                background-color: #3d3d3d;
            }
            QPushButton:hover {
                background-color: #4d4d4d;
            }
            QLabel {
                color: #ffffff;
            }
            QScrollArea {
                background-color: #1e1e1e;
                border: none;
            }
            QGroupBox {
                font-weight: bold;
                border: 1px solid #3d3d3d;
                margin-top: 10px;
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                subcontrol-position: top left;
                padding: 0 3px;
            }
        """
        if not self.dark_mode:
            base_style = base_style.replace("#1e1e1e", "#ffffff").replace("#2d2d2d", "#f0f0f0") \
                                     .replace("#3d3d3d", "#d0d0d0").replace("#4d4d4d", "#c0c0c0") \
                                     .replace("#ffffff", "#000000")
        return base_style

    def add_file_to_context(self):
        logging.info("Chat tab: Add File button clicked.")
        # Function to add file content as context
        file_path, _ = QFileDialog.getOpenFileName(self, "Select a File")
        if file_path:
            try:
                # Read the file content
                _, ext = os.path.splitext(file_path)
                ext = ext.lower()

                if ext == ".txt":
                    with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
                        content = f.read()
                elif ext == ".pdf":
                    from pdfminer.high_level import extract_text as pdf_extract_text
                    content = pdf_extract_text(file_path)
                elif ext == ".docx":
                    from docx import Document
                    doc = Document(file_path)
                    content = "\n".join([paragraph.text for paragraph in doc.paragraphs])
                elif ext == ".pptx":
                    from pptx import Presentation
                    prs = Presentation(file_path)
                    text = []
                    for slide in prs.slides:
                        for shape in slide.shapes:
                            if hasattr(shape, "text"):
                                text.append(shape.text)
                    content = "\n".join(text)
                else:
                    logging.warning("Unsupported file format attempted to be added: %s", ext)
                    QMessageBox.warning(self, "Unsupported File", "The selected file format is not supported.")
                    return

                # Store the file content and name
                self.file_context = content
                self.file_name = os.path.basename(file_path)

                # For chat context
                self.rag_llm = openrag_S.RAGEnabledLLM(
                    self.llm_model, self.search_engine,
                    self.max_token_length)

                self.update_info_widget()
                logging.info(f"File '{self.file_name}' added to context.")

            except Exception as e:
                logging.error("Error reading file for context: %s", str(e), exc_info=True)
                QMessageBox.critical(self, "Error", f"An error occurred while reading the file: {str(e)}")
        else:
            logging.debug("Add file to context canceled or no file selected.")

# ChatBubble Class
class ChatBubble(QWidget):
    def __init__(self, text, is_user, metadata=None, file_indicator=None, custom_font=None):
        super().__init__()
        self.is_user = is_user
        self.text = text
        self.metadata = metadata  # Dictionary to hold metadata like latency
        self.file_indicator = file_indicator  # File name if any
        self.custom_font = custom_font
        self.init_ui()
        logging.debug("ChatBubble created. User: %s, Text: %s", self.is_user, self.text)

    def init_ui(self):
        layout = QHBoxLayout()
        if self.is_user:
            layout.addStretch()
            bubble_color = QColor(76, 175, 80)  # Green bubble
        else:
            bubble_color = QColor(66, 133, 244)  # Blue bubble

        # Create a vertical layout to include metadata
        bubble_layout = QVBoxLayout()

        bubble_text = self.text
        if self.file_indicator:
            bubble_text += f"\n[Attached File: {self.file_indicator}]"

        bubble = QLabel(bubble_text)
        bubble.setWordWrap(True)
        bubble.setFont(QFont(self.custom_font, 11))
        bubble.setContentsMargins(10, 10, 10, 10)
        bubble.setStyleSheet(f"background-color: {bubble_color.name()};"
                             "border-radius: 10px; color: white;")
        bubble_layout.addWidget(bubble)

        if self.metadata:
            # Display metadata like response time and source file
            metadata_text = ""
            if "latency" in self.metadata:
                latency = self.metadata["latency"]
                metadata_text += f"Time Taken: {latency:.2f} sec"
            if "source_file" in self.metadata and self.metadata["source_file"]:
                source_file = self.metadata["source_file"]
                metadata_text += f"\nSource: {os.path.basename(source_file)}"
            metadata_label = QLabel(metadata_text)
            metadata_label.setFont(QFont(self.custom_font, 8))
            metadata_label.setStyleSheet("color: gray;")
            bubble_layout.addWidget(metadata_label)

        bubble_widget = QWidget()
        bubble_widget.setLayout(bubble_layout)
        bubble_widget.setStyleSheet("background-color: transparent;")

        # Add shadow effect to bubble_widget
        shadow = QGraphicsDropShadowEffect()
        shadow.setBlurRadius(5)
        shadow.setOffset(0, 0)
        bubble_widget.setGraphicsEffect(shadow)

        layout.addWidget(bubble_widget)
        if not self.is_user:
            layout.addStretch()
        self.setLayout(layout)

    def setText(self, text):
        logging.debug("Updating ChatBubble text from '%s' to '%s'.", self.text, text)
        self.text = text
        bubble_layout = self.layout().itemAt(0).widget().layout()
        bubble_label = bubble_layout.itemAt(0).widget()

        if isinstance(bubble_label, QLabel):
            bubble_label.setText(self.text)
        else:
            logging.warning("Expected QLabel, but got %s", type(bubble_label))

    def setMetadata(self, metadata):
        logging.debug("Updating ChatBubble metadata.")
        self.metadata = metadata
        # Update the metadata label
        bubble_layout = self.layout().itemAt(0).widget().layout()
        if self.metadata:
            metadata_text = ""
            if "latency" in self.metadata:
                latency = self.metadata["latency"]
                metadata_text += f"Time Taken: {latency:.2f} sec"
            if "source_file" in self.metadata and self.metadata["source_file"]:
                source_file = self.metadata["source_file"]
                metadata_text += f"\nSource: {os.path.basename(source_file)}"
            metadata_label = QLabel(metadata_text)
            metadata_label.setFont(QFont(self.custom_font, 8))
            metadata_label.setStyleSheet("color: gray;")
            if bubble_layout.count() >= 2:
                bubble_layout.itemAt(1).widget().deleteLater()
            bubble_layout.addWidget(metadata_label)

# Initialize and Run the Application
if __name__ == "__main__":
    # Initialize logging before creating the application
    logging.basicConfig(
        filename='chathx.log',
        level=logging.DEBUG,  # Set to DEBUG to capture all levels of logs
        format='%(asctime)s - %(levelname)s - %(threadName)s - %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )
    logging.info("Application started.")

    app = QApplication(sys.argv)
    window = ChatHXApp()
    window.show()
    try:
        sys.exit(app.exec())
    except Exception as e:
        logging.critical("Unhandled exception: %s", str(e), exc_info=True)