behebe alle fehler in diesem skript und sorge dafür das das gui vom aufbau und design wie eine chat app ist. Das hautfenster sollte quasi fasst nur aus dem chatfenster bestehen und die meissten interaktiven ui elemente sollten ansonsten bei bedarf ein/auasgeblendet werden können und alles sollte sinnvoll organisiert, modern und intuitiv sein.

    import sys
    import os
    import re
    import subprocess
    import json
    import threading
    import sqlite3
    import time
    import logging
    import requests
    import uuid
    import math

    from PyQt5.QtWidgets import (
        QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit,
        QPushButton, QLabel, QDialog, QFormLayout, QProgressBar, QMessageBox, QComboBox,
        QTextBrowser, QScrollArea, QListWidget, QListWidgetItem, QInputDialog, QTextEdit,
        QGridLayout, QFrame, QSplitter, QTreeWidget, QTreeWidgetItem, QGraphicsView,
        QGraphicsScene, QSlider
    )
    from PyQt5.QtCore import (
        Qt, QTimer, pyqtSignal, QObject, QPointF, QUrl, QRegularExpression
    )
    from PyQt5.QtGui import (
        QTextCursor, QColor, QFont, QTextCharFormat, QSyntaxHighlighter, QPainter, QPen, QBrush,
        QPainterPath
    )

    # ----------------------------
    # Logging Configuration
    # ----------------------------
    logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')

    def find_ollama_executable():
        if os.name == 'nt':  # Windows
            direct_path = r"C:\Users\Admin\AppData\Local\Programs\Ollama\ollama.exe"
            if os.path.exists(direct_path):
                return direct_path

            alt_path = os.path.join("C:", "Users", "Admin", "AppData", "Local", "Programs", "Ollama", "ollama.exe")
            if os.path.exists(alt_path):
                return alt_path

        else:
            # Add Unix-based paths here if needed
            pass

        return None

    # ----------------------------
    # Database Manager
    # ----------------------------

    class DatabaseManager:
        def __init__(self, db_name="autollm_context.db", max_context_length=10000):
            try:
                self.conn = sqlite3.connect(db_name, check_same_thread=False)
                self.max_context_length = max_context_length
                self.create_table()
                logging.info("Database connected and table ensured.")
            except sqlite3.Error as e:
                logging.error(f"Database connection error: {e}")

        def create_table(self):
            try:
                with self.conn:
                    self.conn.execute('''
                        CREATE TABLE IF NOT EXISTS context (
                            id INTEGER PRIMARY KEY AUTOINCREMENT,
                            sender TEXT NOT NULL,
                            message TEXT NOT NULL,
                            timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
                            task_name TEXT
                        )
                    ''')
                logging.debug("Context table ensured.")
            except sqlite3.Error as e:
                logging.error(f"Error creating table: {e}")

        def add_message(self, sender, message, task_name=None):
            try:
                with self.conn:
                    self.conn.execute("INSERT INTO context (sender, message, task_name) VALUES (?, ?, ?)", (sender, message, task_name))
                    logging.debug(f"Message added to context: {sender}: {message[:50]}..., Task: {task_name}")
                    self.ensure_context_limit()
            except sqlite3.Error as e:
                logging.error(f"Error adding message: {e}")

        def get_context(self, max_length=None):
            try:
                if max_length is None:
                    max_length = self.max_context_length
                cursor = self.conn.cursor()
                cursor.execute("SELECT sender, message FROM context ORDER BY id DESC")
                rows = cursor.fetchall()
                context = []
                current_length = 0
                for row in reversed(rows):
                    msg = f"{row[0]}: {row[1]}"
                    msg_length = len(msg)
                    if current_length + msg_length > max_length:
                        break
                    context.insert(0, msg)
                    current_length += msg_length
                context_str = "\n".join(context)
                logging.debug(f"Retrieved context of length {current_length}.")
                return context_str
            except sqlite3.Error as e:
                logging.error(f"Error retrieving context: {e}")
                return ""

        def clear_context(self, specific_ids=None):
            try:
                with self.conn:
                    if specific_ids:
                        placeholders = ','.join('?' for _ in specific_ids)
                        self.conn.execute(f"DELETE FROM context WHERE id IN ({placeholders})", specific_ids)
                        logging.debug(f"Cleared specific context IDs: {specific_ids}.")
                    else:
                        self.conn.execute("DELETE FROM context")
                        logging.debug("Entire context cleared.")
            except sqlite3.Error as e:
                logging.error(f"Error clearing context: {e}")

        def clear_context_by_id(self, context_id):
            try:
                with self.conn:
                    self.conn.execute("DELETE FROM context WHERE id = ?", (context_id,))
                    logging.debug(f"Message with ID {context_id} deleted.")
            except sqlite3.Error as e:
                logging.error(f"Error selectively deleting context: {e}")

        def ensure_context_limit(self):
            try:
                cursor = self.conn.cursor()
                cursor.execute("SELECT SUM(LENGTH(message)) FROM context")
                total_length = cursor.fetchone()[0] or 0
                logging.debug(f"Total context length: {total_length}")
                if total_length > self.max_context_length:
                    # Delete the oldest 100 messages to reduce length
                    cursor.execute("SELECT id FROM context ORDER BY id ASC LIMIT 100")
                    ids_to_delete = [row[0] for row in cursor.fetchall()]
                    self.clear_context(ids_to_delete)
                    logging.info(f"Context limit exceeded. Deleted {len(ids_to_delete)} oldest messages.")
            except sqlite3.Error as e:
                logging.error(f"Error ensuring context limit: {e}")

        def clear_llm_context(self):
            """
            Removes all messages from LLM and system from the context.
            """
            try:
                with self.conn:
                    self.conn.execute("DELETE FROM context WHERE sender IN ('LLM', 'System')")
                    logging.debug("LLM context and system messages cleared.")
            except sqlite3.Error as e:
                logging.error(f"Error clearing LLM context: {e}")

        def close_connection(self):
            try:
                if self.conn:
                    self.conn.close()
                    logging.info("Database connection closed.")
            except sqlite3.Error as e:
                logging.error(f"Error closing database: {e}")

    # ----------------------------
    # Context Manager
    # ----------------------------

    class ContextManager(QObject):
        summarization_requested = pyqtSignal(str)

        def __init__(self, ollama_controller):
            super().__init__()
            self.db = DatabaseManager()
            self.summary = ""
            self.ollama = ollama_controller
            self.ollama.summarization_received.connect(self.handle_summarization_response)
            self.summarization_requested.connect(self.summarize_context)
            self.summarize_request_event = threading.Event()
            self.summarization_result = ""
            self.lock = threading.Lock()
            self.conversation_history = []  # To store chat history
            self.settings = {"temperature": 0.5, "max_new_tokens": 1500}  # Default settings
            self.archive = []  # List of archived contexts
            logging.info("ContextManager initialized.")

        def add_message(self, sender, message, task_name=None):
            self.db.add_message(sender, message, task_name=task_name)
            if sender in ["User", "LLM"]:
                self.conversation_history.append((sender, message, task_name))
            logging.debug(f"Message added: {sender}: {message[:50]}..., Task: {task_name}")
            self.check_and_summarize_context()

        def add_system_message(self, message):
            self.db.add_message("System", message)
            # System messages are not included in conversation_history
            self.check_and_summarize_context()

        def get_context(self):
            with self.lock:
                if self.summary:
                    return self.summary + "\n" + self.db.get_context()
                return self.db.get_context()

        def clear_context(self, context_id=None):
            try:
                if context_id is not None:
                    # Archive context before deleting
                    archived_context = self.archive_context(context_id)
                    if archived_context:
                        self.archive.append(archived_context)
                    # Delete only the message with the specific ID
                    self.db.clear_context_by_id(context_id)
                    logging.info(f"Context with ID {context_id} archived and deleted.")
                else:
                    self.db.clear_context()
                    with self.lock:
                        self.summary = ""
                    logging.info("Entire context cleared.")
            except Exception as e:
                logging.error(f"Error clearing context: {e}")

        def archive_context(self, context_id):
            try:
                cursor = self.db.conn.cursor()
                cursor.execute("SELECT sender, message, timestamp, task_name FROM context WHERE id <= ?", (context_id,))
                rows = cursor.fetchall()
                archived_context = {
                    "id": str(uuid.uuid4()),
                    "messages": [{"sender": row[0], "message": row[1], "timestamp": row[2], "task_name": row[3]} for row in rows],
                    "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
                }
                logging.debug(f"Context archived with ID: {archived_context['id']}.")
                return archived_context
            except sqlite3.Error as e:
                logging.error(f"Error archiving context: {e}")
                return None

        def get_archived_contexts(self):
            return self.archive

        def restore_archive_context(self, archive_id):
            try:
                archived = next((item for item in self.archive if item["id"] == archive_id), None)
                if archived:
                    with self.lock:
                        self.summary = archived["summary"] if "summary" in archived else ""
                    self.db.clear_context()
                    for msg in archived["messages"]:
                        self.db.add_message(msg["sender"], msg["message"], task_name=msg.get("task_name"))
                    self.conversation_history = [(msg["sender"], msg["message"], msg.get("task_name")) for msg in archived["messages"] if msg["sender"] in ["User", "LLM"]]
                    logging.info(f"Archived context with ID {archive_id} restored.")
                    return True
                logging.error(f"Archived context with ID {archive_id} not found.")
                return False
            except Exception as e:
                logging.error(f"Error restoring archived context: {e}")
                return False

        def get_message_id_by_task_id(self, task_id):
            """
            Finds the message ID based on the Task ID.
            """
            for archived in self.archive:
                if archived["id"] == task_id:
                    return archived["id"]
            return None

        def link_final_answer_to_archive(self, final_answer_id, archive_id):
            """
            Links the final answer with an archived context.
            """
            try:
                # Find the archive and add the final answer ID
                archived = next((item for item in self.archive if item["id"] == archive_id), None)
                if archived:
                    archived["final_answer_id"] = final_answer_id
                    logging.debug(f"Final answer ID {final_answer_id} linked with archive ID {archive_id}.")
            except Exception as e:
                logging.error(f"Error linking final answer: {e}")

        def check_and_summarize_context(self):
            context = self.db.get_context()
            if len(context) > self.db.max_context_length * 0.8:
                self.summarization_requested.emit(context)
                logging.info("Summarization requested due to context length.")

        def summarize_context(self, context_text):
            summary_prompt = (
                "Please summarize the following context to make space for new information. "
                "Keep all important details and key points:\n\n"
                f"{context_text}"
            )
            self.summarization_result = ""
            self.summarize_request_event.clear()

            self.ollama.send_request("/api/generate", {
                "model": self.ollama.selected_llm,
                "prompt": summary_prompt,
                "options": {
                    "temperature": self.settings.get("temperature", 0.5),
                    "max_new_tokens": self.settings.get("max_new_tokens", 1000),
                    "stream": False,
                    "format": "json"
                }
            })
            logging.info("Summarization request sent to LLM.")
            threading.Thread(target=self.wait_for_summary, daemon=True).start()

        def wait_for_summary(self):
            if self.summarize_request_event.wait(timeout=60):
                if self.summarization_result:
                    with self.lock:
                        self.summary = self.summarization_result
                    self.archive_context_entire()
                    self.db.clear_context()
                    self.db.add_message("System", f"Summarized previous context:\n{self.summary}")
                    # Restore conversation history after summarization
                    self.restore_conversation_history()
                    logging.info("Summarization completed and context updated.")
            else:
                logging.error("Timeout during context summarization.")
                self.summarize_request_event.set()

        def handle_summarization_response(self, response):
            if "response" in response:
                summary = response["response"].strip()
                if summary:
                    with self.lock:
                        self.summarization_result = summary
                    logging.debug(f"Received summary: {summary[:50]}...")
            else:
                logging.error("No response received for summarization.")
            self.summarize_request_event.set()
            logging.info("Summarization response processed.")

        def archive_context_entire(self):
            """
            Archives the entire current context.
            """
            try:
                cursor = self.db.conn.cursor()
                cursor.execute("SELECT sender, message, timestamp, task_name FROM context")
                rows = cursor.fetchall()
                archived_context = {
                    "id": str(uuid.uuid4()),
                    "messages": [{"sender": row[0], "message": row[1], "timestamp": row[2], "task_name": row[3]} for row in rows],
                    "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
                }
                self.archive.append(archived_context)
                logging.info("Entire context archived.")
            except sqlite3.Error as e:
                logging.error(f"Error archiving entire context: {e}")

        def restore_conversation_history(self):
            """
            Restores conversation history after summarization.
            """
            with self.lock:
                self.conversation_history = [(msg[0], msg[1], msg[2]) for msg in self.conversation_history]
            logging.debug("Conversation history restored.")

        def clear_llm_context(self):
            """
            Removes all messages generated by the LLM and clears the conversation history.
            """
            self.db.clear_llm_context()
            with self.lock:
                self.conversation_history.clear()
            logging.info("LLM context and conversation history cleared.")

    # ----------------------------
    # TaskWidget for Visual Representation
    # ----------------------------

    class TaskWidget(QWidget):
        status_changed = pyqtSignal(str, str)  # task_name, new_status

        STATUS_COLORS = {
            "Pending": "#FFA500",      # Orange
            "Processing": "#1E90FF",   # Dodger Blue
            "Completed": "#32CD32",    # Lime Green
            "Error": "#FF4500"         # Orange Red
        }

        def __init__(self, task_name, goals, status="Pending"):
            super().__init__()
            self.task_name = task_name
            self.goals = goals  # List of goals
            self.status = status
            self.dependencies = []  # List of dependent tasks
            self.init_ui()

        def init_ui(self):
            layout = QVBoxLayout()
            self.name_label = QLabel(f"<b>Task:</b> {self.task_name}")
            goals_formatted = "<br>".join([f"• {goal}" for goal in self.goals])
            self.goal_label = QLabel(f"<b>Goals:</b><br>{goals_formatted}")
            self.status_label = QLabel(f"<b>Status:</b> {self.status}")
            self.set_status(self.status)
            layout.addWidget(self.name_label)
            layout.addWidget(self.goal_label)
            layout.addWidget(self.status_label)
            self.setLayout(layout)
            self.setStyleSheet(f"""
                QWidget {{
                    border: 2px solid {self.STATUS_COLORS.get(self.status, '#FFA500')};
                    border-radius: 5px;
                    padding: 10px;
                    background-color: #3c3f41;
                    min-width: 200px;
                    max-width: 300px;
                }}
            """)
            self.setToolTip(f"Task: {self.task_name}\nStatus: {self.status}")
            self.setFixedWidth(300)  # Fixed width for better arrangement

        def set_status(self, status):
            if status in self.STATUS_COLORS:
                self.status = status
                self.status_label.setText(f"<b>Status:</b> {self.status}")
                self.setStyleSheet(f"""
                    QWidget {{
                        border: 2px solid {self.STATUS_COLORS.get(self.status, '#FFA500')};
                        border-radius: 5px;
                        padding: 10px;
                        background-color: #3c3f41;
                        min-width: 200px;
                        max-width: 300px;
                    }}
                """)
                self.update()
                self.status_changed.emit(self.task_name, self.status)

        def add_dependency(self, task_name):
            if task_name not in self.dependencies:
                self.dependencies.append(task_name)
                logging.debug(f"Added dependency '{task_name}' to task '{self.task_name}'.")

    # ----------------------------
    # Code Highlighter
    # ----------------------------

    class CodeHighlighter(QSyntaxHighlighter):
        def __init__(self, parent=None):
            super().__init__(parent)
            self.highlightingRules = []
            format_keyword = QTextCharFormat()
            format_keyword.setForeground(QColor("orange"))
            keywords = [
                "if", "else", "for", "while", "def", "return", "class", "import", "from",
                "try", "except", "elif", "with", "as", "and", "or", "not", "in", "is", "lambda"
            ]
            for keyword in keywords:
                pattern = QRegularExpression(r"\b" + keyword + r"\b")
                self.highlightingRules.append((pattern, format_keyword))

        def highlightBlock(self, text):
            for pattern, format in self.highlightingRules:
                match_iterator = pattern.globalMatch(text)
                while match_iterator.hasNext():
                    match = match_iterator.next()
                    self.setFormat(match.capturedStart(), match.capturedLength(), format)

    # ----------------------------
    # Ollama Controller
    # ----------------------------

    class OllamaController(QObject):
        server_status_changed = pyqtSignal(bool)
        response_received = pyqtSignal(dict)
        error_occurred = pyqtSignal(str)
        llm_list_updated = pyqtSignal(list)
        summarization_received = pyqtSignal(dict)

        def __init__(self):
            super().__init__()
            self.process = None
            self.selected_llm = None
            self.api_port = 11434
            self.server_started = False
            self.interrupt_event = threading.Event()
            self.thread_lock = threading.Lock()
            self.server_check_thread = threading.Thread(target=self.monitor_server_status, daemon=True)
            self.server_check_thread.start()
            logging.info("OllamaController initialized and server monitoring started.")

        def monitor_server_status(self):
            while True:
                status = self.is_server_running()
                if status != self.server_started:
                    self.server_started = status
                    self.server_status_changed.emit(self.server_started)
                    if self.server_started:
                        logging.info("Ollama server is running.")
                        self.update_llm_list()
                    else:
                        logging.info("Ollama server is stopped.")
                time.sleep(2)  # Poll every 2 seconds

        def update_llm_list(self):
            try:
                response = requests.get(f"http://localhost:{self.api_port}/api/tags", timeout=2)
                response.raise_for_status()
                data = response.json()
                llms = [model["name"] for model in data.get("models", [])]
                logging.debug(f"Installed LLMs: {llms}")
                self.llm_list_updated.emit(llms)
            except requests.exceptions.RequestException as e:
                self.error_occurred.emit(f"Error fetching models: {e}")
                logging.error(f"Error fetching models: {e}")
                self.llm_list_updated.emit([])

        def start_server(self):
            if not self.is_server_running():
                try:
                    ollama_path = find_ollama_executable()
                    if not ollama_path:
                        self.error_occurred.emit("Ollama executable not found.")
                        logging.error("Ollama executable not found.")
                        return

                    startupinfo = None
                    if os.name == 'nt':
                        startupinfo = subprocess.STARTUPINFO()
                        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW

                    self.process = subprocess.Popen(
                        [ollama_path, 'serve'],
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE,
                        shell=False,
                        startupinfo=startupinfo,
                        cwd=os.path.expanduser("~")
                    )
                    logging.info("Starting Ollama server...")

                except Exception as e:
                    self.error_occurred.emit(f"Error starting server: {str(e)}")
                    logging.error(f"Error starting server: {e}")
            else:
                self.error_occurred.emit("Server is already running.")
                logging.warning("Attempted to start server, but it's already running.")

        def stop_server(self):
            if self.is_server_running() and self.process:
                try:
                    self.interrupt_event.set()
                    self.process.terminate()
                    self.process.wait(timeout=10)
                    self.process = None
                    self.selected_llm = None
                    self.llm_list_updated.emit([])
                    logging.info("Ollama server stopped.")
                except Exception as e:
                    self.error_occurred.emit(f"Error stopping server: {e}")
                    logging.error(f"Error stopping server: {e}")
            else:
                self.error_occurred.emit("Server is not running.")
                logging.warning("Attempted to stop server, but it's not running.")

        def get_installed_llms(self):
            try:
                response = requests.get(f"http://localhost:{self.api_port}/api/tags", timeout=2)
                response.raise_for_status()
                data = response.json()
                llms = [model["name"] for model in data.get("models", [])]
                logging.debug(f"Installed LLMs: {llms}")
                return llms
            except requests.exceptions.RequestException as e:
                self.error_occurred.emit(f"Error fetching models: {e}")
                logging.error(f"Error fetching models: {e}")
                return []

        def is_server_running(self):
            try:
                response = requests.get(f"http://localhost:{self.api_port}/api/tags", timeout=2)
                running = response.status_code == 200
                logging.debug(f"Server running: {running}")
                return running
            except Exception:
                logging.debug("Server is not running.")
                return False

        def set_selected_llm(self, llm_name):
            self.selected_llm = llm_name
            logging.info(f"Selected LLM set to: {llm_name}")

        def send_request(self, endpoint, data, stream=False):
            if not self.is_server_running():
                self.error_occurred.emit("Ollama server is not running.")
                logging.warning("Attempted to send request while server is not running.")
                return
            threading.Thread(target=self._handle_request, args=(endpoint, data, stream), daemon=True).start()

        def _handle_request(self, endpoint, data, stream):
            self.interrupt_event.clear()
            try:
                url = f"http://localhost:{self.api_port}{endpoint}"
                headers = {'Content-Type': 'application/json'}
                if stream:
                    response = requests.post(url, json=data, headers=headers, stream=True, timeout=60)
                else:
                    response = requests.post(url, json=data, headers=headers, timeout=60)
                if response.status_code != 200:
                    raise requests.exceptions.HTTPError(f"Status code: {response.status_code}")

                if stream:
                    for line in response.iter_lines():
                        if self.interrupt_event.is_set():
                            raise InterruptedError("User interrupted the request.")
                        if line:
                            decoded_line = line.decode('utf-8')
                            try:
                                data = json.loads(decoded_line)
                                if "error" in data:
                                    raise Exception(f"Ollama API Error: {data['error']}")
                                self.response_received.emit(data)
                            except json.JSONDecodeError as e:
                                self.error_occurred.emit(f"Invalid JSON response: {e}")
                                logging.error(f"Invalid JSON response: {e}")
                                return
                else:
                    try:
                        data = response.json()
                        if "error" in data:
                            raise Exception(f"Ollama API Error: {data['error']}")
                        self.response_received.emit(data)
                    except json.JSONDecodeError as e:
                        self.error_occurred.emit(f"Invalid JSON response: {e}")
                        logging.error(f"Invalid JSON response: {e}")
            except InterruptedError:
                self.error_occurred.emit("Request was interrupted.")
                logging.info("Request was interrupted by the user.")
            except requests.exceptions.RequestException as e:
                self.error_occurred.emit(f"API request failed: {e}")
                logging.error(f"API request failed: {e}")
            except Exception as e:
                self.error_occurred.emit(f"Error processing request: {e}")
                logging.error(f"Error processing request: {e}")

    # ----------------------------
    # Task Manager
    # ----------------------------

    class TaskManager(QObject):
        task_completed = pyqtSignal(str)
        task_failed = pyqtSignal(str)
        progress_updated = pyqtSignal(int, int)  # (completed, total)
        final_answer_ready = pyqtSignal(str)
        message_sent = pyqtSignal(str, str, str)  # (sender, message, message_type)

        # New signals for loading animations
        typing_started = pyqtSignal()
        typing_stopped = pyqtSignal()

        def __init__(self, ollama_controller, context_manager, get_llm_callback):
            super().__init__()
            self.ollama = ollama_controller
            self.context_manager = context_manager
            self.get_llm = get_llm_callback  # Callback to retrieve LLM selection
            self.tasks = {}  # task_name: TaskWidget
            self.completed_tasks = []
            self.failed_tasks = []
            self.current_task = None
            self.total_tasks = 0
            self.completed_count = 0
            self.finalizing = False
            self.final_answer_id = None
            self.ollama.response_received.connect(self.handle_response)
            self.ollama.error_occurred.connect(self.on_error)
            logging.info("TaskManager initialized.")

        def add_task(self, user_request):
            try:
                # Classify the complexity of the request using the LLM
                classification_prompt = (
                    f"Please evaluate the complexity of the following user request. "
                    f"Respond with 'simple' if the request is simple, or 'complex' if the request is complex.\n\n"
                    f"Request: {user_request}\n\nResponse:"
                )
                classification_llm = self.get_llm("classification")
                if not classification_llm:
                    self.message_sent.emit("System", "No classification LLM selected.", "error")
                    logging.error("No classification LLM selected.")
                    return
                self.ollama.set_selected_llm(classification_llm)
                self.ollama.send_request("/api/generate", {
                    "model": classification_llm,
                    "prompt": classification_prompt,
                    "options": {
                        "temperature": 0.0,  # Deterministic
                        "max_new_tokens": 10,
                        "stream": False,
                        "format": "json"
                    }
                })
                self.current_user_request = user_request
                logging.info("Classification prompt sent to LLM.")
                self.typing_started.emit()  # Start loading animation
            except requests.exceptions.RequestException as e:
                self.message_sent.emit("System", f"Error classifying request: {e}", "error")
                logging.error(f"Error classifying request: {e}")

        def handle_response(self, response):
            if self.finalizing:
                self.handle_final_answer(response)
                return

            if not self.current_task:
                # Handling classification response
                if "response" in response:
                    classification = response["response"].strip().lower()
                    logging.debug(f"Classification result: {classification}")
                    if classification == "simple":
                        self.handle_simple_task(self.current_user_request)
                    elif classification == "complex":
                        self.create_subtasks(self.current_user_request)
                    else:
                        self.message_sent.emit("System", "Unclear classification of the request.", "error")
                        logging.error("Unclear classification of the request received.")
                else:
                    self.task_failed.emit("No response received from LLM for classification.")
                    logging.error("No response received for classification.")
                self.typing_stopped.emit()  # Stop loading animation
            else:
                # Handling responses for execution or verification
                if "response" in response:
                    result = response["response"].strip()
                    if self.current_task_type == "execution":
                        self.append_subtask_result(result)
                    elif self.current_task_type == "verification":
                        self.handle_verification_response({"response": result})
                else:
                    self.task_failed.emit("No response received from LLM.")
                    logging.error("No response received during execution.")

        def create_subtasks(self, user_request):
            planning_prompt = (
                "Please create a detailed plan to break down the following request into meaningful subtasks. "
                "For each subtask, define clear goals that must be achieved for the task to be considered successfully completed. "
                "Return the steps in a numbered list format."
                "\n\n"
                f"Request: {user_request}"
            )
            planning_llm = self.get_llm("planning")
            if not planning_llm:
                self.message_sent.emit("System", "No planning LLM selected.", "error")
                logging.error("No planning LLM selected.")
                return
            self.ollama.set_selected_llm(planning_llm)
            self.ollama.send_request("/api/generate", {
                "model": planning_llm,
                "prompt": planning_prompt,
                "options": {
                    "temperature": 0.3,
                    "max_new_tokens": 1500,
                    "stream": False,
                    "format": "json"
                }
            })
            self.total_tasks = 0
            self.completed_count = 0
            self.completed_tasks.clear()
            self.failed_tasks.clear()
            self.current_task = None
            self.finalizing = False
            self.progress_updated.emit(self.completed_count, self.total_tasks)
            self.typing_started.emit()  # Start loading animation
            logging.info("Planning prompt sent to LLM.")

        def parse_planning_response(self, planning_response):
            """
            Parses the planning response to extract subtasks and their goals.
            Expected Format:
            1. Task 1.
            Goals:
                - Goal 1
                - Goal 2
            2. Task 2.
            Goals:
                - Goal 1
            """
            try:
                data = json.loads(planning_response["response"])
                subtasks = data.get("subtasks", [])
                return subtasks
            except json.JSONDecodeError as e:
                self.message_sent.emit("System", f"Error parsing planning response: {e}", "error")
                logging.error(f"Error parsing planning response: {e}")
                return []
            except Exception as e:
                self.message_sent.emit("System", f"Unknown error parsing planning response: {e}", "error")
                logging.error(f"Unknown error parsing planning response: {e}")
                return []

        def create_and_add_task_widget(self, task_name, goals):
            task_widget = TaskWidget(task_name, goals, status="Pending")
            self.tasks[task_name] = task_widget
            logging.debug(f"TaskWidget for '{task_name}' created and added.")
            return task_widget

        def create_subtasks(self, planning_response):
            """
            Creates subtasks based on the LLM's planning response.
            """
            subtasks = self.parse_planning_response(planning_response)
            for subtask in subtasks:
                task_name = subtask.get("task")
                goals = subtask.get("goals", [])
                task_widget = self.create_and_add_task_widget(task_name, goals)
                self.total_tasks += 1
                logging.debug(f"Subtask '{task_name}' with goals {goals} added to TaskManager.")
            self.progress_updated.emit(self.completed_count, self.total_tasks)
            self.process_next_task()
            self.typing_stopped.emit()  # Stop loading animation
            logging.info(f"{self.total_tasks} subtasks created and added to TaskManager.")

        def process_next_task(self):
            if self.current_task is not None:
                return
            if self.completed_count >= self.total_tasks:
                if not self.finalizing:
                    self.finalize()
                return
            # Get the next task from the list
            pending_tasks = [task for task in self.tasks.values() if task.status == "Pending"]
            if not pending_tasks:
                if not self.finalizing:
                    self.finalize()
                return
            self.current_task = pending_tasks[0]
            task_name = self.current_task.task_name
            goals = self.current_task.goals
            logging.info(f"Processing next subtask: {task_name}")

            # Prompt to execute the subtask
            task_prompt = (
                f"Please complete the following task and provide results according to the defined goals.\n\n"
                f"Task: {task_name}\n\n"
                f"Goals:\n" + "\n".join([f"- {goal}" for goal in goals]) + "\n\n"
                f"Results:"
            )
            processing_llm = self.get_llm("processing")
            if not processing_llm:
                self.message_sent.emit("System", "No processing LLM selected.", "error")
                logging.error("No processing LLM selected.")
                return
            self.ollama.set_selected_llm(processing_llm)
            self.current_task_type = "execution"
            self.current_task_name = task_name
            self.ollama.send_request("/api/generate", {
                "model": processing_llm,
                "prompt": task_prompt,
                "options": {
                    "temperature": self.context_manager.settings.get("temperature", 0.5),
                    "max_new_tokens": self.context_manager.settings.get("max_new_tokens", 1500),
                    "stream": False,
                    "format": "json"
                }
            })
            self.typing_started.emit()  # Start loading animation
            logging.info(f"Subtask '{task_name}' prompt sent to LLM.")

        def append_subtask_result(self, result):
            """
            Archives the result of a subtask and manages context accordingly.
            """
            subtask = self.current_task_name
            # Save the task result with the task name
            self.context_manager.add_message("LLM", f"Result of task '{subtask}': {result}", task_name=subtask)
            self.message_sent.emit("LLM", f"Task '{subtask}' completed with result: {result}", "info")
            self.completed_tasks.append(subtask)
            self.completed_count += 1
            self.progress_updated.emit(self.completed_count, self.total_tasks)
            logging.info(f"Subtask '{subtask}' completed with result: {result}")

            # Verification step
            verification_prompt = (
                f"Verify whether the following result meets all the defined goals for the task '{subtask}'.\n\n"
                f"Task: {subtask}\n"
                f"Goals:\n" + "\n".join([f"- {goal}" for goal in self.current_task.goals]) + "\n\n"
                f"Result: {result}\n\n"
                f"Do all goals for this task satisfy the results? Respond with 'Ok to Save' if all goals are met, or provide detailed feedback otherwise."
            )
            verification_llm = self.get_llm("verification")
            if not verification_llm:
                self.message_sent.emit("System", "No verification LLM selected.", "error")
                logging.error("No verification LLM selected.")
                return
            self.ollama.set_selected_llm(verification_llm)
            self.current_task_type = "verification"
            self.current_task_name = subtask
            self.ollama.send_request("/api/generate", {
                "model": verification_llm,
                "prompt": verification_prompt,
                "options": {
                    "temperature": 0.3,
                    "max_new_tokens": 100,
                    "stream": False,
                    "format": "json"
                }
            })
            self.typing_started.emit()  # Start loading animation
            logging.info(f"Verification prompt for task '{subtask}' sent to LLM.")

        def handle_verification_response(self, response):
            """
            Verifies whether the goals of the current task have been met based on the LLM's summary.
            """
            if "response" in response:
                verification_result = response["response"].strip().lower()
                logging.debug(f"Verification result: {verification_result}")
                if verification_result == "ok to save":
                    # Mark task as completed
                    task_name = self.current_task_name
                    self.tasks[task_name].set_status("Completed")
                    self.completed_tasks.append(task_name)
                    self.completed_count += 1
                    self.progress_updated.emit(self.completed_count, self.total_tasks)
                    self.message_sent.emit("System", f"Subtask '{task_name}' successfully completed.", "info")
                    # Check if all tasks are completed
                    if self.completed_count >= self.total_tasks and not self.finalizing:
                        self.finalize()
                    else:
                        self.current_task = None
                        self.process_next_task()
                else:
                    # Goals were not met, repeat the task
                    self.message_sent.emit("System", f"Subtask '{self.current_task_name}' did not meet all goals. Reprocessing...", "warning")
                    # Clear context to remove memory of the previous attempt
                    self.context_manager.clear_llm_context()
                    # Re-add the task to the beginning of the queue for reprocessing
                    self.tasks[self.current_task_name].set_status("Pending")
                    self.process_next_task()
                    logging.info(f"Subtask '{self.current_task_name}' did not meet goals and is being reprocessed.")
            else:
                self.task_failed.emit("No response received from LLM for verification.")
                logging.error("No response received for verification.")
            self.typing_stopped.emit()  # Stop loading animation

        def finalize(self):
            if self.finalizing:
                return
            self.finalizing = True
            final_prompt = (
                "Please summarize the results of all completed tasks and provide a final recommendation or conclusion based on these results.\n\n"
                "Summary of tasks:\n" + "\n".join([f"{i+1}. {task}" for i, task in enumerate(self.completed_tasks)]) + "\n\n"
                "Final recommendation or conclusion:"
            )
            final_llm = self.get_llm("final")
            if not final_llm:
                self.message_sent.emit("System", "No final LLM selected.", "error")
                logging.error("No final LLM selected.")
                return

            self.final_answer_id = str(uuid.uuid4())

            self.current_task_type = "finalization"
            self.current_task_name = "Final Answer"
            self.ollama.set_selected_llm(final_llm)
            self.ollama.send_request("/api/generate", {
                "model": final_llm,
                "prompt": final_prompt,
                "options": {
                    "temperature": 0.3,
                    "max_new_tokens": 1000,
                    "stream": False,
                    "format": "json"
                }
            })
            self.typing_started.emit()  # Start loading animation
            logging.info("Finalization prompt sent to LLM.")

        def handle_final_answer(self, response):
            """
            Handles the final answer from the LLM after all tasks are completed.
            """
            if "response" in response:
                final_answer = response["response"].strip()
                self.final_answer_ready.emit(final_answer)
                self.context_manager.add_message("LLM", final_answer, task_name=self.final_answer_id)
                self.message_sent.emit("LLM", f"Final Answer: {final_answer}", "info")
                logging.info("Final answer received and displayed.")
            else:
                self.task_failed.emit("No final answer received from LLM.")
                logging.error("No final answer received from LLM.")
            self.typing_stopped.emit()  # Stop loading animation

        def handle_simple_task(self, message):
            """
            Handles simple user messages by directly getting a response from the LLM.
            """
            direct_prompt = (
                f"Respond clearly and concisely to the following user request:\n\n"
                f"{message}\n\nResponse:"
            )
            processing_llm = self.get_llm("processing")
            if not processing_llm:
                self.message_sent.emit("System", "No processing LLM selected.", "error")
                logging.error("No processing LLM selected for simple message.")
                return
            self.ollama.set_selected_llm(processing_llm)
            self.current_task_type = "simple_response"
            self.current_task_name = "Simple Response"
            self.ollama.send_request("/api/generate", {
                "model": processing_llm,
                "prompt": direct_prompt,
                "options": {
                    "temperature": self.context_manager.settings["temperature"],
                    "max_new_tokens": self.context_manager.settings["max_new_tokens"],
                    "stream": False,
                    "format": "json"
                }
            })
            self.typing_started.emit()  # Start loading animation
            logging.info("Direct response prompt for simple message sent to LLM.")

        def append_subtask_result(self, result):
            """
            Archives the result of a subtask and manages context accordingly.
            """
            subtask = self.current_task_name
            # Save the task result with the task name
            self.context_manager.add_message("LLM", f"Result of task '{subtask}': {result}", task_name=subtask)
            self.message_sent.emit("LLM", f"Task '{subtask}' completed with result: {result}", "info")
            self.completed_tasks.append(subtask)
            self.completed_count += 1
            self.progress_updated.emit(self.completed_count, self.total_tasks)
            logging.info(f"Subtask '{subtask}' completed with result: {result}")

            # Verification step
            verification_prompt = (
                f"Verify whether the following result meets all the defined goals for the task '{subtask}'.\n\n"
                f"Task: {subtask}\n"
                f"Goals:\n" + "\n".join([f"- {goal}" for goal in self.tasks[subtask].goals]) + "\n\n"
                f"Result: {result}\n\n"
                f"Do all goals for this task satisfy the results? Respond with 'Ok to Save' if all goals are met, or provide detailed feedback otherwise."
            )
            verification_llm = self.get_llm("verification")
            if not verification_llm:
                self.message_sent.emit("System", "No verification LLM selected.", "error")
                logging.error("No verification LLM selected.")
                return
            self.ollama.set_selected_llm(verification_llm)
            self.current_task_type = "verification"
            self.current_task_name = subtask
            self.ollama.send_request("/api/generate", {
                "model": verification_llm,
                "prompt": verification_prompt,
                "options": {
                    "temperature": 0.3,
                    "max_new_tokens": 100,
                    "stream": False,
                    "format": "json"
                }
            })
            self.typing_started.emit()  # Start loading animation
            logging.info(f"Verification prompt for task '{subtask}' sent to LLM.")

        def handle_final_answer(self, response):
            """
            Handles the final answer from the LLM after all tasks are completed.
            """
            if "response" in response:
                final_answer = response["response"].strip()
                self.final_answer_ready.emit(final_answer)
                self.context_manager.add_message("LLM", final_answer, task_name=self.final_answer_id)
                self.message_sent.emit("LLM", f"Final Answer: {final_answer}", "info")
                logging.info("Final answer received and displayed.")
            else:
                self.task_failed.emit("No final answer received from LLM.")
                logging.error("No final answer received from LLM.")
            self.typing_stopped.emit()  # Stop loading animation

        def on_error(self, error_message):
            logging.error(f"Error occurred: {error_message}")
            if self.finalizing:
                # Error during verification or finalization
                if "save" in error_message.lower():
                    self.task_failed.emit(f"Error saving summary: {error_message}")
            elif self.current_task:
                # Error during task execution or verification
                self.failed_tasks.append(self.current_task.task_name)
                self.completed_count += 1
                self.progress_updated.emit(self.completed_count, self.total_tasks)
                self.task_failed.emit(f"Error in task '{self.current_task.task_name}': {error_message}")
                self.current_task = None
                self.process_next_task()
            else:
                self.task_failed.emit(f"Error: {error_message}")

    # ----------------------------
    # Settings Dialog
    # ----------------------------

    class SettingsDialog(QDialog):
        settings_updated = pyqtSignal(dict)

        def __init__(self, current_settings, parent=None):
            super().__init__(parent)
            self.setWindowTitle("Settings")
            self.current_settings = current_settings
            self.init_ui()

        def init_ui(self):
            layout = QFormLayout()

            # Temperature Slider
            self.temperature_slider = QSlider(Qt.Horizontal)
            self.temperature_slider.setRange(0, 100)
            self.temperature_slider.setValue(int(self.current_settings.get("temperature", 0.5) * 100))
            self.temperature_slider.valueChanged.connect(self.update_temperature_label)
            self.temperature_label = QLabel(f"{self.current_settings.get('temperature', 0.5):.2f}")
            temp_layout = QHBoxLayout()
            temp_layout.addWidget(self.temperature_slider)
            temp_layout.addWidget(self.temperature_label)
            layout.addRow("Temperature:", temp_layout)

            # Max Tokens Slider
            self.output_slider = QSlider(Qt.Horizontal)
            self.output_slider.setRange(100, 2000)
            self.output_slider.setValue(self.current_settings.get("max_new_tokens", 1500))
            self.output_slider.valueChanged.connect(self.update_output_label)
            self.output_label = QLabel(str(self.current_settings.get("max_new_tokens", 1500)))
            output_layout = QHBoxLayout()
            output_layout.addWidget(self.output_slider)
            output_layout.addWidget(self.output_label)
            layout.addRow("Max Tokens:", output_layout)

            # Additional Options
            self.additional_options_box = QLabel("Additional Options (JSON):")
            self.additional_options_input = QLineEdit()
            self.additional_options_input.setPlaceholderText('e.g., "top_p": 0.9, "frequency_penalty": 1.0')
            layout.addRow(self.additional_options_box, self.additional_options_input)

            # Save and Cancel Buttons
            buttons_layout = QHBoxLayout()
            self.save_button = QPushButton("Save")
            self.save_button.clicked.connect(self.save_settings)
            self.cancel_button = QPushButton("Cancel")
            self.cancel_button.clicked.connect(self.reject)
            buttons_layout.addWidget(self.save_button)
            buttons_layout.addWidget(self.cancel_button)
            layout.addRow(buttons_layout)

            self.setLayout(layout)

        def update_temperature_label(self, value):
            temperature = value / 100
            self.temperature_label.setText(f"{temperature:.2f}")

        def update_output_label(self, value):
            self.output_label.setText(str(value))

        def save_settings(self):
            try:
                additional_text = self.additional_options_input.text().strip()
                additional_options = {}
                if additional_text:
                    additional_options = json.loads("{" + additional_text + "}")
                temperature = self.temperature_slider.value() / 100
                output_length = self.output_slider.value()
                settings = {
                    "temperature": temperature,
                    "max_new_tokens": output_length,
                    "additional_options": additional_options
                }
                self.settings_updated.emit(settings)
                logging.info("Settings updated via SettingsDialog.")
                self.accept()
            except json.JSONDecodeError:
                QMessageBox.warning(self, "Invalid JSON", "Please enter valid JSON for additional options.")
                logging.warning("Invalid JSON entered in SettingsDialog.")

    # ----------------------------
    # Connection Widget for Dependencies
    # ----------------------------

    class ConnectionWidget(QGraphicsView):
        def __init__(self, task_widgets):
            super().__init__()
            self.task_widgets = task_widgets  # Dictionary mapping task_name to TaskWidget
            self.scene = QGraphicsScene(self)
            self.setScene(self.scene)
            self.setMinimumWidth(400)
            self.setRenderHint(QPainter.Antialiasing)
            self.setStyleSheet("background-color: #2b2b2b;")
            logging.info("ConnectionWidget initialized.")

        def update_connections(self):
            self.scene.clear()
            pen = QPen(QColor("white"))
            pen.setWidth(3)  # Thicker lines
            for task_name, widget in self.task_widgets.items():
                for dep in widget.dependencies:
                    if dep in self.task_widgets:
                        start_widget = self.task_widgets[dep]
                        end_widget = widget
                        # Get positions relative to the ConnectionWidget
                        start_global = start_widget.mapToGlobal(start_widget.rect().center())
                        end_global = end_widget.mapToGlobal(end_widget.rect().center())
                        start_pos = self.mapFromGlobal(start_global)
                        end_pos = self.mapFromGlobal(end_global)
                        # Draw arrow line
                        self.draw_arrow(self.scene, QPointF(start_pos.x(), start_pos.y()), QPointF(end_pos.x(), end_pos.y()), pen)
            self.scene.update()
            logging.debug("Connections updated in ConnectionWidget.")

        def draw_arrow(self, scene, start, end, pen):
            line = QPainterPath()
            line.moveTo(start)
            line.lineTo(end)
            scene.addPath(line, pen)

            # Draw arrowhead
            angle = self.calculate_angle(start, end)
            arrow_size = 15  # Increase arrowhead size
            arrow_p1 = end - QPointF(arrow_size * math.cos(angle - 0.5),
                                    arrow_size * math.sin(angle - 0.5))
            arrow_p2 = end - QPointF(arrow_size * math.cos(angle + 0.5),
                                    arrow_size * math.sin(angle + 0.5))
            arrow_head = QPainterPath()
            arrow_head.moveTo(end)
            arrow_head.lineTo(arrow_p1)
            arrow_head.lineTo(arrow_p2)
            arrow_head.closeSubpath()
            scene.addPath(arrow_head, pen, QBrush(QColor("white")))

        def calculate_angle(self, start, end):
            delta = end - start
            angle = math.atan2(delta.y(), delta.x())
            return angle

    # ----------------------------
    # AutoLLM App
    # ----------------------------

    class AutoLLMApp(QWidget):
        loading_signal = pyqtSignal(bool)  # Loading signal for process display

        def __init__(self):
            super().__init__()
            self.setWindowTitle("AutoLLM")
            self.resize(1200, 800)  # Adjusted window size for better overview
            self.ollama = OllamaController()
            self.context_manager = ContextManager(self.ollama)
            self.current_parameters = {"temperature": 0.5, "max_new_tokens": 1500, "additional_options": {}}

            # Initialize UI
            self.init_ui()
            self.ollama.start_server()
            self.enable_ui(False)

            # Task Manager with LLM selection callback
            self.task_manager = TaskManager(self.ollama, self.context_manager, self.get_llm_selection)
            self.task_manager.task_completed.connect(self.on_task_completed_step)
            self.task_manager.task_failed.connect(self.on_task_failed_step)
            self.task_manager.progress_updated.connect(self.update_progress)
            self.task_manager.final_answer_ready.connect(self.display_final_answer)
            self.task_manager.message_sent.connect(self.display_message)

            # Connect new signals for loading animations
            self.task_manager.typing_started.connect(self.start_typing_animation)
            self.task_manager.typing_stopped.connect(self.stop_typing_animation)

            self.ollama.server_status_changed.connect(self.update_server_status)
            self.ollama.error_occurred.connect(self.show_error)
            self.ollama.llm_list_updated.connect(self.update_llm_list)
            self.ollama.response_received.connect(self.task_manager.handle_response)

            self.ollama.summarization_received.connect(self.task_manager.handle_verification_response)

            # Initialize Code Highlighter
            self.highlighter = CodeHighlighter(self.chat_area.document())

            # For blinking "..."
            self.blink_timer = QTimer()
            self.blink_timer.timeout.connect(self.toggle_ellipsis)
            self.ellipsis_visible = True

            # Initialize Archiving UI
            self.init_archive_ui()

            # Connect LinkClicked signal
            self.chat_area.anchorClicked.connect(self.handle_link_click)

            logging.info("AutoLLMApp initialized.")

        def init_ui(self):
            main_layout = QVBoxLayout()

            # Chat area with scrollbar
            self.chat_area = QTextBrowser()
            self.chat_area.setReadOnly(True)
            self.chat_area.setOpenExternalLinks(False)  # Handle links internally
            self.chat_area.setOpenLinks(False)
            self.chat_area.setFont(QFont("Courier", 10))
            self.chat_area.setStyleSheet("""
                QTextBrowser  {
                    background-color: #2b2b2b;
                    color: #ffffff;
                    border: 1px solid #5c5c5c;
                    padding: 5px;
                }
            """)
            main_layout.addWidget(self.chat_area)

            # Input field and Send Button
            input_layout = QHBoxLayout()
            self.input_field = QLineEdit()
            self.input_field.setPlaceholderText("Enter your message here...")
            self.input_field.returnPressed.connect(self.send_message)
            self.send_button = QPushButton("Send")
            self.send_button.clicked.connect(self.send_message)
            input_layout.addWidget(self.input_field)
            input_layout.addWidget(self.send_button)
            main_layout.addLayout(input_layout)

            # Interrupt Button
            self.interrupt_button = QPushButton("Interrupt")
            self.interrupt_button.clicked.connect(self.interrupt_processing)
            self.interrupt_button.setEnabled(False)
            main_layout.addWidget(self.interrupt_button)

            # Server and LLM Controls
            controls_layout = QHBoxLayout()

            # Server Monitoring
            server_layout = QHBoxLayout()
            self.server_status = QLabel("🔴 Server Stopped")
            self.server_button = QPushButton("Start Server")
            self.server_button.clicked.connect(self.toggle_server)
            server_layout.addWidget(self.server_status)
            server_layout.addWidget(self.server_button)
            controls_layout.addLayout(server_layout)

            # LLM Selection for Classification, Planning, Processing, and Final Answer
            llm_selection_layout = QFormLayout()

            self.classification_llm_combo = QComboBox()
            self.classification_llm_combo.setEnabled(False)
            self.classification_llm_combo.setToolTip("Select the LLM for classification")
            llm_selection_layout.addRow("Classification LLM:", self.classification_llm_combo)

            self.planning_llm_combo = QComboBox()
            self.planning_llm_combo.setEnabled(False)
            self.planning_llm_combo.setToolTip("Select the LLM for planning")
            llm_selection_layout.addRow("Planning LLM:", self.planning_llm_combo)

            self.processing_llm_combo = QComboBox()
            self.processing_llm_combo.setEnabled(False)
            self.processing_llm_combo.setToolTip("Select the LLM for processing subtasks")
            llm_selection_layout.addRow("Processing LLM:", self.processing_llm_combo)

            self.verification_llm_combo = QComboBox()
            self.verification_llm_combo.setEnabled(False)
            self.verification_llm_combo.setToolTip("Select the LLM for verifying tasks")
            llm_selection_layout.addRow("Verification LLM:", self.verification_llm_combo)

            self.final_llm_combo = QComboBox()
            self.final_llm_combo.setEnabled(False)
            self.final_llm_combo.setToolTip("Select the LLM for the final answer")
            llm_selection_layout.addRow("Final Answer LLM:", self.final_llm_combo)

            controls_layout.addLayout(llm_selection_layout)

            main_layout.addLayout(controls_layout)

            # Settings Button
            self.settings_button = QPushButton("Settings")
            self.settings_button.clicked.connect(self.open_settings)
            main_layout.addWidget(self.settings_button)

            # Progress Area
            progress_layout = QHBoxLayout()
            self.progress_label = QLabel("Progress:")
            self.progress_bar = QProgressBar()
            self.progress_bar.setValue(0)
            progress_layout.addWidget(self.progress_label)
            progress_layout.addWidget(self.progress_bar)
            main_layout.addLayout(progress_layout)

            # Task Area with dynamic layout
            self.task_area = QGridLayout()
            self.task_area.setSpacing(20)
            self.task_widgets = {}  # Mapping from task_name to TaskWidget

            self.task_container = QWidget()
            self.task_container.setLayout(self.task_area)

            task_scroll_area = QScrollArea()
            task_scroll_area.setWidgetResizable(True)
            task_scroll_area.setWidget(self.task_container)
            main_layout.addWidget(task_scroll_area)

            # Toggle Task Area Display
            tasks_toggle_layout = QHBoxLayout()
            self.tasks_toggle_button = QPushButton("Show Tasks")
            self.tasks_toggle_button.setCheckable(True)
            self.tasks_toggle_button.clicked.connect(self.toggle_task_display)
            tasks_toggle_layout.addWidget(self.tasks_toggle_button)
            main_layout.addLayout(tasks_toggle_layout)

            # ConnectionWidget for Dependencies
            self.connection_widget = ConnectionWidget(self.task_widgets)
            connection_scroll = QScrollArea()
            connection_scroll.setWidgetResizable(True)
            connection_scroll.setWidget(self.connection_widget)
            main_layout.addWidget(connection_scroll)

            # Archiving Area
            self.archive_label = QLabel("Archived Contexts:")
            self.archive_tree = QTreeWidget()
            self.archive_tree.setHeaderLabels(["Summary", "Timestamp"])
            self.archive_tree.itemClicked.connect(self.restore_archived_context)
            main_layout.addWidget(self.archive_label)
            main_layout.addWidget(self.archive_tree)

            # Loading Indicator added to main layout
            self.loading_indicator = QLabel("Loading...")
            self.loading_indicator.setVisible(False)  # Initially hidden
            self.loading_indicator.setStyleSheet("color: yellow; font-weight: bold;")
            main_layout.addWidget(self.loading_indicator)

            self.setLayout(main_layout)

        def init_archive_ui(self):
            """
            Initializes the archiving UI for stored contexts.
            """
            self.update_archive_tree()
            logging.info("Archiving UI initialized.")

        def update_archive_tree(self):
            """
            Updates the archive tree with stored contexts.
            """
            self.archive_tree.clear()
            archived_contexts = self.context_manager.get_archived_contexts()
            for index, arch in enumerate(archived_contexts):
                summary = "Summary Available" if "summary" in arch else "No Summary"
                item = QTreeWidgetItem([summary, arch["timestamp"]])
                item.setData(0, Qt.UserRole, arch["id"])  # Store the archive ID for restoration
                self.archive_tree.addTopLevelItem(item)
            logging.debug("Archive tree updated.")

        def restore_archived_context(self, item, column):
            """
            Restores an archived context when an entry is clicked.
            """
            archive_id = item.data(0, Qt.UserRole)
            if self.context_manager.restore_archive_context(archive_id):
                self.append_message("System", f"Archived context with ID {archive_id} loaded.", "info")
                self.update_archive_tree()
                logging.info(f"Archived context with ID {archive_id} restored.")
            else:
                self.append_message("System", "Error restoring archived context.", "error")
                logging.error("Error restoring archived context.")

        def enable_ui(self, enabled):
            # Enable or disable UI components based on server and LLM status
            self.classification_llm_combo.setEnabled(enabled)
            self.planning_llm_combo.setEnabled(enabled)
            self.processing_llm_combo.setEnabled(enabled)
            self.verification_llm_combo.setEnabled(enabled)
            self.final_llm_combo.setEnabled(enabled)
            self.send_button.setEnabled(enabled)
            self.input_field.setEnabled(enabled)
            self.interrupt_button.setEnabled(enabled)
            self.settings_button.setEnabled(enabled)
            logging.debug(f"UI enabled: {enabled}")

        def update_server_status(self, status):
            if status:
                self.server_status.setText("🟢 Server Running")
                self.server_button.setText("Stop Server")
                self.enable_ui(True)
                self.tasks_toggle_button.setEnabled(True)
                self.interrupt_button.setEnabled(True)
                logging.info("Server status updated: Running.")
            else:
                self.server_status.setText("🔴 Server Stopped")
                self.server_button.setText("Start Server")
                self.enable_ui(False)
                self.tasks_toggle_button.setEnabled(False)
                self.interrupt_button.setEnabled(False)
                logging.info("Server status updated: Stopped.")

        def update_llm_list(self, llms):
            self.classification_llm_combo.clear()
            self.planning_llm_combo.clear()
            self.processing_llm_combo.clear()
            self.verification_llm_combo.clear()
            self.final_llm_combo.clear()
            self.classification_llm_combo.addItems(llms)
            self.planning_llm_combo.addItems(llms)
            self.processing_llm_combo.addItems(llms)
            self.verification_llm_combo.addItems(llms)
            self.final_llm_combo.addItems(llms)
            if llms:
                self.classification_llm_combo.setCurrentIndex(0)
                self.planning_llm_combo.setCurrentIndex(0)
                self.processing_llm_combo.setCurrentIndex(0)
                self.verification_llm_combo.setCurrentIndex(0)
                self.final_llm_combo.setCurrentIndex(0)
                self.classification_llm_combo.setEnabled(True)
                self.planning_llm_combo.setEnabled(True)
                self.processing_llm_combo.setEnabled(True)
                self.verification_llm_combo.setEnabled(True)
                self.final_llm_combo.setEnabled(True)
            logging.debug(f"LLM lists in UI updated: {llms}")

        def get_llm_selection(self, llm_type):
            """
            Callback method to retrieve the selected LLM based on the type.
            """
            if llm_type == "classification":
                return self.classification_llm_combo.currentText()
            elif llm_type == "planning":
                return self.planning_llm_combo.currentText()
            elif llm_type == "processing":
                return self.processing_llm_combo.currentText()
            elif llm_type == "verification":
                return self.verification_llm_combo.currentText()
            elif llm_type == "final":
                return self.final_llm_combo.currentText()
            logging.warning(f"Invalid LLM type requested: {llm_type}")
            return None

        def send_message(self):
            user_message = self.input_field.text().strip()
            if user_message:
                # Add to context management and conversation history
                self.context_manager.add_message("User", user_message)
                self.append_message("User", user_message)
                self.input_field.clear()

                self.task_manager.add_task(user_message)
                logging.info(f"Message sent: {user_message} for classification.")
                # Loading animation is managed by TaskManager

        def append_message(self, sender, message, message_type="normal"):
            if sender == "User":
                color = "#00ccff"  # Color for user messages
                prefix = "<b>User:</b> "
            elif sender == "LLM":
                color = "green"  # Color for LLM messages
                prefix = "<b>LLM:</b> "
            else:
                color = "gray"  # Color for system messages
                prefix = "<b>System:</b> "

            # Distinguish between different message types
            if message_type == "info":
                color = "yellow"
                prefix = "<b>Info:</b> "
            elif message_type == "error":
                color = "red"
                prefix = "<b>Error:</b> "
            elif message_type == "warning":
                color = "orange"
                prefix = "<b>Warning:</b> "

            formatted_message = self.format_message(message)
            html_message = f'<p style="color: {color};">{prefix}{formatted_message}</p>'
            self.chat_area.insertHtml(html_message + "<br>")
            self.chat_area.moveCursor(QTextCursor.End)

        def format_message(self, message):
            # Convert URLs to clickable links
            message = re.sub(r'(https?://\S+)', r'<a href="\1">\1</a>', message)
            # Format code blocks
            message = re.sub(
                r'```(.*?)```',
                r'<pre style="background-color:#f0f0f0; padding:5px; font-family: monospace; white-space: pre-wrap;"><code>\1</code></pre>',
                message,
                flags=re.DOTALL | re.MULTILINE
            )
            return message

        def display_message(self, sender, message, message_type="normal"):
            self.append_message(sender, message, message_type)

        def interrupt_processing(self):
            self.task_manager.interrupt_processing()
            self.display_message("System", "LLM processing was interrupted.", "warning")
            self.stop_typing_animation()  # Stop loading animation upon interruption
            logging.info("Processing was interrupted by the user.")

        def toggle_server(self):
            if self.ollama.is_server_running():
                reply = QMessageBox.question(self, 'Stop Server',
                                            'Are you sure you want to stop the Ollama server?',
                                            QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
                if reply == QMessageBox.Yes:
                    self.ollama.stop_server()
                    logging.info("User confirmed stopping the server.")
            else:
                self.ollama.start_server()
                logging.info("User initiated starting the server.")

        def open_settings(self):
            settings_dialog = SettingsDialog(self.current_parameters, self)
            settings_dialog.settings_updated.connect(self.update_parameters)
            settings_dialog.exec_()

        def update_parameters(self, params):
            self.current_parameters = params
            self.context_manager.settings.update({
                "temperature": params["temperature"],
                "max_new_tokens": params["max_new_tokens"]
            })
            # Update additional options if needed
            # Optional: Implement the use of additional options
            QMessageBox.information(self, "Settings Updated", "LLM parameters have been updated.")
            logging.info("LLM parameters updated via settings.")

        def on_task_completed_step(self, result):
            self.append_message("LLM", f"Result: {result}", message_type="info")
            self.task_manager.completed_count += 1
            self.update_progress(self.task_manager.completed_count, self.task_manager.total_tasks)
            self.task_manager.current_task = None
            self.task_manager.process_next_task()
            self.stop_typing_animation()  # Stop loading indicator
            self.update_plan_list()
            logging.info("Task completed and UI updated.")

        def on_task_failed_step(self, error):
            self.append_message("System", f"Error: {error}", message_type="error")
            self.task_manager.failed_tasks.append(self.task_manager.current_task.task_name)
            self.task_manager.completed_count += 1
            self.update_progress(self.task_manager.completed_count, self.task_manager.total_tasks)
            self.task_manager.current_task = None
            self.task_manager.process_next_task()
            self.stop_typing_animation()  # Stop loading indicator
            self.update_plan_list()
            logging.error(f"Task failed: {error}")

        def update_progress(self, completed, total):
            if total == 0:
                self.progress_bar.setValue(0)
            else:
                progress = int((completed / total) * 100) if total else 0
                self.progress_bar.setValue(progress)
            self.connection_widget.update_connections()
            logging.debug(f"Progress updated: {completed}/{total}")

        def update_plan_list(self):
            self.clear_task_area()
            # Display all completed tasks
            for index, task in enumerate(self.task_manager.completed_tasks):
                widget = TaskWidget(task, goals=[], status="Completed")
                self.task_area.addWidget(widget, index, 0)
                self.task_widgets[task] = widget
                logging.debug(f"TaskWidget for completed task '{task}' added.")

            # Display all queued tasks
            for task in self.task_manager.tasks.values():
                if task.status == "Pending":
                    widget = task
                    self.task_area.addWidget(widget, self.task_manager.completed_count + 1, 0)
                    self.task_widgets[task.task_name] = widget
                    logging.debug(f"TaskWidget for queued task '{task.task_name}' added.")

            # Display all failed tasks
            for task in self.task_manager.failed_tasks:
                widget = TaskWidget(task, goals=[], status="Error")
                self.task_area.addWidget(widget, self.task_manager.completed_count + 2, 0)
                self.task_widgets[task] = widget
                logging.debug(f"TaskWidget for failed task '{task}' added.")

            # Update Connections
            self.connection_widget.update_connections()

        def clear_task_area(self):
            while self.task_area.count():
                item = self.task_area.takeAt(0)
                widget = item.widget()
                if widget:
                    widget.deleteLater()
            self.task_widgets.clear()
            logging.debug("All TaskWidgets removed from task_area.")

        def display_final_answer(self, answer):
            unique_id = str(uuid.uuid4())  # Generate unique UUID
            self.append_message("LLM", f"Final Answer: {answer}", message_type="info")
            # Store the key along with the answer in the UI
            # Here, you can store the final answer with the unique_id in a mapping if needed
            self.context_manager.add_message("LLM", answer, task_name=unique_id)
            self.stop_typing_animation()  # Stop loading indicator
            logging.info(f"Final answer displayed and linked with ID {unique_id}.")

        def toggle_ellipsis(self):
            # Optional: Implement blinking "..."
            pass

        def start_typing_animation(self):
            self.loading_indicator.setVisible(True)
            self.loading_indicator.setText("Loading...")
            self.blink_timer.start(500)  # Blink every 500ms
            logging.debug("Loading animation started.")

        def stop_typing_animation(self):
            self.loading_indicator.setVisible(False)
            self.blink_timer.stop()
            self.ellipsis_visible = True
            self.loading_indicator.setText("Loading...")
            logging.debug("Loading animation stopped.")

        def toggle_task_display(self):
            """
            Shows or hides the task area.
            """
            if self.tasks_toggle_button.isChecked():
                self.task_container.show()
                self.tasks_toggle_button.setText("Hide Tasks")
                logging.info("Task area displayed.")
            else:
                self.task_container.hide()
                self.tasks_toggle_button.setText("Show Tasks")
                logging.info("Task area hidden.")

        def show_error(self, error_message):
            self.append_message("System", error_message, message_type="error")
            logging.error(f"Error displayed in UI: {error_message}")

        def handle_link_click(self, url):
            """
            Handles clicking on links in the chat display.
            If the link references an archived context, it is loaded.
            """
            href = url.toString()
            # Check if the link has a data-archive-id attribute
            match = re.search(r'data-archive-id="([^"]+)"', href)
            if match:
                archive_id = match.group(1)
                # Find the archived context linked to this final answer
                archived = next((item for item in self.context_manager.get_archived_contexts() if item["id"] == archive_id), None)
                if archived:
                    # Display the archived context in the chat
                    restored_context = "\n".join([f"{msg['sender']}: {msg['message']}" for msg in archived['messages']])
                    self.append_message("System", f"Archived Context:\n{restored_context}", "info")
                    logging.info(f"Archived context with ID {archive_id} loaded and displayed.")
                else:
                    self.append_message("System", "Archived context not found.", "error")
                    logging.error("Archived context not found when clicking on final answer.")

        def closeEvent(self, event):
            # Ensure the database connection is closed
            self.context_manager.db.close_connection()
            event.accept()

    # ----------------------------
    # Main Execution
    # ----------------------------

    if __name__ == "__main__":
        app = QApplication(sys.argv)
        app.setStyleSheet("""
        QWidget {
            background-color: #2b2b2b;
            color: #ffffff;
        }
        QPushButton {
            background-color: #3c3f41;
            border: none;
            padding: 10px;
            color: #ffffff;
            border-radius: 5px;
        }
        QPushButton:hover {
            background-color: #4b4e52;
        }
        QLineEdit {
            background-color: #3c3f41;
            border: 1px solid #5c5c5c;
            padding: 10px;
            color: #ffffff;
        }
        QTextEdit {
            background-color: #2b2b2b;
            color: #ffffff;
            padding: 10px;
        }
        QTextBrowser {
            background-color: #2b2b2b;
            border: 1px solid #5c5c5c;
            padding: 10px;
            color: #ffffff;
        }
        QComboBox {
            background-color: #3c3f41;
            border: 1px solid #5c5c5c;
            padding: 5px;
            color: #ffffff;
        }
        QLabel {
            color: #ffffff;
        }
        QListWidget {
            background-color: #2b2b2b;
            border: 1px solid #5c5c5c;
            color: #ffffff;
        }
        QProgressBar {
            border: 1px solid #5c5c5c;
            text-align: center;
            color: #ffffff;
            height: 20px;
        }
        QProgressBar::chunk {
            background-color: #4caf50;
        }
        QGraphicsView {
            background-color: #2b2b2b;
            border: none;
        }
        QTreeWidget {
            background-color: #2b2b2b;
            border: 1px solid #5c5c5c;
            color: #ffffff;
        }
        QTreeWidget::item:selected {
            background-color: #4b4e52;
        }
        """)
        window = AutoLLMApp()
        window.show()
        sys.exit(app.exec_())