überprüfe nocheinmal alle methoden und logiken auf fehler und behebe alle. nach start der anwendung gibt es diese überall. die bugs die einen start verhindern hab ich gefixt, daher sende ich am schluss den code des etwas verbesserten skripts. Es sind aber immer noch unmengen davon vorhanden. der ollama server scheint letzlich irgendwie zu starten, allerdings führt ein klick auf den server start button zwar dazu das der server startet, aber es erscheint eine meldung ob ich den server wirklich beenden möchte. Die anzeige des seerver wechselt auch nicht auf grün, aber der server scheint eigentlich online zu sein. Nur ist dieser trotzdem nicht verwendbar da meine installierten llms nicht wählbar sind bzw alle ui elemente noch deaktiviert sind, offenbar weil der server nicht als aktiv betrachtet wird. Zudem ist das gesamte gui schlecht aufgebaut und zumindest das hauptfenster sollte von grundauf neu erstellt und erdacht werden. Viele ui elemente machen ersteinmal nicht viel sinn und scheinen anzeigen für informationen von bearbeitungsoprzessen zu sein oder sonstige dinge die für bearbeitungsvorgänge etc gedacht sind. Eigentlich sollte die anwendung aber schon eher in richtuing minimalistisch gestaltet sein, bzw nicht zu extrem, aber halt wie so ein chat programm in der regel ist. es gibt dort nicht unbedingt viele ui elemente die den bildschirm füllen, es gibt allgemein kaum etwas bis auf das chatfenster in dem gut organisierte, gestaltete und durch formatierungen angeordnete texte erscheinen welche zb von person a links am rand erscheinen während die eigenen nachrichten rechts am rand orientiert sind etc. Nenne mir nocheinmal eine sorgfältig bearbeitete version dieser anwendung und bau auf mein bereits etwas verbesserte skript auf.


# English Only: Ensure that all prompts, comments, and outputs are in English to maintain consistency and prevent language switching issues.

import sys
import os
import re
import subprocess
import json
import threading
import sqlite3
import time
import logging
import requests
import uuid  # For unique IDs
import math  # For angle calculations in ConnectionWidget
from PyQt5.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit,
    QPushButton, QLabel, QDialog, QFormLayout, QSlider, QMessageBox, QComboBox,
    QTextBrowser, QScrollArea, QListWidget, QListWidgetItem, QProgressBar, QInputDialog, QTextEdit,
    QGraphicsView, QGraphicsScene, QGridLayout, QFrame, QSplitter, QTreeWidget, QTreeWidgetItem, QTextBrowser
)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QObject, QRegularExpression, QPointF, QUrl
from PyQt5.QtGui import QTextCursor, QColor, QFont, QTextCharFormat, QSyntaxHighlighter, QPainter, QPen, QBrush, QMouseEvent, 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.check_server_status()

    def check_server_status(self):
        try:
            response = requests.get(f"http://localhost:{self.api_port}/api/tags", timeout=2)
            if response.status_code == 200:
                self.server_started = True
                self.server_status_changed.emit(True)
                logging.info("Ollama server is running.")
                self.update_llm_list()
            else:
                self.server_started = False
                self.server_status_changed.emit(False)
                self.error_occurred.emit(f"API check failed: {response.status_code}")
                logging.warning(f"API check failed with status code: {response.status_code}")
        except requests.exceptions.RequestException as e:
            self.server_started = False
            self.server_status_changed.emit(False)
            self.error_occurred.emit(f"Failed to connect to the server: {e}")
            logging.error(f"Connection to server failed: {e}")

    def update_llm_list(self):
        llms = self.get_installed_llms()
        if llms:
            self.llm_list_updated.emit(llms)
            if not self.selected_llm:
                self.selected_llm = llms[0]
            logging.info(f"LLM list updated: {llms}")
        else:
            self.llm_list_updated.emit([])
            self.selected_llm = None
            logging.warning("No LLMs found.")

    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...")
                threading.Thread(target=self.delayed_check, daemon=True).start()
            except Exception as e:
                self.error_occurred.emit(f"Error starting server: {str(e)}")
                logging.error(f"Error starting server: {e}")
        else:
            self.server_started = True
            self.server_status_changed.emit(True)
            self.update_llm_list()

    def delayed_check(self):
        time.sleep(5)
        self.check_server_status()

    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.server_started = False
                self.server_status_changed.emit(False)
                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}")

    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.task_queue_append(task_widget)
            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 task_queue_append(self, task_widget):
        """
        Appends a task widget to the processing queue.
        """
        self.task_queue.append(task_widget)

    def process_next_task(self):
        if self.current_task is not None:
            return
        if not self.task_queue:
            if not self.finalizing:
                self.finalize()
            return
        # Get the next task in the queue
        self.current_task = self.task_queue.pop(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.current_llm = processing_llm
        self.current_task_prompt = task_prompt
        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.current_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.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 not self.task_queue 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.task_queue.insert(0, self.current_task)
                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 handle_simple_response(self, response):
        if "response" in response:
            reply = response["response"].strip()
            self.append_message("LLM", reply)
            # Add to conversation history
            self.context_manager.add_message("LLM", reply)
            logging.info("Response for simple message received.")
        else:
            self.append_message("System", "No response received from LLM.", "error")
            logging.error("No response received for simple message.")
        self.stop_typing_animation()  # Stop loading indicator
        # Disconnect to avoid multiple connections
        self.ollama.response_received.disconnect(self.handle_simple_response)

    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.current_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 subtask '{subtask}' sent to LLM.")

    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 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}")

    def interrupt_processing(self):
        try:
            self.ollama.interrupt_event.set()
            self.message_sent.emit("System", "LLM processing was interrupted.", "warning")
            logging.info("LLM processing was interrupted by the user.")
            self.typing_stopped.emit()  # Stop loading animation upon interruption
        except Exception as e:
            self.message_sent.emit("System", f"Error interrupting the process: {e}", "error")
            logging.error(f"Error interrupting the process: {e}")

# ----------------------------
# 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(2000, 1200)  # Larger window 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 = QHBoxLayout()
        splitter = QSplitter(Qt.Horizontal)

        # Left Side: Chat and Task Area
        left_widget = QWidget()
        left_layout = QVBoxLayout()

        # Chat area with scrollbar
        self.chat_area = QTextBrowser()
        self.chat_area.setReadOnly(True)
        self.chat_area.setOpenExternalLinks(False)  # Add this line to handle links internally
        self.chat_area.setOpenLinks(False)  # Add this line to prevent automatic link opening
        self.chat_area.setFont(QFont("Courier", 10))
        self.chat_area.anchorClicked.connect(self.handle_link_click)  # This should now work
        self.chat_area.setStyleSheet("""
            QTextBrowser  {
                background-color: #2b2b2b;
                color: #ffffff;
                border: 1px solid #5c5c5c;
                padding: 5px;
            }
        """)

        chat_scroll_area = QScrollArea()
        chat_scroll_area.setWidgetResizable(True)
        chat_scroll_area.setWidget(self.chat_area)
        left_layout.addWidget(chat_scroll_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)
        left_layout.addLayout(input_layout)

        # Interrupt Button
        self.interrupt_button = QPushButton("Interrupt")
        self.interrupt_button.clicked.connect(self.interrupt_processing)
        left_layout.addWidget(self.interrupt_button)

        # 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)
        left_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)

        left_layout.addLayout(llm_selection_layout)

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

        # Progress Area with separate indicators for planning and processing
        progress_layout = QVBoxLayout()
        self.progress_label_planning = QLabel("Planning Progress:")
        self.progress_bar_planning = QProgressBar()
        self.progress_bar_planning.setValue(0)
        self.progress_label_processing = QLabel("Processing Progress:")
        self.progress_bar_processing = QProgressBar()
        self.progress_bar_processing.setValue(0)
        progress_layout.addWidget(self.progress_label_planning)
        progress_layout.addWidget(self.progress_bar_planning)
        progress_layout.addWidget(self.progress_label_processing)
        progress_layout.addWidget(self.progress_bar_processing)
        left_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)
        left_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)
        left_layout.addLayout(tasks_toggle_layout)

        left_widget.setLayout(left_layout)
        splitter.addWidget(left_widget)

        # Right Side: Task Dependencies and Archiving
        right_widget = QWidget()
        right_layout = QVBoxLayout()

        # ConnectionWidget for Dependencies
        self.connection_widget = ConnectionWidget(self.task_widgets)
        right_layout.addWidget(self.connection_widget)

        # 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)
        right_layout.addWidget(self.archive_label)
        right_layout.addWidget(self.archive_tree)

        right_widget.setLayout(right_layout)
        splitter.addWidget(right_widget)

        main_layout.addWidget(splitter, 3)

        # 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)
            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)
            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_planning.setValue(0)
            self.progress_bar_processing.setValue(0)
        else:
            planning_progress = int((completed / total) * 100) if total else 0
            processing_progress = planning_progress  # Can be adjusted further

            self.progress_bar_planning.setValue(planning_progress)
            self.progress_bar_processing.setValue(processing_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.task_queue:
            widget = TaskWidget(task.task_name, goals=task.goals, status="Pending")
            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_())