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.
verbesser die anwendung weiter und sorge dafür das diese sowohl auf einfache, intuitive weise zu verwenden ist und dennoch die erweiterten, komplexere "chat" methoden im hintergrund anwendet. Dabei soll natürlich auch stets der nutzer auf anschauliche weise und allgemein moderne art und weise zu den geschehnissen im hintergund wie im vordergrund, informiert werden.

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, QInputDialog, QGridLayout, QGraphicsView,
    QGraphicsScene, QSlider, QSplitter, QSizePolicy
)
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})
                elif self.current_task_type == "simple_response":
                    self.handle_simple_response(result)
            else:
                self.task_failed.emit("No response received from LLM.")
                logging.error("No response received during execution.")

    def handle_simple_response(self, result):
        """
        Handles the response for a simple task.
        """
        self.context_manager.add_message("LLM", result, task_name="simple_response")
        self.message_sent.emit("LLM", result, "info")
        self.completed_count += 1
        self.progress_updated.emit(self.completed_count, self.total_tasks)
        self.current_task = None
        self.typing_stopped.emit()
        logging.info("Simple task response handled.")

    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"
            }
        })
        # Resetting task counts
        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:
            # Assuming the response is a plain text with numbered list
            response_text = planning_response["response"]
            subtasks = []
            task_blocks = re.split(r'\d+\.\s+', response_text)
            for block in task_blocks[1:]:  # First split is before the first number
                lines = block.strip().split('\n')
                task_name = lines[0].strip().rstrip('.')
                goals = []
                for line in lines[1:]:
                    goal_match = re.match(r'-\s+(.*)', line.strip())
                    if goal_match:
                        goals.append(goal_match.group(1).strip())
                subtasks.append({"task": task_name, "goals": goals})
            return subtasks
        except Exception as e:
            self.message_sent.emit("System", f"Error parsing planning response: {e}", "error")
            logging.error(f"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_from_response(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 create_subtasks(self, planning_response):
        """
        Wrapper to create subtasks; separated for clarity.
        """
        self.create_subtasks_from_response(planning_response)

    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.completed_count += 1  # Adjust completed count since the task is pending again
                self.progress_updated.emit(self.completed_count, self.total_tasks)
                self.current_task = None
                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 on_error(self, error_message):
        logging.error(f"Error occurred: {error_message}")
        if self.finalizing:
            # Error during finalization
            self.task_failed.emit(f"Error during finalization: {error_message}")
            logging.error(f"Error during finalization: {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.message_sent.emit(f"Error in task '{self.current_task.task_name}': {error_message}", "System", "error")
            self.current_task = None
            self.process_next_task()
        else:
            # General error
            self.task_failed.emit(f"Error: {error_message}")
            self.message_sent.emit(f"Error: {error_message}", "System", "error")


# ----------------------------
# 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')
        # Populate existing additional options if any
        additional_options = self.current_settings.get("additional_options", {})
        if additional_options:
            additional_text = ', '.join([f'"{k}": {v}' for k, v in additional_options.items()])
            self.additional_options_input.setText(additional_text)
        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)
                    # Adjust for viewport
                    start_pos = self.mapToScene(start_widget.rect().center())
                    end_pos = self.mapToScene(end_widget.rect().center())
                    # Draw arrow line
                    self.draw_arrow(self.scene, start_pos, end_pos, 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(1400, 900)  # 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()  # Automatically start the server on launch
        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)

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

        # Left Panel: Chat Area
        chat_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;
                border-radius: 5px;
            }
        """)
        chat_layout.addWidget(self.chat_area)

        # Input field and Send Button
        input_layout = QHBoxLayout()
        self.input_field = QLineEdit()
        self.input_field.setPlaceholderText("Geben Sie hier Ihre Nachricht ein...")
        self.input_field.returnPressed.connect(self.send_message)
        self.send_button = QPushButton("Senden")
        self.send_button.clicked.connect(self.send_message)
        input_layout.addWidget(self.input_field)
        input_layout.addWidget(self.send_button)
        chat_layout.addLayout(input_layout)

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

        # Server and LLM Controls
        controls_layout = QHBoxLayout()

        # Server Monitoring
        server_layout = QHBoxLayout()
        self.server_status = QLabel("🔴 Server gestoppt")
        self.server_button = QPushButton("Server starten")
        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("Wählen Sie das LLM für die Klassifikation aus")
        llm_selection_layout.addRow("Klassifikations-LLM:", self.classification_llm_combo)

        self.planning_llm_combo = QComboBox()
        self.planning_llm_combo.setEnabled(False)
        self.planning_llm_combo.setToolTip("Wählen Sie das LLM für die Planung aus")
        llm_selection_layout.addRow("Planungs-LLM:", self.planning_llm_combo)

        self.processing_llm_combo = QComboBox()
        self.processing_llm_combo.setEnabled(False)
        self.processing_llm_combo.setToolTip("Wählen Sie das LLM für die Verarbeitung der Unteraufgaben aus")
        llm_selection_layout.addRow("Verarbeitungs-LLM:", self.processing_llm_combo)

        self.verification_llm_combo = QComboBox()
        self.verification_llm_combo.setEnabled(False)
        self.verification_llm_combo.setToolTip("Wählen Sie das LLM für die Verifizierung der Aufgaben aus")
        llm_selection_layout.addRow("Verifizierungs-LLM:", self.verification_llm_combo)

        self.final_llm_combo = QComboBox()
        self.final_llm_combo.setEnabled(False)
        self.final_llm_combo.setToolTip("Wählen Sie das LLM für die finale Antwort aus")
        llm_selection_layout.addRow("Finales Antwort-LLM:", self.final_llm_combo)

        controls_layout.addLayout(llm_selection_layout)

        chat_layout.addLayout(controls_layout)

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

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

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

        main_layout.addLayout(chat_layout, 3)  # Chat area takes more space

        # Right Panel: Tasks and Archives
        right_panel = QVBoxLayout()

        # 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)
        task_scroll_area.setStyleSheet("""
            QScrollArea {
                background-color: #2b2b2b;
                border: 1px solid #5c5c5c;
                border-radius: 5px;
            }
        """)
        right_panel.addWidget(QLabel("<b>Aufgaben:</b>"))
        right_panel.addWidget(task_scroll_area)

        # Toggle Task Area Display
        tasks_toggle_layout = QHBoxLayout()
        self.tasks_toggle_button = QPushButton("Aufgaben anzeigen/verbergen")
        self.tasks_toggle_button.setCheckable(True)
        self.tasks_toggle_button.setChecked(True)
        self.tasks_toggle_button.clicked.connect(self.toggle_task_display)
        tasks_toggle_layout.addWidget(self.tasks_toggle_button)
        right_panel.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)
        connection_scroll.setStyleSheet("""
            QScrollArea {
                background-color: #2b2b2b;
                border: 1px solid #5c5c5c;
                border-radius: 5px;
            }
        """)
        right_panel.addWidget(QLabel("<b>Aufgabenabhängigkeiten:</b>"))
        right_panel.addWidget(connection_scroll)

        # Archiving Area
        self.archive_label = QLabel("<b>Archivierte Kontexte:</b>")
        self.archive_tree = QTreeWidget()
        self.archive_tree.setHeaderLabels(["Zusammenfassung", "Zeitstempel"])
        self.archive_tree.itemClicked.connect(self.restore_archived_context)
        self.archive_tree.setStyleSheet("""
            QTreeWidget {
                background-color: #2b2b2b;
                border: 1px solid #5c5c5c;
                color: #ffffff;
                border-radius: 5px;
            }
            QTreeWidget::item:selected {
                background-color: #4b4e52;
            }
        """)
        right_panel.addWidget(self.archive_label)
        right_panel.addWidget(self.archive_tree)

        # Add right panel to main layout with resize policy
        main_layout.addLayout(right_panel, 1)

        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 = "Zusammenfassung verfügbar" if "summary" in arch else "Keine Zusammenfassung"
            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"Archivierter Kontext mit ID {archive_id} geladen.", "info")
            self.update_archive_tree()
            logging.info(f"Archived context with ID {archive_id} restored.")
        else:
            self.append_message("System", "Fehler beim Wiederherstellen des archivierten Kontexts.", "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 läuft")
            self.server_button.setText("Server stoppen")
            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 gestoppt")
            self.server_button.setText("Server starten")
            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>Benutzer:</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>Fehler:</b> "
        elif message_type == "warning":
            color = "orange"
            prefix = "<b>Warnung:</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
        )
        # Add clickable archive links if relevant
        # Example: <a href="data-archive-id="UUID">Link</a>
        # Dies kann je nach Bedarf weiter angepasst werden
        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_event.set()
        self.display_message("System", "LLM-Verarbeitung wurde unterbrochen.", "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, 'Server stoppen',
                'Sind Sie sicher, dass Sie den Ollama-Server stoppen möchten?',
                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, "Einstellungen aktualisiert", "LLM-Parameter wurden aktualisiert.")
        logging.info("LLM parameters updated via settings.")

    def on_task_completed_step(self, result):
        self.append_message("LLM", f"Ergebnis: {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_tasks_ui()
        logging.info("Task completed and UI updated.")

    def on_task_failed_step(self, error):
        self.append_message("System", f"Fehler: {error}", message_type="error")
        if self.task_manager.current_task:
            self.task_manager.failed_tasks.append(self.task_manager.current_task.task_name)
            self.task_manager.completed_count += 1
            self.task_manager.current_task = None
        self.update_progress(self.task_manager.completed_count, self.task_manager.total_tasks)
        self.task_manager.process_next_task()
        self.stop_typing_animation()  # Stop loading indicator
        self.update_tasks_ui()
        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_tasks_ui(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"Finale Antwort: {answer}", message_type="info")
        # Store the key along with the answer in the UI
        # Hier können Sie die finale Antwort mit der unique_id in einer Zuordnung speichern, falls erforderlich
        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}.")
        self.update_tasks_ui()

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

    def start_typing_animation(self):
        self.loading_indicator.setVisible(True)
        self.loading_indicator.setText("Lädt...")
        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("Lädt...")
        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("Aufgaben verbergen")
            logging.info("Task area displayed.")
        else:
            self.task_container.hide()
            self.tasks_toggle_button.setText("Aufgaben anzeigen")
            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.
        Falls der Link auf einen archivierten Kontext verweist, wird dieser geladen.
        """
        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"Archivierter Kontext:\n{restored_context}", "info")
                logging.info(f"Archived context with ID {archive_id} loaded and displayed.")
            else:
                self.append_message("System", "Archivierter Kontext nicht gefunden.", "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()
        # Optionally, stop the Ollama server if running
        if self.ollama.is_server_running():
            self.ollama.stop_server()
        event.accept()

    def format_linked_archive(self, archive_id):
        """
        Formats a clickable link for an archived context.
        """
        return f'<a href="data-archive-id={archive_id}">Archivierter Kontext ({archive_id})</a>'


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

def 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;
        border-radius: 5px;
    }
    QTextEdit {
        background-color: #2b2b2b;
        color: #ffffff;
        padding: 10px;
    }
    QTextBrowser {
        background-color: #2b2b2b;
        border: 1px solid #5c5c5c;
        padding: 10px;
        color: #ffffff;
        border-radius: 5px;
    }
    QComboBox {
        background-color: #3c3f41;
        border: 1px solid #5c5c5c;
        padding: 5px;
        color: #ffffff;
        border-radius: 5px;
    }
    QLabel {
        color: #ffffff;
    }
    QListWidget {
        background-color: #2b2b2b;
        border: 1px solid #5c5c5c;
        color: #ffffff;
        border-radius: 5px;
    }
    QProgressBar {
        border: 1px solid #5c5c5c;
        text-align: center;
        color: #ffffff;
        height: 20px;
        border-radius: 10px;
    }
    QProgressBar::chunk {
        background-color: #4caf50;
        border-radius: 10px;
    }
    QGraphicsView {
        background-color: #2b2b2b;
        border: none;
    }
    QTreeWidget {
        background-color: #2b2b2b;
        border: 1px solid #5c5c5c;
        color: #ffffff;
        border-radius: 5px;
    }
    QTreeWidget::item:selected {
        background-color: #4b4e52;
    }
    """)

    window = AutoLLMApp()
    window.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()