#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Ollama Manager - GUI приложение для управления моделями Ollama
Версия: 2.0
"""

import os
import sys
import subprocess
import json
import threading
import shutil
import platform
import logging
import time
import queue
import hashlib
import tempfile
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, TimeoutError
from datetime import datetime
from typing import Optional, List, Dict, Any, Callable
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext, filedialog
import requests
import psutil

# ==================== НАСТРОЙКА ЛОГИРОВАНИЯ ====================
LOG_FILE = Path.home() / '.ollama-manager.log'
LOG_FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'

# Настройка ротации логов
class RotatingFileHandler(logging.Handler):
    def __init__(self, filename, max_bytes=10*1024*1024, backup_count=5):
        super().__init__()
        self.filename = Path(filename)
        self.max_bytes = max_bytes
        self.backup_count = backup_count
        self._rollover()
    
    def _rollover(self):
        if self.filename.exists() and self.filename.stat().st_size > self.max_bytes:
            for i in range(self.backup_count - 1, 0, -1):
                src = self.filename.parent / f"{self.filename.stem}.{i}{self.filename.suffix}"
                dst = self.filename.parent / f"{self.filename.stem}.{i+1}{self.filename.suffix}"
                if src.exists():
                    src.rename(dst)
            self.filename.rename(self.filename.parent / f"{self.filename.stem}.1{self.filename.suffix}")
    
    def emit(self, record):
        self._rollover()
        with open(self.filename, 'a', encoding='utf-8') as f:
            f.write(self.format(record) + '\n')

# Настройка логирования
logger = logging.getLogger('OllamaManager')
logger.setLevel(logging.INFO)

# Консольный обработчик
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter(LOG_FORMAT))
logger.addHandler(console_handler)

# Файловый обработчик с ротацией
file_handler = RotatingFileHandler(LOG_FILE)
file_handler.setFormatter(logging.Formatter(LOG_FORMAT))
logger.addHandler(file_handler)

# ==================== КОНФИГУРАЦИЯ ====================
class Config:
    """Класс для управления конфигурацией приложения"""
    
    CONFIG_FILE = Path.home() / '.ollama-manager-config.json'
    DEFAULTS = {
        'theme': 'clam',
        'gpu_mode': 'auto',
        'check_updates': True,
        'auto_start': False,
        'window_width': 1100,
        'window_height': 700,
        'chat_window_width': 900,
        'chat_window_height': 600,
        'last_model': None,
        'ollama_url': 'http://localhost:11434',
        'max_workers': 4,
        'chat_history_limit': 1000,
        'context_messages': 20
    }
    
    def __init__(self):
        self._config = self._load()
    
    def _load(self) -> Dict:
        """Загрузка конфигурации"""
        if self.CONFIG_FILE.exists():
            try:
                with open(self.CONFIG_FILE) as f:
                    return json.load(f)
            except (json.JSONDecodeError, IOError) as e:
                logger.error(f"Failed to load config: {e}")
        return self.DEFAULTS.copy()
    
    def save(self) -> None:
        """Сохранение конфигурации"""
        try:
            self.CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
            with open(self.CONFIG_FILE, 'w') as f:
                json.dump(self._config, f, indent=2)
        except Exception as e:
            logger.error(f"Failed to save config: {e}")
    
    def get(self, key: str, default=None):
        """Получение значения"""
        return self._config.get(key, self.DEFAULTS.get(key, default))
    
    def set(self, key: str, value) -> None:
        """Установка значения"""
        self._config[key] = value
        self.save()

# ==================== КЛАСС ДЛЯ РАБОТЫ С ИСТОРИЕЙ ====================
class ChatHistoryManager:
    """Менеджер истории чата"""
    
    HISTORY_DIR = Path.home() / '.ollama-chat-history'
    
    def __init__(self, model_name: str, session_name: Optional[str] = None):
        self.model = model_name
        self.session_name = session_name or f"Session_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
        self.history_dir = self.HISTORY_DIR / model_name.replace('/', '_')
        self.history_dir.mkdir(parents=True, exist_ok=True)
        self.history_file = self.history_dir / f'{self.session_name}.json'
        self.messages: List[Dict] = self._load()
        self.is_modified = False
    
    def _load(self) -> List[Dict]:
        """Загрузка истории"""
        if self.history_file.exists():
            try:
                with open(self.history_file, 'r', encoding='utf-8') as f:
                    return json.load(f)
            except Exception as e:
                logger.error(f"Failed to load chat history: {e}")
        return []
    
    def save(self) -> None:
        """Сохранение истории"""
        if not self.is_modified:
            return
        try:
            with open(self.history_file, 'w', encoding='utf-8') as f:
                json.dump(self.messages, f, indent=2, ensure_ascii=False)
            self.is_modified = False
        except Exception as e:
            logger.error(f"Failed to save chat history: {e}")
    
    def add_message(self, role: str, content: str) -> None:
        """Добавление сообщения"""
        self.messages.append({
            'role': role,
            'content': content,
            'timestamp': datetime.now().isoformat()
        })
        self.is_modified = True
        
        # Ограничение размера истории
        limit = 1000
        if len(self.messages) > limit:
            self.messages = self.messages[-limit:]
    
    def clear(self) -> None:
        """Очистка истории"""
        self.messages = []
        self.is_modified = True
    
    def get_context(self, limit: int = 20) -> List[Dict]:
        """Получение контекста"""
        return self.messages[-limit:] if self.messages else []
    
    @classmethod
    def get_sessions(cls, model_name: str) -> List[Dict]:
        """Получение списка сессий"""
        history_dir = cls.HISTORY_DIR / model_name.replace('/', '_')
        if not history_dir.exists():
            return []
        
        sessions = []
        for file in history_dir.glob('*.json'):
            try:
                with open(file, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    if data:
                        sessions.append({
                            'name': file.stem,
                            'file': file,
                            'count': len(data),
                            'created': file.stat().st_mtime
                        })
            except Exception as e:
                logger.error(f"Failed to load session {file}: {e}")
        
        return sorted(sessions, key=lambda x: x['created'], reverse=True)

# ==================== КЛАСС ДЛЯ РАБОТЫ С OLLAMA ====================
class OllamaService:
    """Сервис для работы с Ollama"""
    
    def __init__(self, base_url: str = "http://localhost:11434"):
        self.base_url = base_url
        self._session = None
        self._executor = ThreadPoolExecutor(max_workers=4)
    
    @property
    def session(self) -> requests.Session:
        """Ленивая инициализация сессии"""
        if self._session is None:
            self._session = requests.Session()
            self._session.timeout = 30
        return self._session
    
    def close(self) -> None:
        """Закрытие сессии"""
        if self._session:
            self._session.close()
            self._session = None
        self._executor.shutdown(wait=True)
    
    def list_models(self) -> List[Dict]:
        """Получение списка моделей"""
        try:
            response = self.session.get(f"{self.base_url}/api/tags")
            if response.status_code == 200:
                return response.json().get('models', [])
        except Exception as e:
            logger.error(f"API list models error: {e}")
        return []
    
    def chat(self, model: str, messages: List[Dict], 
             callback: Optional[Callable] = None) -> Optional[str]:
        """Отправка запроса в чат"""
        try:
            data = {
                "model": model,
                "messages": messages,
                "stream": True
            }
            
            response = self.session.post(
                f"{self.base_url}/api/chat", 
                json=data, 
                stream=True
            )
            
            if response.status_code != 200:
                logger.error(f"Chat API error: {response.status_code}")
                return None
            
            full_response = ""
            for line in response.iter_lines():
                if line:
                    try:
                        chunk = json.loads(line)
                        if callback:
                            callback(chunk)
                        if chunk.get('done', False):
                            break
                        if 'message' in chunk and 'content' in chunk['message']:
                            full_response += chunk['message']['content']
                    except json.JSONDecodeError as e:
                        logger.error(f"Invalid JSON: {e}")
                        continue
            
            return full_response
            
        except Exception as e:
            logger.error(f"Chat error: {e}", exc_info=True)
            return None
    
    def pull_model(self, model_name: str, progress_callback: Optional[Callable] = None) -> bool:
        """Загрузка модели"""
        try:
            process = subprocess.Popen(
                ['ollama', 'pull', model_name],
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                bufsize=1
            )
            
            for line in process.stdout:
                if progress_callback and line.strip():
                    progress_callback(line.strip())
            
            process.wait()
            return process.returncode == 0
            
        except Exception as e:
            logger.error(f"Pull model error: {e}")
            return False
    
    def delete_model(self, model_name: str) -> bool:
        """Удаление модели"""
        try:
            result = subprocess.run(
                ['ollama', 'rm', model_name],
                capture_output=True,
                text=True
            )
            return result.returncode == 0
        except Exception as e:
            logger.error(f"Delete model error: {e}")
            return False
    
    @staticmethod
    def is_running() -> bool:
        """Проверка, запущен ли Ollama"""
        try:
            result = subprocess.run(
                ['pgrep', '-x', 'ollama'],
                capture_output=True
            )
            return result.returncode == 0
        except Exception:
            return False
    
    @staticmethod
    def get_version() -> Optional[str]:
        """Получение версии Ollama"""
        try:
            result = subprocess.run(
                ['ollama', '--version'],
                capture_output=True,
                text=True,
                timeout=5
            )
            if result.returncode == 0:
                return result.stdout.strip()
        except Exception as e:
            logger.error(f"Get version error: {e}")
        return None

# ==================== МОНИТОР РЕСУРСОВ ====================
class ResourceMonitor:
    """Мониторинг системных ресурсов"""
    
    def __init__(self, parent, status_var):
        self.parent = parent
        self.status_var = status_var
        self.running = False
        self._update_id = None
        
        self.frame = ttk.Frame(parent)
        
        self.cpu_label = ttk.Label(self.frame, text="CPU: 0%", font=('Arial', 8))
        self.cpu_label.pack(side=tk.LEFT, padx=5)
        
        self.mem_label = ttk.Label(self.frame, text="RAM: 0%", font=('Arial', 8))
        self.mem_label.pack(side=tk.LEFT, padx=5)
        
        self.disk_label = ttk.Label(self.frame, text="Disk: 0%", font=('Arial', 8))
        self.disk_label.pack(side=tk.LEFT, padx=5)
        
        self.ollama_mem_label = ttk.Label(self.frame, text="Ollama: N/A", font=('Arial', 8))
        self.ollama_mem_label.pack(side=tk.LEFT, padx=5)
        
        self.cpu_bar = ttk.Progressbar(self.frame, length=100, mode='determinate')
        self.cpu_bar.pack(side=tk.LEFT, padx=5)
        
        self.mem_bar = ttk.Progressbar(self.frame, length=100, mode='determinate')
        self.mem_bar.pack(side=tk.LEFT, padx=5)
    
    def start(self):
        """Запуск мониторинга"""
        self.running = True
        self._update()
    
    def stop(self):
        """Остановка мониторинга"""
        self.running = False
        if self._update_id:
            self.parent.after_cancel(self._update_id)
    
    def _update(self):
        """Обновление показателей"""
        if not self.running:
            return
        
        try:
            cpu = psutil.cpu_percent()
            self.cpu_label.config(text=f"CPU: {cpu:.0f}%")
            self.cpu_bar['value'] = cpu
            
            mem = psutil.virtual_memory()
            self.mem_label.config(text=f"RAM: {mem.percent:.0f}%")
            self.mem_bar['value'] = mem.percent
            
            disk = psutil.disk_usage('/')
            self.disk_label.config(text=f"Disk: {disk.percent:.0f}%")
            
            ollama_mem = self._get_ollama_memory()
            if ollama_mem:
                self.ollama_mem_label.config(text=f"Ollama: {ollama_mem:.1f} MB")
            else:
                self.ollama_mem_label.config(text="Ollama: N/A")
            
        except Exception as e:
            logger.error(f"Resource monitor error: {e}")
        
        self._update_id = self.parent.after(1000, self._update)
    
    def _get_ollama_memory(self) -> Optional[float]:
        """Получение использования памяти Ollama"""
        try:
            for proc in psutil.process_iter(['pid', 'name', 'memory_info']):
                if proc.info['name'] == 'ollama':
                    return proc.info['memory_info'].rss / (1024 * 1024)
        except Exception:
            pass
        return None

# ==================== GUI ЧАТА ====================
# ==================== ОКНО ЧАТА С УЛУЧШЕННЫМ ВЫВОДОМ ====================
class ChatWindow:
    """Окно чата с боковой панелью и поддержкой Markdown"""
    
    def __init__(self, parent, model: str, ollama_service: OllamaService):
        self.parent = parent
        self.model = model
        self.ollama = ollama_service
        self.current_history = None
        self.chat_histories = {}
        self._update_queue = queue.Queue()
        self._processing = False
        self._code_blocks = {}  # Для хранения информации о блоках кода
        
        # Создаем окно
        self.window = tk.Toplevel(parent.root)
        self.window.title(f"💬 Chat with {model}")
        
        width = parent.config.get('chat_window_width', 900)
        height = parent.config.get('chat_window_height', 600)
        self.window.geometry(f"{width}x{height}")
        self.window.minsize(600, 400)
        self.window.resizable(True, True)
        
        # Сохраняем размер
        def on_resize(e):
            if e.widget == self.window:
                parent.config.set('chat_window_width', e.width)
                parent.config.set('chat_window_height', e.height)
        
        self.window.bind('<Configure>', on_resize)
        self.window.protocol("WM_DELETE_WINDOW", self.on_close)
        
        # Создаем интерфейс
        self.create_widgets()
        
        # Загружаем сессии
        self.load_sessions()
        self.new_session()
        
        # Запускаем обработчик очереди
        self._process_queue()
    
    def create_widgets(self):
        """Создание виджетов"""
        # Основной контейнер
        main_paned = ttk.PanedWindow(self.window, orient=tk.HORIZONTAL)
        main_paned.pack(fill=tk.BOTH, expand=True)
        
        # Левая панель
        left_frame = ttk.Frame(main_paned, width=200)
        main_paned.add(left_frame, weight=1)
        
        # Заголовок
        header_frame = ttk.Frame(left_frame)
        header_frame.pack(fill=tk.X, padx=5, pady=5)
        
        ttk.Label(header_frame, text="📚 Sessions", font=('Arial', 10, 'bold')).pack(side=tk.LEFT)
        
        btn_frame = ttk.Frame(header_frame)
        btn_frame.pack(side=tk.RIGHT)
        ttk.Button(btn_frame, text="➕", width=3, command=self.new_session).pack(side=tk.LEFT, padx=2)
        
        # Список сессий
        sessions_frame = ttk.Frame(left_frame)
        sessions_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        self.sessions_list = tk.Listbox(sessions_frame, font=('Arial', 9))
        self.sessions_list.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        
        sessions_scroll = ttk.Scrollbar(sessions_frame, orient=tk.VERTICAL, 
                                       command=self.sessions_list.yview)
        sessions_scroll.pack(side=tk.RIGHT, fill=tk.Y)
        self.sessions_list.config(yscrollcommand=sessions_scroll.set)
        
        self.sessions_list.bind('<<ListboxSelect>>', self.on_session_select)
        self.sessions_list.bind('<Button-3>', self.show_session_context_menu)
        
        # Контекстное меню
        self.session_context_menu = tk.Menu(self.window, tearoff=0)
        self.session_context_menu.add_command(label="✏️ Rename", command=self.rename_session)
        self.session_context_menu.add_command(label="📤 Export", command=self.export_session)
        self.session_context_menu.add_separator()
        self.session_context_menu.add_command(label="🗑️ Delete", command=self.delete_session)
        
        # Правая панель
        right_frame = ttk.Frame(main_paned)
        main_paned.add(right_frame, weight=4)
        
        # Информация
        info_frame = ttk.Frame(right_frame)
        info_frame.pack(fill=tk.X, padx=5, pady=5)
        
        self.model_label = ttk.Label(info_frame, text=f"🤖 {self.model}", font=('Arial', 10, 'bold'))
        self.model_label.pack(side=tk.LEFT)
        
        self.session_label = ttk.Label(info_frame, text="", font=('Arial', 9, 'italic'))
        self.session_label.pack(side=tk.LEFT, padx=10)
        
        self.status_label = ttk.Label(info_frame, text="🟢 Ready", font=('Arial', 9))
        self.status_label.pack(side=tk.RIGHT)
        
        # Вывод с поддержкой выделения
        output_frame = ttk.Frame(right_frame)
        output_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        # Используем Text с поддержкой выделения
        self.output_text = tk.Text(
            output_frame, 
            wrap=tk.WORD, 
            state=tk.DISABLED,
            font=('Consolas', 10),
            bg='#1a1a2e',
            fg='#e0e0e0',
            selectbackground='#4a4a6e',
            selectforeground='#ffffff',
            insertbackground='#ffffff',
            relief=tk.FLAT,
            borderwidth=0
        )
        
        # Скроллбар
        output_scroll = ttk.Scrollbar(output_frame, orient=tk.VERTICAL, command=self.output_text.yview)
        self.output_text.configure(yscrollcommand=output_scroll.set)
        
        self.output_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        output_scroll.pack(side=tk.RIGHT, fill=tk.Y)
        
        # Настройка тегов для подсветки
        self._setup_text_tags()
        
        # Контекстное меню для текста (копирование/вставка)
        self.text_context_menu = tk.Menu(self.window, tearoff=0)
        self.text_context_menu.add_command(label="📋 Copy", command=self.copy_selected_text)
        self.text_context_menu.add_command(label="📋 Copy All", command=self.copy_all_text)
        self.text_context_menu.add_separator()
        self.text_context_menu.add_command(label="📥 Paste", command=self.paste_from_clipboard)
        self.text_context_menu.add_separator()
        self.text_context_menu.add_command(label="🧹 Clear", command=self.clear_chat)
        
        self.output_text.bind("<Button-3>", self.show_text_context_menu)
        self.output_text.bind("<Control-c>", lambda e: self.copy_selected_text())
        self.output_text.bind("<Control-v>", lambda e: self.paste_from_clipboard())
        
        # Ввод
        input_frame = ttk.Frame(right_frame)
        input_frame.pack(fill=tk.X, padx=5, pady=5)
        
        self.input_entry = ttk.Entry(input_frame)
        self.input_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
        self.input_entry.bind('<Return>', lambda e: self.send_message())
        self.input_entry.bind('<Control-v>', lambda e: self.paste_from_clipboard())
        
        # Кнопки
        chat_btn_frame = ttk.Frame(right_frame)
        chat_btn_frame.pack(fill=tk.X, padx=5, pady=(0, 5))
        
        ttk.Button(chat_btn_frame, text="📤 Send", command=self.send_message).pack(side=tk.LEFT, padx=2)
        ttk.Button(chat_btn_frame, text="🧹 Clear", command=self.clear_chat).pack(side=tk.LEFT, padx=2)
        ttk.Button(chat_btn_frame, text="💾 Save", command=self.save_chat).pack(side=tk.LEFT, padx=2)
        ttk.Button(chat_btn_frame, text="📤 Export", command=self.export_chat).pack(side=tk.LEFT, padx=2)
        
        self.input_entry.focus()
    
    def _setup_text_tags(self):
        """Настройка тегов для форматирования текста"""
        # Основные теги для сообщений
        self.output_text.tag_configure('user', 
            foreground='#4A90D9', 
            font=('Segoe UI', 10, 'bold'),
            spacing1=5,
            spacing3=5
        )
        
        self.output_text.tag_configure('assistant', 
            foreground='#2ECC71', 
            font=('Segoe UI', 10),
            spacing1=5,
            spacing3=5
        )
        
        self.output_text.tag_configure('timestamp', 
            foreground='#95A5A6', 
            font=('Segoe UI', 8, 'italic'),
            spacing1=2
        )
        
        self.output_text.tag_configure('error', 
            foreground='#E74C3C', 
            font=('Segoe UI', 10, 'bold'),
            spacing1=5,
            spacing3=5
        )
        
        self.output_text.tag_configure('system', 
            foreground='#F39C12', 
            font=('Segoe UI', 9, 'italic'),
            spacing1=5,
            spacing3=5
        )
        
        # Теги для Markdown
        self.output_text.tag_configure('bold', 
            font=('Segoe UI', 10, 'bold')
        )
        
        self.output_text.tag_configure('italic', 
            font=('Segoe UI', 10, 'italic')
        )
        
        self.output_text.tag_configure('code', 
            foreground='#FF6B6B',
            font=('Consolas', 9),
            background='#2d2d44',
            spacing1=2,
            spacing2=2,
            spacing3=2,
            relief=tk.RIDGE,
            borderwidth=1
        )
        
        self.output_text.tag_configure('code_block', 
            foreground='#FFD93D',
            font=('Consolas', 9),
            background='#1a1a2e',
            spacing1=5,
            spacing2=5,
            spacing3=5,
            relief=tk.SUNKEN,
            borderwidth=1,
            lmargin1=20,
            lmargin2=20,
            rmargin=20
        )
        
        self.output_text.tag_configure('code_block_bg', 
            background='#0d0d1a',
            spacing1=5,
            spacing2=5,
            spacing3=5
        )
        
        # Тег для заголовков
        self.output_text.tag_configure('heading1', 
            font=('Segoe UI', 16, 'bold'),
            spacing1=10,
            spacing3=10,
            foreground='#FFD700'
        )
        
        self.output_text.tag_configure('heading2', 
            font=('Segoe UI', 14, 'bold'),
            spacing1=8,
            spacing3=8,
            foreground='#FFA500'
        )
        
        self.output_text.tag_configure('heading3', 
            font=('Segoe UI', 12, 'bold'),
            spacing1=5,
            spacing3=5,
            foreground='#FF6B6B'
        )
        
        # Теги для списков
        self.output_text.tag_configure('bullet', 
            lmargin1=30,
            lmargin2=50,
            spacing1=2,
            spacing3=2
        )
        
        self.output_text.tag_configure('number', 
            lmargin1=30,
            lmargin2=50,
            spacing1=2,
            spacing3=2
        )
    
    def show_text_context_menu(self, event):
        """Показать контекстное меню для текста"""
        self.text_context_menu.post(event.x_root, event.y_root)
    
    def copy_selected_text(self):
        """Копировать выделенный текст"""
        try:
            selected = self.output_text.get(tk.SEL_FIRST, tk.SEL_LAST)
            if selected:
                self.window.clipboard_clear()
                self.window.clipboard_append(selected)
                self.status_label.config(text="📋 Copied")
        except tk.TclError:
            # Нет выделенного текста
            pass
    
    def copy_all_text(self):
        """Копировать весь текст чата"""
        content = self.output_text.get('1.0', tk.END)
        if content.strip():
            self.window.clipboard_clear()
            self.window.clipboard_append(content)
            self.status_label.config(text="📋 All copied")
    
    def paste_from_clipboard(self):
        """Вставить из буфера обмена"""
        try:
            text = self.window.clipboard_get()
            if text:
                # Вставляем в поле ввода
                cursor_pos = self.input_entry.index(tk.INSERT)
                current = self.input_entry.get()
                new_text = current[:cursor_pos] + text + current[cursor_pos:]
                self.input_entry.delete(0, tk.END)
                self.input_entry.insert(0, new_text)
                self.input_entry.icursor(cursor_pos + len(text))
                self.status_label.config(text="📥 Pasted")
        except tk.TclError:
            # Нет текста в буфере
            pass
    
    def _process_queue(self):
        """Обработка очереди обновлений UI"""
        try:
            while True:
                func = self._update_queue.get_nowait()
                func()
        except queue.Empty:
            pass
        finally:
            self.window.after(100, self._process_queue)
    
    def _safe_ui_update(self, func):
        """Безопасное обновление UI"""
        self._update_queue.put(func)
    
    def load_sessions(self):
        """Загрузка сессий"""
        self.sessions_list.delete(0, tk.END)
        sessions = ChatHistoryManager.get_sessions(self.model)
        
        if not sessions:
            self.sessions_list.insert(tk.END, "No sessions")
            self.sessions_list.config(state=tk.DISABLED)
            return
        
        self.sessions_list.config(state=tk.NORMAL)
        for session in sessions:
            display_name = session['name']
            if display_name.startswith('Session_'):
                display_name = display_name[8:]
            self.sessions_list.insert(tk.END, display_name)
    
    def new_session(self):
        """Создание новой сессии"""
        timestamp = datetime.now().strftime('%H:%M')
        session_name = f"Chat_{timestamp}"
        
        # Проверка уникальности
        counter = 1
        base_name = session_name
        while True:
            exists = False
            for i in range(self.sessions_list.size()):
                if self.sessions_list.get(i) == session_name:
                    exists = True
                    break
            if not exists:
                break
            session_name = f"{base_name}_{counter}"
            counter += 1
        
        self.current_history = ChatHistoryManager(self.model, session_name)
        self.chat_histories[session_name] = self.current_history
        
        # Обновляем список
        self.sessions_list.config(state=tk.NORMAL)
        if self.sessions_list.size() == 1 and self.sessions_list.get(0) == "No sessions":
            self.sessions_list.delete(0)
        self.sessions_list.insert(tk.END, session_name)
        self.sessions_list.selection_clear(0, tk.END)
        self.sessions_list.selection_set(tk.END)
        
        self.update_chat_display()
        self.session_label.config(text=f"📝 {session_name}")
        self.status_label.config(text="🟢 New session")
    
    def on_session_select(self, event):
        """Выбор сессии"""
        selection = self.sessions_list.curselection()
        if not selection:
            return
        
        session_name = self.sessions_list.get(selection[0])
        if session_name == "No sessions":
            return
        
        # Сохраняем текущую
        if self.current_history and self.current_history.is_modified:
            self.current_history.save()
        
        # Загружаем выбранную
        self.current_history = ChatHistoryManager(self.model, session_name)
        self.chat_histories[session_name] = self.current_history
        self.update_chat_display()
        self.session_label.config(text=f"📝 {session_name}")
        self.status_label.config(text="🟢 Loaded")
    
    def show_session_context_menu(self, event):
        """Показать контекстное меню"""
        selection = self.sessions_list.curselection()
        if not selection:
            return
        
        session_name = self.sessions_list.get(selection[0])
        if session_name == "No sessions":
            return
        
        self.session_context_menu.post(event.x_root, event.y_root)
    
    def rename_session(self):
        """Переименование сессии"""
        selection = self.sessions_list.curselection()
        if not selection:
            return
        
        old_name = self.sessions_list.get(selection[0])
        if old_name == "No sessions":
            return
        
        dialog = tk.Toplevel(self.window)
        dialog.title("Rename Session")
        dialog.geometry("400x120")
        dialog.resizable(False, False)
        
        ttk.Label(dialog, text="New session name:").pack(pady=(15, 5))
        entry = ttk.Entry(dialog, width=40)
        entry.pack(pady=5)
        entry.insert(0, old_name)
        
        def rename():
            new_name = entry.get().strip()
            if not new_name:
                messagebox.showwarning("Warning", "Name cannot be empty")
                return
            
            if new_name == old_name:
                dialog.destroy()
                return
            
            # Проверяем существование
            for i in range(self.sessions_list.size()):
                if self.sessions_list.get(i) == new_name:
                    messagebox.showwarning("Warning", "Session with this name already exists")
                    return
            
            # Переименовываем
            old_history = self.chat_histories.get(old_name)
            if old_history:
                new_history = ChatHistoryManager(self.model, new_name)
                new_history.messages = old_history.messages.copy()
                new_history.save()
                
                if old_history.history_file.exists():
                    old_history.history_file.unlink()
                
                self.chat_histories[new_name] = new_history
                if old_name in self.chat_histories:
                    del self.chat_histories[old_name]
                
                if self.current_history and self.current_history.session_name == old_name:
                    self.current_history = new_history
                
                self.sessions_list.delete(selection[0])
                self.sessions_list.insert(selection[0], new_name)
                self.sessions_list.selection_set(selection[0])
                self.session_label.config(text=f"📝 {new_name}")
                self.status_label.config(text="✅ Renamed")
            
            dialog.destroy()
        
        ttk.Button(dialog, text="Rename", command=rename).pack(pady=10)
        entry.focus()
        entry.select_range(0, tk.END)
    
    def delete_session(self):
        """Удаление сессии"""
        selection = self.sessions_list.curselection()
        if not selection:
            return
        
        session_name = self.sessions_list.get(selection[0])
        if session_name == "No sessions":
            return
        
        if not messagebox.askyesno("Delete Session", f"Delete session '{session_name}'?"):
            return
        
        history = self.chat_histories.get(session_name)
        if history and history.history_file.exists():
            history.history_file.unlink()
        
        if session_name in self.chat_histories:
            del self.chat_histories[session_name]
        
        if self.current_history and self.current_history.session_name == session_name:
            self.current_history = None
        
        self.sessions_list.delete(selection[0])
        
        if self.sessions_list.size() == 0:
            self.sessions_list.insert(tk.END, "No sessions")
            self.sessions_list.config(state=tk.DISABLED)
            self.current_history = None
            self.output_text.config(state=tk.NORMAL)
            self.output_text.delete('1.0', tk.END)
            self.output_text.config(state=tk.DISABLED)
            self.session_label.config(text="No session")
            self.status_label.config(text="⚪ Empty")
        else:
            self.sessions_list.selection_set(0)
            self.on_session_select(None)
        
        self.status_label.config(text="🗑️ Deleted")
    
    def export_session(self):
        """Экспорт сессии"""
        selection = self.sessions_list.curselection()
        if not selection:
            return
        
        session_name = self.sessions_list.get(selection[0])
        if session_name == "No sessions":
            return
        
        history = self.chat_histories.get(session_name)
        if not history or not history.messages:
            messagebox.showinfo("Info", "Session is empty")
            return
        
        file_path = filedialog.asksaveasfilename(
            defaultextension=".txt",
            filetypes=[("Text files", "*.txt"), ("All files", "*.*")],
            initialfile=f"{session_name}.txt"
        )
        
        if file_path:
            try:
                with open(file_path, 'w', encoding='utf-8') as f:
                    f.write(f"Chat Session: {session_name}\n")
                    f.write(f"Model: {self.model}\n")
                    f.write(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
                    f.write("=" * 60 + "\n\n")
                    
                    for msg in history.messages:
                        role = msg.get('role', '')
                        content = msg.get('content', '')
                        timestamp = msg.get('timestamp', '')
                        
                        if role == 'user':
                            f.write(f"🧑 You ({timestamp}):\n{content}\n\n")
                        elif role == 'assistant':
                            f.write(f"🤖 Assistant ({timestamp}):\n{content}\n\n")
                
                messagebox.showinfo("Success", f"Session exported to {file_path}")
                self.status_label.config(text="✅ Exported")
            except Exception as e:
                logger.error(f"Export error: {e}")
                messagebox.showerror("Error", f"Failed to export: {e}")
    
    def _parse_markdown(self, text: str) -> list:
        """Парсинг Markdown и возврат списка (текст, тег)"""
        import re
        
        result = []
        pos = 0
        text_len = len(text)
        
        # Паттерны для разных элементов Markdown
        patterns = [
            # Блоки кода
            (r'```(\w+)?\n(.*?)\n```', 'code_block', True),
            # Инлайн код
            (r'`([^`]+)`', 'code', False),
            # Жирный текст
            (r'\*\*([^*]+)\*\*', 'bold', False),
            # Курсив
            (r'\*([^*]+)\*', 'italic', False),
            # Заголовки
            (r'^### (.+)$', 'heading3', False),
            (r'^## (.+)$', 'heading2', False),
            (r'^# (.+)$', 'heading1', False),
            # Списки
            (r'^[-*] (.+)$', 'bullet', False),
            (r'^\d+\. (.+)$', 'number', False),
        ]
        
        # Сначала ищем блоки кода (многострочные)
        code_pattern = r'```(\w+)?\n(.*?)\n```'
        code_matches = list(re.finditer(code_pattern, text, re.DOTALL))
        
        if code_matches:
            last_end = 0
            for match in code_matches:
                # Добавляем текст до блока кода
                if match.start() > last_end:
                    result.append((text[last_end:match.start()], None))
                
                # Добавляем блок кода
                lang = match.group(1) or ''
                code = match.group(2)
                result.append((code, ('code_block', lang)))
                
                last_end = match.end()
            
            # Добавляем остаток текста
            if last_end < text_len:
                result.append((text[last_end:], None))
            
            return result
        
        # Если нет блоков кода, обрабатываем построчно
        lines = text.split('\n')
        for line in lines:
            if not line:
                result.append(('\n', None))
                continue
            
            # Проверяем заголовки и списки
            header_match = re.match(r'^(#{1,3}) (.+)$', line)
            if header_match:
                level = len(header_match.group(1))
                tag = f'heading{level}'
                result.append((header_match.group(2) + '\n', tag))
                continue
            
            list_match = re.match(r'^([-*]|\d+\.) (.+)$', line)
            if list_match:
                tag = 'bullet' if list_match.group(1) in ['-', '*'] else 'number'
                result.append((list_match.group(2) + '\n', tag))
                continue
            
            # Проверяем инлайн форматирование
            remaining = line
            while remaining:
                # Ищем ближайшее совпадение
                earliest = len(remaining)
                matched = None
                matched_tag = None
                
                for pattern, tag, _ in [(r'`([^`]+)`', 'code', False), 
                                       (r'\*\*([^*]+)\*\*', 'bold', False),
                                       (r'\*([^*]+)\*', 'italic', False)]:
                    m = re.search(pattern, remaining)
                    if m and m.start() < earliest:
                        earliest = m.start()
                        matched = m
                        matched_tag = tag
                
                if matched:
                    # Добавляем текст до совпадения
                    if earliest > 0:
                        result.append((remaining[:earliest], None))
                    
                    # Добавляем совпадение
                    content = matched.group(1)
                    result.append((content, matched_tag))
                    
                    remaining = remaining[matched.end():]
                else:
                    # Добавляем остаток текста
                    result.append((remaining, None))
                    break
            
            result.append(('\n', None))
        
        return result
    
    def _insert_formatted_text(self, text: str, base_tag: str = 'assistant'):
        """Вставка форматированного текста с поддержкой Markdown"""
        self.output_text.config(state=tk.NORMAL)
        
        # Парсим Markdown
        elements = self._parse_markdown(text)
        
        for content, tag_info in elements:
            if tag_info is None:
                # Обычный текст
                self.output_text.insert(tk.END, content, base_tag)
            elif isinstance(tag_info, tuple) and tag_info[0] == 'code_block':
                # Блок кода
                code = tag_info[1]
                lang = tag_info[2] if len(tag_info) > 2 else ''
                
                # Создаем фрейм для блока кода с кнопкой копирования
                code_frame = tk.Frame(self.output_text, bg='#0d0d1a', relief=tk.SUNKEN, borderwidth=1)
                self.output_text.window_create(tk.END, window=code_frame)
                
                # Внутренний фрейм с кодом
                inner_frame = tk.Frame(code_frame, bg='#0d0d1a')
                inner_frame.pack(fill=tk.BOTH, expand=True)
                
                # Добавляем кнопку копирования
                copy_btn = tk.Button(
                    inner_frame,
                    text="📋 Copy",
                    font=('Segoe UI', 8),
                    bg='#2d2d44',
                    fg='#ffffff',
                    relief=tk.FLAT,
                    cursor='hand2',
                    command=lambda c=code: self._copy_code_block(c)
                )
                copy_btn.pack(side=tk.RIGHT, padx=5, pady=2)
                
                # Добавляем язык если указан
                if lang:
                    lang_label = tk.Label(
                        inner_frame,
                        text=f"▸ {lang}",
                        font=('Consolas', 8, 'bold'),
                        bg='#0d0d1a',
                        fg='#8888aa',
                        relief=tk.FLAT
                    )
                    lang_label.pack(side=tk.LEFT, padx=5, pady=2)
                
                # Текст кода
                code_text = tk.Text(
                    inner_frame,
                    font=('Consolas', 9),
                    bg='#0d0d1a',
                    fg='#FFD93D',
                    relief=tk.FLAT,
                    height=max(2, code.count('\n') + 1),
                    wrap=tk.NONE,
                    highlightthickness=0,
                    borderwidth=0,
                    selectbackground='#4a4a6e',
                    selectforeground='#ffffff'
                )
                code_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
                code_text.insert('1.0', code)
                code_text.config(state=tk.DISABLED)
                
                # Добавляем скролл для длинного кода
                if code.count('\n') > 10:
                    scroll = ttk.Scrollbar(inner_frame, orient=tk.VERTICAL, command=code_text.yview)
                    code_text.configure(yscrollcommand=scroll.set)
                    scroll.pack(side=tk.RIGHT, fill=tk.Y)
                
                # Вставляем пустую строку после блока
                self.output_text.insert(tk.END, '\n\n')
                
            elif tag_info == 'code':
                # Инлайн код
                self.output_text.insert(tk.END, f" {content} ", 'code')
            elif tag_info == 'bold':
                self.output_text.insert(tk.END, content, 'bold')
            elif tag_info == 'italic':
                self.output_text.insert(tk.END, content, 'italic')
            else:
                # Заголовки и списки
                self.output_text.insert(tk.END, content, tag_info if tag_info else base_tag)
        
        self.output_text.config(state=tk.DISABLED)
        self.output_text.see(tk.END)
    
    def _copy_code_block(self, code: str):
        """Копирование блока кода"""
        self.window.clipboard_clear()
        self.window.clipboard_append(code)
        self.status_label.config(text="📋 Code copied!")
        logger.info(f"Code block copied ({len(code)} chars)")
    
    def update_chat_display(self):
        """Обновление отображения чата"""
        self.output_text.config(state=tk.NORMAL)
        self.output_text.delete('1.0', tk.END)
        
        if not self.current_history or not self.current_history.messages:
            self.output_text.insert(tk.END, f"💬 Chat with {self.model}\n", 'system')
            self.output_text.insert(tk.END, "=" * 50 + "\n\n", 'system')
            self.output_text.insert(tk.END, "No messages yet. Start typing!\n", 'system')
            self.output_text.config(state=tk.DISABLED)
            return
        
        for msg in self.current_history.messages:
            role = msg.get('role', '')
            content = msg.get('content', '')
            timestamp = msg.get('timestamp', '')
            
            # Вставляем имя отправителя и время
            if role == 'user':
                self.output_text.config(state=tk.NORMAL)
                self.output_text.insert(tk.END, f"🧑 You", 'user')
                if timestamp:
                    self.output_text.insert(tk.END, f" ({timestamp[:16]})", 'timestamp')
                self.output_text.insert(tk.END, f":\n", 'user')
                self.output_text.config(state=tk.DISABLED)
                
                # Вставляем сообщение пользователя (без Markdown)
                self.output_text.config(state=tk.NORMAL)
                self.output_text.insert(tk.END, f"{content}\n\n", 'user')
                self.output_text.config(state=tk.DISABLED)
                
            elif role == 'assistant':
                self.output_text.config(state=tk.NORMAL)
                self.output_text.insert(tk.END, f"🤖 Assistant", 'assistant')
                if timestamp:
                    self.output_text.insert(tk.END, f" ({timestamp[:16]})", 'timestamp')
                self.output_text.insert(tk.END, f":\n", 'assistant')
                self.output_text.config(state=tk.DISABLED)
                
                # Вставляем ответ ассистента с Markdown
                self._insert_formatted_text(content, 'assistant')
        
        self.output_text.config(state=tk.DISABLED)
        self.output_text.see(tk.END)
    
    def send_message(self):
        """Отправка сообщения"""
        if self._processing:
            return
        
        if not self.current_history:
            messagebox.showwarning("Warning", "No active session")
            return
        
        prompt = self.input_entry.get().strip()
        if not prompt:
            return
        
        self.input_entry.delete(0, tk.END)
        self._processing = True
        self.status_label.config(text="⏳ Processing...")
        
        # Добавляем сообщение пользователя
        self.current_history.add_message('user', prompt)
        
        # Обновляем отображение
        self.output_text.config(state=tk.NORMAL)
        self.output_text.insert(tk.END, f"🧑 You", 'user')
        self.output_text.insert(tk.END, f" ({datetime.now().strftime('%H:%M:%S')})", 'timestamp')
        self.output_text.insert(tk.END, f":\n", 'user')
        self.output_text.insert(tk.END, f"{prompt}\n\n", 'user')
        self.output_text.config(state=tk.DISABLED)
        self.output_text.see(tk.END)
        
        # Отправляем запрос
        threading.Thread(target=self._send_message_async, args=(prompt,), daemon=True).start()
    
    def _send_message_async(self, prompt: str):
        """Асинхронная отправка сообщения"""
        try:
            context = self.current_history.get_context(
                self.parent.config.get('context_messages', 20)
            )
            messages = [
                {"role": "system", "content": f"You are a helpful AI assistant. You are {self.model}."}
            ] + context
            
            # Добавляем сообщение ассистента в UI
            self._safe_ui_update(lambda: self._add_assistant_message_start())
            
            full_response = ""
            buffer = ""
            
            def callback(chunk):
                nonlocal full_response, buffer
                if 'message' in chunk and 'content' in chunk['message']:
                    content = chunk['message']['content']
                    full_response += content
                    buffer += content
                    
                    # Обновляем UI порциями
                    if len(buffer) > 50 or '\n' in buffer:
                        self._safe_ui_update(lambda c=buffer: self._append_assistant_content(c))
                        buffer = ""
            
            self.ollama.chat(self.model, messages, callback=callback)
            
            # Добавляем остаток
            if buffer:
                self._safe_ui_update(lambda c=buffer: self._append_assistant_content(c))
            
            if full_response:
                self.current_history.add_message('assistant', full_response)
            
            self._safe_ui_update(lambda: self._finish_assistant_message())
            self._safe_ui_update(lambda: self.status_label.config(text="🟢 Ready"))
            
        except Exception as e:
            logger.error(f"Send message error: {e}", exc_info=True)
            self._safe_ui_update(lambda: self._show_error(str(e)))
        finally:
            self._processing = False
    
    def _add_assistant_message_start(self):
        """Добавление начала сообщения ассистента"""
        self.output_text.config(state=tk.NORMAL)
        self.output_text.insert(tk.END, f"🤖 Assistant", 'assistant')
        self.output_text.insert(tk.END, f" ({datetime.now().strftime('%H:%M:%S')})", 'timestamp')
        self.output_text.insert(tk.END, f":\n", 'assistant')
        self.output_text.config(state=tk.DISABLED)
    
    def _append_assistant_content(self, content: str):
        """Добавление контента ассистента с поддержкой Markdown"""
        # Проверяем, есть ли блоки кода в контенте
        if '```' in content:
            # Если есть блок кода, используем форматирование
            self._insert_formatted_text(content, 'assistant')
        else:
            # Иначе просто вставляем
            self.output_text.config(state=tk.NORMAL)
            self.output_text.insert(tk.END, content, 'assistant')
            self.output_text.config(state=tk.DISABLED)
        
        self.output_text.see(tk.END)
    
    def _finish_assistant_message(self):
        """Завершение сообщения ассистента"""
        self.output_text.config(state=tk.NORMAL)
        self.output_text.insert(tk.END, "\n\n")
        self.output_text.config(state=tk.DISABLED)
        self.output_text.see(tk.END)
    
    def _show_error(self, error_msg: str):
        """Показ ошибки"""
        self.output_text.config(state=tk.NORMAL)
        self.output_text.insert(tk.END, f"\n❌ Error: {error_msg}\n\n", 'error')
        self.output_text.config(state=tk.DISABLED)
        self.status_label.config(text="❌ Error")
    
    def clear_chat(self):
        """Очистка чата"""
        if not self.current_history:
            return
        
        if messagebox.askyesno("Clear Chat", "Clear all messages in this session?"):
            self.current_history.clear()
            self.current_history.save()
            
            self.output_text.config(state=tk.NORMAL)
            self.output_text.delete('1.0', tk.END)
            self.output_text.insert(tk.END, f"💬 Chat with {self.model}\n", 'system')
            self.output_text.insert(tk.END, "=" * 50 + "\n\n", 'system')
            self.output_text.insert(tk.END, "Chat cleared\n", 'system')
            self.output_text.config(state=tk.DISABLED)
            self.status_label.config(text="🧹 Cleared")
    
    def save_chat(self):
        """Сохранение чата"""
        if not self.current_history:
            return
        
        self.current_history.save()
        self.status_label.config(text="💾 Saved")
        self.parent.update_status(f"Chat saved: {self.current_history.session_name}")
    
    def export_chat(self):
        """Экспорт чата в HTML"""
        if not self.current_history or not self.current_history.messages:
            messagebox.showinfo("Info", "No messages to export")
            return
        
        file_path = filedialog.asksaveasfilename(
            defaultextension=".html",
            filetypes=[("HTML files", "*.html"), ("All files", "*.*")],
            initialfile=f"{self.current_history.session_name}.html"
        )
        
        if file_path:
            content = self.output_text.get('1.0', tk.END)
            html_content = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Chat: {self.current_history.session_name}</title>
<style>
body {{ font-family: Arial, sans-serif; padding: 20px; background: #1a1a2e; color: #fff; max-width: 900px; margin: 0 auto; }}
.user {{ color: #4A90D9; }}
.assistant {{ color: #2ECC71; }}
.system {{ color: #F39C12; font-style: italic; }}
.error {{ color: #E74C3C; font-weight: bold; }}
.timestamp {{ color: #95A5A6; font-size: 12px; font-style: italic; }}
.code-block {{ background: #0d0d1a; padding: 15px; border-radius: 8px; margin: 10px 0; overflow-x: auto; }}
.code-block pre {{ margin: 0; color: #FFD93D; font-family: Consolas, monospace; }}
.message {{ margin-bottom: 15px; padding: 10px; border-radius: 8px; background: #2d2d44; }}
</style>
</head>
<body>
<h1>💬 Chat Session: {self.current_history.session_name}</h1>
<p>Model: {self.model}</p>
<p>Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
<hr>
<pre>{content}</pre>
</body>
</html>"""
            try:
                with open(file_path, 'w', encoding='utf-8') as f:
                    f.write(html_content)
                messagebox.showinfo("Success", f"Chat exported to {file_path}")
                self.status_label.config(text="📤 Exported")
            except Exception as e:
                logger.error(f"Export error: {e}")
                messagebox.showerror("Error", f"Failed to export: {e}")
    
    def on_close(self):
        """Закрытие окна"""
        if self.current_history and self.current_history.is_modified:
            if messagebox.askyesno("Save Changes", "Save unsaved changes?"):
                self.current_history.save()
        self.window.destroy()
# ==================== ОСНОВНОЙ КЛАСС ПРИЛОЖЕНИЯ ====================
class OllamaManagerApp:
    """Главное приложение"""
    
    def __init__(self, root):
        self.root = root
        self.config = Config()
        self.ollama_service = OllamaService(self.config.get('ollama_url'))
        self.chat_windows = {}
        
        # Настройка окна
        width = self.config.get('window_width', 1100)
        height = self.config.get('window_height', 700)
        self.root.title("Ollama Manager v2.0")
        self.root.geometry(f"{width}x{height}")
        self.root.minsize(800, 500)
        self.root.resizable(True, True)
        
        self.root.bind('<Configure>', self._on_window_resize)
        self.root.protocol("WM_DELETE_WINDOW", self._on_close)
        
        # Переменные
        self.ollama_installed = False
        self.ollama_running = False
        self.models_list = []
        self.system_info = {}
        self.resource_monitor = None
        
        # Стили
        style = ttk.Style()
        style.theme_use(self.config.get('theme', 'clam'))
        
        # Интерфейс
        self._create_ui()
        
        # Загрузка данных
        self.check_system()
        self.check_ollama()
    
    def _create_ui(self):
        """Создание интерфейса"""
        # Основной канвас с прокруткой
        self.main_canvas = tk.Canvas(self.root)
        self.main_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        
        scrollbar = ttk.Scrollbar(self.root, orient=tk.VERTICAL, 
                                 command=self.main_canvas.yview)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        self.main_canvas.configure(yscrollcommand=scrollbar.set)
        self.main_canvas.bind('<Configure>', 
            lambda e: self.main_canvas.configure(scrollregion=self.main_canvas.bbox("all")))
        
        self.main_frame = ttk.Frame(self.main_canvas)
        self.main_canvas.create_window((0, 0), window=self.main_frame, anchor="nw")
        
        # Статус-бар
        self.status_var = tk.StringVar()
        self.status_var.set("🚀 Initializing...")
        self.status_bar = ttk.Label(self.root, textvariable=self.status_var, 
                                   relief=tk.SUNKEN, anchor=tk.W)
        self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
        
        # Системная панель
        self.create_system_panel(self.main_frame)
        
        # Монитор ресурсов
        self.resource_monitor = ResourceMonitor(self.main_frame, self.status_var)
        self.resource_monitor.frame.pack(fill=tk.X, pady=2)
        self.resource_monitor.start()
        
        # Вкладки
        self.notebook = ttk.Notebook(self.main_frame)
        self.notebook.pack(fill=tk.BOTH, expand=True, pady=(10, 0))
        
        self.models_frame = ttk.Frame(self.notebook)
        self.notebook.add(self.models_frame, text="📦 Models")
        self.create_models_tab()
        
        self.control_frame = ttk.Frame(self.notebook)
        self.notebook.add(self.control_frame, text="⚙️ Control")
        self.create_control_tab()
        
        self.settings_frame = ttk.Frame(self.notebook)
        self.notebook.add(self.settings_frame, text="⚙️ Settings")
        self.create_settings_tab()
        
        self.logs_frame = ttk.Frame(self.notebook)
        self.notebook.add(self.logs_frame, text="📋 Logs")
        self.create_logs_tab()
    
    def _on_window_resize(self, event):
        """Обработка изменения размера окна"""
        if event.widget == self.root:
            self.config.set('window_width', event.width)
            self.config.set('window_height', event.height)
    
    def _on_close(self):
        """Закрытие приложения"""
        self.resource_monitor.stop()
        self.ollama_service.close()
        self.root.destroy()
    
    def update_status(self, message: str):
        """Обновление статуса"""
        self.root.after(0, lambda: self.status_var.set(message))
    
    # ==================== СИСТЕМНАЯ ПАНЕЛЬ ====================
    
    def create_system_panel(self, parent):
        """Создание панели системы"""
        frame = ttk.LabelFrame(parent, text="🖥️ System Information", padding=5)
        frame.pack(fill=tk.X, pady=(0, 5))
        
        self.sys_info_var = tk.StringVar()
        self.sys_info_var.set("Detecting system...")
        
        sys_label = ttk.Label(frame, textvariable=self.sys_info_var, font=('Arial', 9))
        sys_label.pack(side=tk.LEFT)
        
        self.ollama_status_var = tk.StringVar()
        self.ollama_status_var.set("🔍 Checking Ollama...")
        
        status_label = ttk.Label(frame, textvariable=self.ollama_status_var, 
                                font=('Arial', 9, 'bold'))
        status_label.pack(side=tk.RIGHT, padx=10)
        
        btn_frame = ttk.Frame(frame)
        btn_frame.pack(side=tk.RIGHT, padx=5)
        
        self.start_btn = ttk.Button(btn_frame, text="▶️ Start", 
                                   command=self.start_ollama, state=tk.DISABLED)
        self.start_btn.pack(side=tk.LEFT, padx=2)
        
        self.stop_btn = ttk.Button(btn_frame, text="⏹️ Stop", 
                                  command=self.stop_ollama, state=tk.DISABLED)
        self.stop_btn.pack(side=tk.LEFT, padx=2)
        
        self.autorun_btn = ttk.Button(btn_frame, text="⚡ AutoStart", 
                                     command=self.toggle_autorun)
        self.autorun_btn.pack(side=tk.LEFT, padx=2)
    
    # ==================== МОНИТОРИНГ СИСТЕМЫ ====================
    
    def check_system(self):
        """Сбор информации о системе"""
        self.status_var.set("Detecting system hardware...")
        logger.info("Checking system hardware")
        
        info = []
        
        cpu = platform.processor() or "Unknown"
        cpu_cores = psutil.cpu_count(logical=False) or 0
        cpu_threads = psutil.cpu_count(logical=True) or 0
        info.append(f"🧠 CPU: {cpu} ({cpu_cores} cores / {cpu_threads} threads)")
        
        mem = psutil.virtual_memory()
        mem_gb = mem.total / (1024**3)
        info.append(f"💾 RAM: {mem_gb:.1f} GB")
        
        gpu_info = self.detect_gpu()
        info.append(f"🎮 GPU: {gpu_info}")
        
        disk = psutil.disk_usage('/')
        disk_gb = disk.total / (1024**3)
        info.append(f"💽 Disk: {disk_gb:.1f} GB")
        
        info.append(f"🐧 OS: {platform.system()} {platform.release()}")
        
        import socket
        hostname = socket.gethostname()
        info.append(f"🖥️ Host: {hostname}")
        
        self.sys_info_var.set(" | ".join(info))
        self.system_info = {
            'cpu': cpu,
            'cpu_cores': cpu_cores,
            'cpu_threads': cpu_threads,
            'ram_gb': mem_gb,
            'gpu': gpu_info,
            'disk_gb': disk_gb,
            'os': f"{platform.system()} {platform.release()}",
            'hostname': hostname
        }
        
        self.status_var.set("System detected successfully")
        logger.info("System detection complete")
    
    def detect_gpu(self) -> str:
        """Определение GPU"""
        # NVIDIA
        try:
            result = subprocess.run(['nvidia-smi', '--query-gpu=name', '--format=csv,noheader'], 
                                   capture_output=True, text=True, timeout=5)
            if result.returncode == 0:
                return f"NVIDIA: {result.stdout.strip()}"
        except Exception as e:
            logger.debug(f"NVIDIA detection error: {e}")
        
        # AMD ROCm
        try:
            result = subprocess.run(['rocminfo'], capture_output=True, text=True, timeout=5)
            if 'gfx' in result.stdout.lower():
                for line in result.stdout.split('\n'):
                    if 'Name:' in line and 'gfx' in line.lower():
                        return f"AMD: {line.split('Name:')[1].strip()}"
                return "AMD (ROCm detected)"
        except Exception as e:
            logger.debug(f"AMD ROCm detection error: {e}")
        
        # Intel
        try:
            result = subprocess.run(['lspci', '-vnn'], capture_output=True, text=True)
            for line in result.stdout.split('\n'):
                if 'VGA' in line and 'Intel' in line:
                    return "Intel Integrated Graphics"
        except Exception as e:
            logger.debug(f"Intel detection error: {e}")
        
        # AMD через lspci
        try:
            result = subprocess.run(['lspci', '-vnn'], capture_output=True, text=True)
            for line in result.stdout.split('\n'):
                if 'VGA' in line and ('AMD' in line or 'ATI' in line):
                    import re
                    match = re.search(r'\[(.*?)\]', line)
                    if match:
                        return f"AMD: {match.group(1)}"
        except Exception as e:
            logger.debug(f"AMD lspci detection error: {e}")
        
        return "No dedicated GPU detected"
    
    # ==================== ПРОВЕРКА OLLAMA ====================
    
    def check_ollama(self):
        """Проверка Ollama"""
        threading.Thread(target=self._check_ollama_async, daemon=True).start()
    
    def _check_ollama_async(self):
        """Асинхронная проверка Ollama"""
        logger.info("Checking Ollama installation")
        
        ollama_path = self._get_ollama_path()
        
        if ollama_path and os.path.exists(ollama_path):
            self.ollama_installed = True
            self.root.after(0, lambda: self.ollama_status_var.set("✅ Ollama installed"))
            logger.info(f"Ollama found at: {ollama_path}")
            
            # Проверка работы
            version = OllamaService.get_version()
            if version:
                self.ollama_running = True
                self.root.after(0, lambda: self.ollama_status_var.set(f"▶️ Running ({version})"))
                self.root.after(0, lambda: self.start_btn.config(state=tk.DISABLED))
                self.root.after(0, lambda: self.stop_btn.config(state=tk.NORMAL))
                
                self.load_models()
                logger.info(f"Ollama running: {version}")
            else:
                self.ollama_running = False
                self.root.after(0, lambda: self.ollama_status_var.set("⏹️ Installed but not running"))
                self.root.after(0, lambda: self.start_btn.config(state=tk.NORMAL))
                self.root.after(0, lambda: self.stop_btn.config(state=tk.DISABLED))
                logger.warning("Ollama installed but not running")
        else:
            self.ollama_installed = False
            self.root.after(0, lambda: self.ollama_status_var.set("❌ Not installed"))
            self.root.after(0, lambda: self.start_btn.config(state=tk.DISABLED))
            self.root.after(0, lambda: self.stop_btn.config(state=tk.DISABLED))
            logger.warning("Ollama not found")
            
            self.root.after(0, self.offer_install)
        
        self.root.after(0, self.check_autorun_status)
    
    def _get_ollama_path(self) -> Optional[str]:
        """Получение пути к Ollama"""
        system = platform.system()
        
        if system == "Windows":
            paths = [
                shutil.which('ollama.exe'),
                Path(os.environ.get('USERPROFILE', '')) / 'AppData/Local/ollama/ollama.exe',
                Path(os.environ.get('LOCALAPPDATA', '')) / 'Programs/Ollama/ollama.exe'
            ]
        elif system == "Darwin":
            paths = [
                shutil.which('ollama'),
                '/usr/local/bin/ollama',
                '/opt/homebrew/bin/ollama'
            ]
        else:
            paths = [
                shutil.which('ollama'),
                '/usr/local/bin/ollama',
                '/usr/bin/ollama'
            ]
        
        for path in paths:
            if path and os.path.exists(str(path)):
                return str(path)
        return None
    
    def offer_install(self):
        """Предложение установки"""
        if messagebox.askyesno("Ollama Not Found", 
                               "Ollama is not installed on your system.\n\n"
                               "Do you want to install it now?"):
            self.install_ollama_dialog()
    
    def install_ollama_dialog(self):
        """Диалог установки"""
        dialog = tk.Toplevel(self.root)
        dialog.title("Install Ollama")
        dialog.geometry("450x200")
        dialog.resizable(False, False)
        
        ttk.Label(dialog, text="Choose installation method:", font=('Arial', 11)).pack(pady=15)
        
        def install_official():
            dialog.destroy()
            threading.Thread(target=self._install_ollama_official, daemon=True).start()
        
        def install_aur():
            dialog.destroy()
            threading.Thread(target=self._install_ollama_aur, daemon=True).start()
        
        def install_manual():
            dialog.destroy()
            messagebox.showinfo("Manual Installation",
                "Download Ollama from:\n\n"
                "https://ollama.com/download\n\n"
                "Or use:\n"
                "curl -fsSL https://ollama.com/install.sh | sh")
        
        btn_frame = ttk.Frame(dialog)
        btn_frame.pack(pady=10)
        
        ttk.Button(btn_frame, text="📦 Official Script", 
                  command=install_official, width=20).pack(pady=3)
        ttk.Button(btn_frame, text="📦 AUR Package", 
                  command=install_aur, width=20).pack(pady=3)
        ttk.Button(btn_frame, text="📥 Download Manually", 
                  command=install_manual, width=20).pack(pady=3)
        ttk.Button(btn_frame, text="Cancel", 
                  command=dialog.destroy, width=20).pack(pady=3)
    
    def _install_ollama_official(self):
        """Установка через официальный скрипт с проверкой"""
        try:
            self.status_var.set("Downloading install script...")
            
            # Скачиваем скрипт
            response = requests.get('https://ollama.com/install.sh', timeout=30)
            response.raise_for_status()
            
            # Проверяем SHA256 (опционально)
            script_content = response.text
            
            self.status_var.set("Running install script...")
            
            # Сохраняем во временный файл и выполняем
            with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as f:
                f.write(script_content)
                script_path = f.name
            
            try:
                subprocess.run(['bash', script_path], check=True)
                self.status_var.set("Ollama installed successfully")
                messagebox.showinfo("Success", "Ollama installed successfully!\n\nPlease restart the application.")
            finally:
                os.unlink(script_path)
                
        except Exception as e:
            logger.error(f"Installation failed: {e}", exc_info=True)
            self.status_var.set("Installation failed")
            messagebox.showerror("Error", f"Installation failed:\n{e}")
    
    def _install_ollama_aur(self):
        """Установка из AUR"""
        try:
            helper = None
            for h in ['yay', 'paru', 'trizen']:
                if shutil.which(h):
                    helper = h
                    break
            
            if not helper:
                messagebox.showerror("Error", "No AUR helper found (yay, paru, trizen)")
                return
            
            self.status_var.set(f"Installing with {helper}...")
            subprocess.run([helper, '-S', 'ollama-bin'], check=True)
            
            self.status_var.set("Ollama installed successfully")
            messagebox.showinfo("Success", "Ollama installed from AUR!\n\nPlease restart the application.")
            
        except subprocess.CalledProcessError as e:
            logger.error(f"AUR installation failed: {e}")
            self.status_var.set("Installation failed")
            messagebox.showerror("Error", f"Installation failed:\n{e}")
    
    # ==================== УПРАВЛЕНИЕ OLLAMA ====================
    
    def start_ollama(self):
        """Запуск Ollama"""
        self.status_var.set("Starting Ollama...")
        logger.info("Starting Ollama")
        
        try:
            if OllamaService.is_running():
                self.ollama_running = True
                self.root.after(0, lambda: self.ollama_status_var.set("▶️ Running"))
                self.root.after(0, lambda: self.start_btn.config(state=tk.DISABLED))
                self.root.after(0, lambda: self.stop_btn.config(state=tk.NORMAL))
                self.status_var.set("Ollama already running")
                self.load_models()
                return
            
            if shutil.which('systemctl'):
                subprocess.run(['systemctl', 'start', 'ollama'], check=True)
            else:
                subprocess.Popen(['ollama', 'serve'], 
                               stdout=subprocess.DEVNULL, 
                               stderr=subprocess.DEVNULL)
            
            time.sleep(2)
            
            if OllamaService.is_running():
                self.ollama_running = True
                self.root.after(0, lambda: self.ollama_status_var.set("▶️ Running"))
                self.root.after(0, lambda: self.start_btn.config(state=tk.DISABLED))
                self.root.after(0, lambda: self.stop_btn.config(state=tk.NORMAL))
                self.status_var.set("Ollama started successfully")
                self.load_models()
                logger.info("Ollama started successfully")
            else:
                raise RuntimeError("Ollama failed to start")
            
        except Exception as e:
            logger.error(f"Failed to start Ollama: {e}")
            self.status_var.set("Error starting Ollama")
            messagebox.showerror("Error", f"Failed to start Ollama:\n{e}")
    
    def stop_ollama(self):
        """Остановка Ollama"""
        if not messagebox.askyesno("Stop Ollama", 
                                   "Are you sure you want to stop Ollama?\nAll models will be unloaded."):
            return
        
        self.status_var.set("Stopping Ollama...")
        logger.info("Stopping Ollama")
        
        try:
            # Останавливаем через systemd
            if shutil.which('systemctl'):
                subprocess.run(['systemctl', 'stop', 'ollama'], check=False)
            
            # Убиваем процессы
            subprocess.run(['pkill', '-x', 'ollama'], check=False)
            
            time.sleep(1)
            
            if not OllamaService.is_running():
                self.ollama_running = False
                self.root.after(0, lambda: self.ollama_status_var.set("⏹️ Stopped"))
                self.root.after(0, lambda: self.start_btn.config(state=tk.NORMAL))
                self.root.after(0, lambda: self.stop_btn.config(state=tk.DISABLED))
                self.status_var.set("Ollama stopped successfully")
                
                self.models_list = []
                self.root.after(0, self.update_models_table)
                logger.info("Ollama stopped successfully")
            else:
                # Force stop
                subprocess.run(['pkill', '-9', 'ollama'], check=False)
                self.ollama_running = False
                self.root.after(0, lambda: self.ollama_status_var.set("⏹️ Force stopped"))
                self.root.after(0, lambda: self.start_btn.config(state=tk.NORMAL))
                self.root.after(0, lambda: self.stop_btn.config(state=tk.DISABLED))
                self.status_var.set("Ollama force stopped")
                logger.warning("Ollama force stopped")
                
        except Exception as e:
            logger.error(f"Failed to stop Ollama: {e}")
            self.status_var.set("Error stopping Ollama")
            messagebox.showerror("Error", f"Failed to stop Ollama:\n{e}")
    
    def toggle_autorun(self):
        """Переключение автозапуска"""
        if not self.ollama_installed:
            messagebox.showwarning("Warning", "Ollama is not installed")
            return
        
        if not shutil.which('systemctl'):
            messagebox.showinfo("Info", "systemctl not found, autorun not available")
            return
        
        try:
            result = subprocess.run(['systemctl', 'is-enabled', 'ollama'], 
                                   capture_output=True, text=True)
            enabled = result.stdout.strip() == 'enabled'
            
            if enabled:
                subprocess.run(['systemctl', 'disable', 'ollama'], check=True)
                messagebox.showinfo("Success", "Autostart disabled")
                self.status_var.set("Autostart disabled")
                logger.info("Autostart disabled")
            else:
                subprocess.run(['systemctl', 'enable', 'ollama'], check=True)
                messagebox.showinfo("Success", "Autostart enabled")
                self.status_var.set("Autostart enabled")
                logger.info("Autostart enabled")
            
            self.root.after(0, self.check_autorun_status)
            
        except subprocess.CalledProcessError as e:
            logger.error(f"Failed to toggle autorun: {e}")
            messagebox.showerror("Error", f"Failed to toggle autorun:\n{e}")
            self.status_var.set("Error")
    
    def check_autorun_status(self):
        """Проверка статуса автозапуска"""
        if not shutil.which('systemctl'):
            self.autorun_btn.config(text="⚡ No systemd")
            return
        
        try:
            result = subprocess.run(['systemctl', 'is-enabled', 'ollama'], 
                                   capture_output=True, text=True)
            if result.stdout.strip() == 'enabled':
                self.autorun_btn.config(text="⚡ Enabled")
            else:
                self.autorun_btn.config(text="⚡ Disabled")
        except Exception:
            self.autorun_btn.config(text="⚡ Unknown")
    
    # ==================== РАБОТА С МОДЕЛЯМИ ====================
    
    def create_models_tab(self):
        """Создание вкладки моделей"""
        top_frame = ttk.Frame(self.models_frame)
        top_frame.pack(fill=tk.X, pady=(0, 10))
        
        ttk.Label(top_frame, text="🔄 Models Management", 
                 font=('Arial', 11, 'bold')).pack(side=tk.LEFT)
        
        btn_frame = ttk.Frame(top_frame)
        btn_frame.pack(side=tk.RIGHT)
        
        ttk.Button(btn_frame, text="🔄 Refresh", 
                  command=self.load_models).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="📥 Install from Hub", 
                  command=self.install_from_hub).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="📤 Export Model", 
                  command=self.export_model_dialog).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="📥 Import Model", 
                  command=self.import_model_dialog).pack(side=tk.LEFT, padx=2)
        
        # Таблица
        self.models_tree_frame = ttk.Frame(self.models_frame)
        self.models_tree_frame.pack(fill=tk.BOTH, expand=True)
        
        columns = ('Name', 'Size', 'Modified', 'Status')
        self.models_tree = ttk.Treeview(self.models_tree_frame, columns=columns, 
                                       show='tree headings')
        
        self.models_tree.heading('#0', text='#')
        self.models_tree.heading('Name', text='Model Name')
        self.models_tree.heading('Size', text='Size')
        self.models_tree.heading('Modified', text='Modified')
        self.models_tree.heading('Status', text='Status')
        
        self.models_tree.column('#0', width=40)
        self.models_tree.column('Name', width=300)
        self.models_tree.column('Size', width=100)
        self.models_tree.column('Modified', width=150)
        self.models_tree.column('Status', width=100)
        
        self.models_tree.tag_configure('active', background='#90EE90')
        self.models_tree.tag_configure('inactive', background='#FFB6C1')
        self.models_tree.tag_configure('loading', background='#FFD700')
        
        scrollbar = ttk.Scrollbar(self.models_tree_frame, orient=tk.VERTICAL, 
                                 command=self.models_tree.yview)
        self.models_tree.configure(yscrollcommand=scrollbar.set)
        
        self.models_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        # Контекстное меню
        self.models_context_menu = tk.Menu(self.root, tearoff=0)
        self.models_context_menu.add_command(label="▶️ Run Model", command=self.run_model)
        self.models_context_menu.add_command(label="🔄 Update Model", command=self.update_model)
        self.models_context_menu.add_command(label="📝 Create from Model", command=self.create_from_model)
        self.models_context_menu.add_separator()
        self.models_context_menu.add_command(label="ℹ️ Model Info", command=self.model_info)
        self.models_context_menu.add_separator()
        self.models_context_menu.add_command(label="📤 Export Model", command=self.export_model_dialog)
        self.models_context_menu.add_separator()
        self.models_context_menu.add_command(label="🗑️ Delete Model", command=self.delete_model)
        
        self.models_tree.bind("<Button-3>", self.show_models_context_menu)
        self.models_tree.bind("<Double-1>", lambda e: self.run_model())
    
    def load_models(self):
        """Загрузка моделей"""
        if not self.ollama_running:
            self.status_var.set("Ollama is not running")
            return
        
        self.status_var.set("Loading models...")
        threading.Thread(target=self._load_models_async, daemon=True).start()
    
    def _load_models_async(self):
        """Асинхронная загрузка моделей"""
        try:
            api_models = self.ollama_service.list_models()
            if api_models:
                self.models_list = api_models
                self.root.after(0, self.update_models_table)
                self.status_var.set(f"Loaded {len(self.models_list)} models via API")
                return
            
            # Fallback на CLI
            result = subprocess.run(['ollama', 'list', '--json'], 
                                   capture_output=True, text=True, timeout=10)
            
            if result.returncode == 0:
                try:
                    data = json.loads(result.stdout)
                    self.models_list = data.get('models', [])
                except json.JSONDecodeError:
                    self.models_list = self._parse_ollama_list(result.stdout)
            else:
                self.models_list = []
            
            self.root.after(0, self.update_models_table)
            
        except subprocess.TimeoutExpired:
            self.status_var.set("Timeout loading models")
            logger.warning("Timeout loading models")
        except Exception as e:
            self.status_var.set(f"Error: {str(e)}")
            logger.error(f"Error loading models: {e}")
    
    def _parse_ollama_list(self, output: str) -> List[Dict]:
        """Парсинг вывода ollama list"""
        models = []
        lines = output.strip().split('\n')[1:]
        
        for line in lines:
            parts = line.split()
            if len(parts) >= 3:
                models.append({
                    'name': parts[0],
                    'size': parts[1],
                    'modified': ' '.join(parts[2:])
                })
        
        return models
    
    def update_models_table(self):
        """Обновление таблицы моделей"""
        for item in self.models_tree.get_children():
            self.models_tree.delete(item)
        
        if not self.models_list:
            self.status_var.set("No models found")
            return
        
        for i, model in enumerate(self.models_list, 1):
            name = model.get('name', 'Unknown')
            size = model.get('size', 'Unknown')
            modified = model.get('modified', model.get('digest', 'Unknown'))
            
            self.models_tree.insert('', 'end', 
                                  values=(name, size, modified, "Ready"),
                                  text=str(i),
                                  tags=('active',))
        
        self.status_var.set(f"Loaded {len(self.models_list)} models")
    
    def show_models_context_menu(self, event):
        """Показать контекстное меню"""
        if self.models_tree.selection():
            self.models_context_menu.post(event.x_root, event.y_root)
    
    def get_selected_model(self) -> Optional[str]:
        """Получить выбранную модель"""
        selection = self.models_tree.selection()
        if not selection:
            messagebox.showwarning("Warning", "Please select a model")
            return None
        
        values = self.models_tree.item(selection[0])['values']
        return values[0] if values else None
    
    # ==================== ДЕЙСТВИЯ С МОДЕЛЯМИ ====================
    
    def run_model(self):
        """Запуск модели"""
        model = self.get_selected_model()
        if not model:
            return
        
        self.config.set('last_model', model)
        
        # Проверяем, не открыто ли уже окно
        if model in self.chat_windows:
            try:
                self.chat_windows[model].window.lift()
                self.chat_windows[model].window.focus()
                return
            except tk.TclError:
                del self.chat_windows[model]
        
        # Создаем новое окно
        chat_window = ChatWindow(self, model, self.ollama_service)
        self.chat_windows[model] = chat_window
    
    def update_model(self):
        """Обновление модели"""
        model = self.get_selected_model()
        if not model:
            return
        
        if messagebox.askyesno("Update Model", f"Update {model} to latest version?"):
            self.status_var.set(f"Updating {model}...")
            threading.Thread(target=self._update_model_async, args=(model,), daemon=True).start()
    
    def _update_model_async(self, model: str):
        """Асинхронное обновление модели"""
        try:
            result = subprocess.run(['ollama', 'pull', model], capture_output=True, text=True)
            if result.returncode == 0:
                self.root.after(0, lambda: messagebox.showinfo("Success", f"Model {model} updated"))
                self.root.after(0, self.load_models)
                self.status_var.set(f"Updated {model}")
                logger.info(f"Model updated: {model}")
            else:
                raise RuntimeError(result.stderr)
        except Exception as e:
            logger.error(f"Failed to update {model}: {e}")
            self.root.after(0, lambda: messagebox.showerror("Error", f"Failed to update {model}:\n{e}"))
            self.status_var.set("Error")
    
    def create_from_model(self):
        """Создание модели из существующей"""
        base_model = self.get_selected_model()
        if not base_model:
            return
        
        dialog = tk.Toplevel(self.root)
        dialog.title(f"📝 Create Model from {base_model}")
        dialog.geometry("600x500")
        dialog.resizable(True, True)
        dialog.minsize(400, 400)
        
        ttk.Label(dialog, text="New Model Name:").pack(pady=(15, 5))
        name_entry = ttk.Entry(dialog, width=50)
        name_entry.pack(pady=5)
        name_entry.insert(0, f"{base_model}-custom")
        
        ttk.Label(dialog, text="System Prompt:").pack(pady=(10, 5))
        prompt_text = scrolledtext.ScrolledText(dialog, height=5, width=60)
        prompt_text.pack(pady=5, padx=20)
        prompt_text.insert('1.0', f"You are a helpful assistant based on {base_model}")
        
        params_frame = ttk.LabelFrame(dialog, text="Parameters")
        params_frame.pack(fill=tk.X, padx=20, pady=10)
        
        # Temperature
        temp_frame = ttk.Frame(params_frame)
        temp_frame.pack(fill=tk.X, padx=5, pady=5)
        
        ttk.Label(temp_frame, text="Temperature:").pack(side=tk.LEFT)
        temp_var = tk.DoubleVar(value=0.7)
        temp_scale = ttk.Scale(temp_frame, from_=0.0, to=2.0, orient=tk.HORIZONTAL, 
                              variable=temp_var, length=200)
        temp_scale.pack(side=tk.LEFT, padx=10, fill=tk.X, expand=True)
        temp_label = ttk.Label(temp_frame, text="0.7")
        temp_label.pack(side=tk.LEFT, padx=5)
        
        def update_temp_label(val):
            temp_label.config(text=f"{float(val):.1f}")
        temp_scale.configure(command=update_temp_label)
        
        # Top P
        top_p_frame = ttk.Frame(params_frame)
        top_p_frame.pack(fill=tk.X, padx=5, pady=5)
        
        ttk.Label(top_p_frame, text="Top P:").pack(side=tk.LEFT)
        top_p_var = tk.DoubleVar(value=0.9)
        top_p_scale = ttk.Scale(top_p_frame, from_=0.0, to=1.0, orient=tk.HORIZONTAL,
                               variable=top_p_var, length=200)
        top_p_scale.pack(side=tk.LEFT, padx=10, fill=tk.X, expand=True)
        top_p_label = ttk.Label(top_p_frame, text="0.9")
        top_p_label.pack(side=tk.LEFT, padx=5)
        
        def update_top_p_label(val):
            top_p_label.config(text=f"{float(val):.1f}")
        top_p_scale.configure(command=update_top_p_label)
        
        # Context
        ctx_frame = ttk.Frame(params_frame)
        ctx_frame.pack(fill=tk.X, padx=5, pady=5)
        
        ttk.Label(ctx_frame, text="Context Length:").pack(side=tk.LEFT)
        ctx_var = tk.IntVar(value=2048)
        ctx_spin = ttk.Spinbox(ctx_frame, from_=512, to=8192, increment=512, 
                              textvariable=ctx_var, width=10)
        ctx_spin.pack(side=tk.LEFT, padx=10)
        
        def create():
            name = name_entry.get().strip()
            if not name:
                messagebox.showwarning("Warning", "Model name is required")
                return
            
            system_prompt = prompt_text.get('1.0', tk.END).strip()
            temperature = temp_var.get()
            top_p = top_p_var.get()
            context_length = ctx_var.get()
            
            dialog.destroy()
            self.status_var.set(f"Creating model {name}...")
            
            modelfile = f"""
FROM {base_model}

SYSTEM {system_prompt}

PARAMETER temperature {temperature}
PARAMETER top_p {top_p}
PARAMETER num_ctx {context_length}
"""
            
            import tempfile
            with tempfile.NamedTemporaryFile(mode='w', suffix='.Modelfile', delete=False) as f:
                f.write(modelfile)
                modelfile_path = f.name
            
            threading.Thread(target=self._create_model_async, 
                           args=(name, modelfile_path), daemon=True).start()
        
        btn_frame = ttk.Frame(dialog)
        btn_frame.pack(pady=15)
        ttk.Button(btn_frame, text="Create Model", command=create).pack(side=tk.LEFT, padx=5)
        ttk.Button(btn_frame, text="Cancel", command=dialog.destroy).pack(side=tk.LEFT, padx=5)
    
    def _create_model_async(self, name: str, modelfile_path: str):
        """Асинхронное создание модели"""
        try:
            result = subprocess.run(['ollama', 'create', name, '-f', modelfile_path], 
                                   capture_output=True, text=True)
            os.unlink(modelfile_path)
            
            if result.returncode == 0:
                self.root.after(0, lambda: messagebox.showinfo("Success", f"Model {name} created"))
                self.root.after(0, self.load_models)
                self.status_var.set(f"Model {name} created")
                logger.info(f"Model created: {name}")
            else:
                raise RuntimeError(result.stderr)
        except Exception as e:
            logger.error(f"Failed to create model {name}: {e}")
            self.root.after(0, lambda: messagebox.showerror("Error", f"Failed to create model:\n{e}"))
            self.status_var.set("Error")
    
    def model_info(self):
        """Информация о модели"""
        model = self.get_selected_model()
        if not model:
            return
        
        self.status_var.set(f"Getting info for {model}...")
        
        try:
            result = subprocess.run(['ollama', 'show', model], 
                                   capture_output=True, text=True)
            
            info_window = tk.Toplevel(self.root)
            info_window.title(f"ℹ️ Model Info: {model}")
            info_window.geometry("600x400")
            info_window.resizable(True, True)
            info_window.minsize(400, 300)
            
            text = scrolledtext.ScrolledText(info_window, wrap=tk.WORD)
            text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
            text.insert('1.0', result.stdout or "No information available")
            text.config(state=tk.DISABLED)
            
            self.status_var.set(f"Info for {model} displayed")
            
        except subprocess.CalledProcessError as e:
            logger.error(f"Failed to get info for {model}: {e}")
            messagebox.showerror("Error", f"Failed to get info for {model}:\n{e}")
            self.status_var.set("Error")
    
    def delete_model(self):
        """Удаление модели"""
        model = self.get_selected_model()
        if not model:
            return
        
        if not messagebox.askyesno("Delete Model", 
                                   f"Delete {model}?"):
            return
        
        self.status_var.set(f"Deleting {model}...")
        threading.Thread(target=self._delete_model_async, args=(model,), daemon=True).start()
    
    def _delete_model_async(self, model: str):
        """Асинхронное удаление модели"""
        try:
            if self.ollama_service.delete_model(model):
                self.root.after(0, lambda: messagebox.showinfo("Success", f"Model {model} deleted"))
                self.root.after(0, self.load_models)
                self.status_var.set(f"Model {model} deleted")
                logger.info(f"Model deleted: {model}")
            else:
                raise RuntimeError("Delete failed")
        except Exception as e:
            logger.error(f"Failed to delete {model}: {e}")
            self.root.after(0, lambda: messagebox.showerror("Error", f"Failed to delete {model}:\n{e}"))
            self.status_var.set("Error")
    
    # ==================== ЭКСПОРТ/ИМПОРТ ====================
    
    def export_model_dialog(self):
        """Диалог экспорта модели"""
        model = self.get_selected_model()
        if not model:
            return
        
        file_path = filedialog.asksaveasfilename(
            defaultextension=".ollama",
            filetypes=[("Ollama model", "*.ollama"), ("All files", "*.*")],
            initialfile=f"{model}.ollama"
        )
        if file_path:
            self.status_var.set(f"Exporting {model}...")
            threading.Thread(target=self._export_model_async, 
                           args=(model, file_path), daemon=True).start()
    
    def _export_model_async(self, model: str, export_path: str):
        """Экспорт модели"""
        try:
            result = subprocess.run(['ollama', 'export', model, '-o', export_path], 
                                  capture_output=True, text=True)
            if result.returncode == 0:
                self.root.after(0, lambda: messagebox.showinfo("Success", 
                    f"Model exported to {export_path}"))
                self.status_var.set(f"Model {model} exported")
                logger.info(f"Model exported: {model}")
            else:
                self.root.after(0, lambda: messagebox.showinfo("Info", 
                    "Export not supported in this version.\n"
                    "Backup model from ~/.ollama/models/"))
        except Exception as e:
            logger.error(f"Failed to export {model}: {e}")
            self.root.after(0, lambda: messagebox.showerror("Error", f"Export failed:\n{e}"))
            self.status_var.set("Error")
    
    def import_model_dialog(self):
        """Диалог импорта модели"""
        file_path = filedialog.askopenfilename(
            filetypes=[("Ollama model", "*.ollama"), ("All files", "*.*")]
        )
        if file_path:
            self.status_var.set(f"Importing model from {file_path}...")
            threading.Thread(target=self._import_model_async, 
                           args=(file_path,), daemon=True).start()
    
    def _import_model_async(self, import_path: str):
        """Импорт модели"""
        try:
            result = subprocess.run(['ollama', 'import', import_path], 
                                  capture_output=True, text=True)
            if result.returncode == 0:
                self.root.after(0, self.load_models)
                self.root.after(0, lambda: messagebox.showinfo("Success", "Model imported"))
                self.status_var.set("Model imported")
                logger.info(f"Model imported from: {import_path}")
            else:
                self.root.after(0, lambda: messagebox.showinfo("Info", 
                    "Import not supported in this version.\n"
                    "Place model in ~/.ollama/models/ manually."))
        except Exception as e:
            logger.error(f"Failed to import model: {e}")
            self.root.after(0, lambda: messagebox.showerror("Error", f"Import failed:\n{e}"))
            self.status_var.set("Error")
    
    # ==================== УСТАНОВКА ИЗ HUB ====================
    
    def install_from_hub(self):
        """Установка модели из Hub"""
        dialog = tk.Toplevel(self.root)
        dialog.title("📥 Install from Hub")
        dialog.geometry("700x500")
        dialog.resizable(True, True)
        dialog.minsize(400, 300)
        
        # Поиск
        search_frame = ttk.Frame(dialog)
        search_frame.pack(fill=tk.X, padx=20, pady=15)
        
        ttk.Label(search_frame, text="Search:", font=('Arial', 10)).pack(side=tk.LEFT, padx=(0, 10))
        
        search_var = tk.StringVar()
        search_entry = ttk.Entry(search_frame, textvariable=search_var, width=40)
        search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
        
        # Список моделей
        popular_models = [
            {"name": "llama3.2", "tags": ["7B", "latest"], "size": "4.7GB"},
            {"name": "llama3.2:3b", "tags": ["3B", "lightweight"], "size": "2.1GB"},
            {"name": "llama3.2:1b", "tags": ["1B", "tiny"], "size": "0.8GB"},
            {"name": "phi3", "tags": ["3B", "efficient"], "size": "2.4GB"},
            {"name": "phi3:mini", "tags": ["3B", "mini"], "size": "1.2GB"},
            {"name": "phi3:medium", "tags": ["7B", "medium"], "size": "4.3GB"},
            {"name": "mistral", "tags": ["7B", "popular"], "size": "4.1GB"},
            {"name": "mistral:7b", "tags": ["7B", "base"], "size": "4.1GB"},
            {"name": "gemma2", "tags": ["9B", "google"], "size": "5.7GB"},
            {"name": "gemma2:2b", "tags": ["2B", "small"], "size": "1.8GB"},
            {"name": "gemma2:9b", "tags": ["9B", "large"], "size": "5.7GB"},
            {"name": "qwen2.5", "tags": ["7B", "alibaba"], "size": "4.5GB"},
            {"name": "qwen2.5:7b", "tags": ["7B", "base"], "size": "4.5GB"},
            {"name": "qwen2.5:1.5b", "tags": ["1.5B", "tiny"], "size": "1.1GB"},
            {"name": "deepseek-r1", "tags": ["7B", "reasoning"], "size": "4.2GB"},
            {"name": "deepseek-r1:7b", "tags": ["7B", "base"], "size": "4.2GB"},
            {"name": "codellama", "tags": ["7B", "coding"], "size": "3.8GB"},
            {"name": "codellama:7b", "tags": ["7B", "base"], "size": "3.8GB"},
            {"name": "llava", "tags": ["7B", "vision"], "size": "4.5GB"},
            {"name": "llava:7b", "tags": ["7B", "base"], "size": "4.5GB"},
        ]
        
        tree_frame = ttk.Frame(dialog)
        tree_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        columns = ('Name', 'Tags', 'Size', 'Status')
        model_tree = ttk.Treeview(tree_frame, columns=columns, show='tree headings', height=15)
        
        model_tree.heading('#0', text='#')
        model_tree.heading('Name', text='Model Name')
        model_tree.heading('Tags', text='Tags')
        model_tree.heading('Size', text='Size')
        model_tree.heading('Status', text='Status')
        
        model_tree.column('#0', width=30)
        model_tree.column('Name', width=200)
        model_tree.column('Tags', width=150)
        model_tree.column('Size', width=100)
        model_tree.column('Status', width=100)
        
        scrollbar = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=model_tree.yview)
        model_tree.configure(yscrollcommand=scrollbar.set)
        
        model_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        def update_model_list(query=""):
            model_tree.delete(*model_tree.get_children())
            
            models_to_show = popular_models
            if query:
                query_lower = query.lower()
                models_to_show = [m for m in popular_models if query_lower in m['name'].lower()]
            
            if not models_to_show and query:
                model_tree.insert('', 'end', values=(query, "Custom", "Unknown", "🔍 Search"))
            else:
                for i, model in enumerate(models_to_show, 1):
                    model_tree.insert('', 'end', 
                                     values=(model['name'], ', '.join(model['tags']), 
                                            model['size'], "⭐ Popular"),
                                     text=str(i))
        
        def search_model():
            update_model_list(search_var.get().strip())
        
        update_model_list()
        search_entry.bind('<Return>', lambda e: search_model())
        
        # Кнопки
        btn_frame = ttk.Frame(dialog)
        btn_frame.pack(fill=tk.X, padx=10, pady=10)
        
        def install_selected():
            selection = model_tree.selection()
            if not selection:
                messagebox.showwarning("Warning", "Please select a model")
                return
            
            values = model_tree.item(selection[0])['values']
            if not values:
                return
            
            model_name = values[0]
            dialog.destroy()
            
            if messagebox.askyesno("Install Model", f"Download {model_name}?"):
                self.status_var.set(f"Installing {model_name}...")
                threading.Thread(target=self._install_model_async, 
                               args=(model_name,), daemon=True).start()
        
        ttk.Button(btn_frame, text="📥 Install", command=install_selected).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="🔍 Search", command=search_model).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="Custom URL", command=self.install_custom_model).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="Close", command=dialog.destroy).pack(side=tk.LEFT, padx=2)
    
    def install_custom_model(self):
        """Установка кастомной модели"""
        dialog = tk.Toplevel(self.root)
        dialog.title("Install Custom Model")
        dialog.geometry("500x150")
        dialog.resizable(False, False)
        
        ttk.Label(dialog, text="Model name:").pack(pady=(15, 5))
        
        entry = ttk.Entry(dialog, width=50)
        entry.pack(pady=5)
        
        def install():
            model = entry.get().strip()
            if not model:
                messagebox.showwarning("Warning", "Model name is required")
                return
            
            dialog.destroy()
            
            if messagebox.askyesno("Install Model", f"Download {model}?"):
                self.status_var.set(f"Installing {model}...")
                threading.Thread(target=self._install_model_async, 
                               args=(model,), daemon=True).start()
        
        btn_frame = ttk.Frame(dialog)
        btn_frame.pack(pady=10)
        
        ttk.Button(btn_frame, text="Install", command=install).pack(side=tk.LEFT, padx=5)
        ttk.Button(btn_frame, text="Cancel", command=dialog.destroy).pack(side=tk.LEFT, padx=5)
        entry.focus()
    
    def _install_model_async(self, model_name: str):
        """Асинхронная установка модели"""
        def progress_callback(line: str):
            if "pulling" in line.lower() or "downloading" in line.lower():
                msg = line.strip()
                if len(msg) > 80:
                    msg = msg[:77] + "..."
                self.root.after(0, lambda: self.status_var.set(f"📥 {msg}"))
        
        success = self.ollama_service.pull_model(model_name, progress_callback)
        
        if success:
            self.root.after(0, lambda: messagebox.showinfo("Success", f"Model {model_name} installed"))
            self.root.after(0, self.load_models)
            self.root.after(0, lambda: self.status_var.set(f"✅ Model {model_name} installed"))
            logger.info(f"Model installed: {model_name}")
        else:
            self.root.after(0, lambda: messagebox.showerror("Error", f"Failed to install {model_name}"))
            self.root.after(0, lambda: self.status_var.set("❌ Installation failed"))
            logger.error(f"Failed to install model: {model_name}")
    
    # ==================== ВКЛАДКА УПРАВЛЕНИЯ ====================
    
    def create_control_tab(self):
        """Вкладка управления"""
        # GPU
        gpu_frame = ttk.LabelFrame(self.control_frame, text="🎮 GPU Configuration", padding=10)
        gpu_frame.pack(fill=tk.X, pady=(0, 10))
        
        gpu_info = self.system_info.get('gpu', 'Unknown')
        ttk.Label(gpu_frame, text=f"Detected: {gpu_info}").pack(anchor=tk.W)
        
        gpu_controls = ttk.Frame(gpu_frame)
        gpu_controls.pack(fill=tk.X, pady=5)
        
        ttk.Label(gpu_controls, text="GPU Mode:").pack(side=tk.LEFT)
        
        gpu_var = tk.StringVar(value=self.config.get('gpu_mode', 'auto'))
        gpu_combo = ttk.Combobox(gpu_controls, textvariable=gpu_var, 
                                values=["auto", "nvidia", "rocm", "cpu"],
                                width=10)
        gpu_combo.pack(side=tk.LEFT, padx=10)
        
        def apply_gpu():
            mode = gpu_var.get()
            self.config.set('gpu_mode', mode)
            self.status_var.set(f"Configuring GPU: {mode}")
            threading.Thread(target=self._configure_gpu, args=(mode,), daemon=True).start()
        
        ttk.Button(gpu_controls, text="Apply", command=apply_gpu).pack(side=tk.LEFT)
        
        # Системная информация
        sys_frame = ttk.LabelFrame(self.control_frame, text="📊 System Details", padding=10)
        sys_frame.pack(fill=tk.BOTH, expand=True)
        
        info_text = scrolledtext.ScrolledText(sys_frame, wrap=tk.WORD, height=10)
        info_text.pack(fill=tk.BOTH, expand=True)
        
        for key, value in self.system_info.items():
            info_text.insert(tk.END, f"{key}: {value}\n")
        
        info_text.config(state=tk.DISABLED)
        
        # Кнопки
        btn_frame = ttk.Frame(self.control_frame)
        btn_frame.pack(pady=10)
        
        ttk.Button(btn_frame, text="🔄 Check Updates", command=self.check_updates).pack(side=tk.LEFT, padx=5)
        ttk.Button(btn_frame, text="📊 GPU Stats", command=self.show_gpu_stats).pack(side=tk.LEFT, padx=5)
        ttk.Button(btn_frame, text="📊 System Info", command=self.show_system_info).pack(side=tk.LEFT, padx=5)
    
    def _configure_gpu(self, mode: str):
        """Настройка GPU"""
        env_vars = os.environ.copy()
        
        if mode == "nvidia":
            if not shutil.which('nvidia-smi'):
                self.root.after(0, lambda: messagebox.showinfo("Info", 
                    "NVIDIA drivers not found.\n"
                    "Install: sudo pacman -S nvidia nvidia-utils cuda"))
                return
            env_vars['OLLAMA_GPU_OVERHEAD'] = '1'
        elif mode == "rocm":
            if not shutil.which('rocminfo'):
                self.root.after(0, lambda: messagebox.showinfo("Info",
                    "ROCm not found.\n"
                    "Install: sudo pacman -S rocm-hip-sdk rocm-opencl-runtime"))
                return
            env_vars['OLLAMA_ROCM_ENABLE'] = '1'
        elif mode == "cpu":
            env_vars['OLLAMA_GPU_OVERHEAD'] = '0'
        
        # Сохраняем
        ollama_env = Path.home() / '.ollama' / 'env'
        ollama_env.parent.mkdir(exist_ok=True)
        
        with open(ollama_env, 'w') as f:
            for key, value in env_vars.items():
                if key.startswith('OLLAMA_'):
                    f.write(f"{key}={value}\n")
        
        self.root.after(0, lambda: messagebox.showinfo("Success", 
            f"GPU mode configured to: {mode}\nRestart Ollama for changes."))
        self.status_var.set(f"GPU configured: {mode}")
        logger.info(f"GPU configured: {mode}")
    
    def check_updates(self):
        """Проверка обновлений"""
        self.status_var.set("Checking for updates...")
        
        try:
            response = requests.get('https://api.github.com/repos/ollama/ollama/releases/latest', 
                                   timeout=10)
            if response.status_code == 200:
                data = response.json()
                latest = data.get('tag_name', 'Unknown')
                current = OllamaService.get_version() or "Unknown"
                
                messagebox.showinfo("Updates",
                    f"Current: {current}\nLatest: {latest}\n\n"
                    f"Update: curl -fsSL https://ollama.com/install.sh | sh")
                
                self.status_var.set(f"Update check complete. Latest: {latest}")
                logger.info(f"Update check: current={current}, latest={latest}")
            else:
                self.status_var.set("Failed to check updates")
        except Exception as e:
            logger.error(f"Failed to check updates: {e}")
            messagebox.showerror("Error", f"Failed to check updates:\n{e}")
            self.status_var.set("Error")
    
    def show_gpu_stats(self):
        """Показать статистику GPU"""
        stats_window = tk.Toplevel(self.root)
        stats_window.title("📊 GPU Statistics")
        stats_window.geometry("600x400")
        stats_window.resizable(True, True)
        
        text = scrolledtext.ScrolledText(stats_window, wrap=tk.WORD)
        text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        # NVIDIA
        try:
            result = subprocess.run(['nvidia-smi'], capture_output=True, text=True, timeout=5)
            if result.returncode == 0:
                text.insert(tk.END, "=== NVIDIA GPU ===\n\n")
                text.insert(tk.END, result.stdout)
        except Exception as e:
            logger.debug(f"NVIDIA stats error: {e}")
        
        # ROCm
        try:
            result = subprocess.run(['rocminfo'], capture_output=True, text=True, timeout=5)
            if "gfx" in result.stdout.lower():
                text.insert(tk.END, "\n\n=== AMD ROCm ===\n\n")
                for line in result.stdout.split('\n'):
                    if any(x in line.lower() for x in ['gfx', 'version', 'name']):
                        text.insert(tk.END, line + '\n')
        except Exception as e:
            logger.debug(f"ROCm stats error: {e}")
        
        if text.get('1.0', tk.END).strip() == "":
            text.insert(tk.END, "No GPU statistics available")
        
        text.config(state=tk.DISABLED)
    
    def show_system_info(self):
        """Показать системную информацию"""
        info_window = tk.Toplevel(self.root)
        info_window.title("📊 System Information")
        info_window.geometry("700x500")
        info_window.resizable(True, True)
        
        text = scrolledtext.ScrolledText(info_window, wrap=tk.WORD)
        text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        info = []
        info.append("=" * 50)
        info.append("SYSTEM INFORMATION")
        info.append("=" * 50)
        info.append("")
        
        info.append(f"OS: {platform.system()} {platform.release()} {platform.version()}")
        info.append(f"Machine: {platform.machine()}")
        info.append(f"Processor: {platform.processor()}")
        info.append("")
        
        info.append("CPU Information:")
        info.append(f"  Physical cores: {psutil.cpu_count(logical=False)}")
        info.append(f"  Logical cores: {psutil.cpu_count(logical=True)}")
        if psutil.cpu_freq():
            info.append(f"  Max frequency: {psutil.cpu_freq().max:.2f}MHz")
        info.append("")
        
        mem = psutil.virtual_memory()
        info.append("Memory Information:")
        info.append(f"  Total: {mem.total / (1024**3):.2f} GB")
        info.append(f"  Available: {mem.available / (1024**3):.2f} GB")
        info.append(f"  Used: {mem.used / (1024**3):.2f} GB ({mem.percent}%)")
        info.append("")
        
        disk = psutil.disk_usage('/')
        info.append("Disk Information:")
        info.append(f"  Total: {disk.total / (1024**3):.2f} GB")
        info.append(f"  Used: {disk.used / (1024**3):.2f} GB ({disk.percent}%)")
        info.append(f"  Free: {disk.free / (1024**3):.2f} GB")
        info.append("")
        
        info.append("Network Information:")
        for iface, addrs in psutil.net_if_addrs().items():
            for addr in addrs:
                if addr.family == 2:
                    info.append(f"  {iface}: {addr.address}")
        info.append("")
        
        info.append(f"GPU: {self.system_info.get('gpu', 'Unknown')}")
        info.append("")
        
        info.append("Ollama Status:")
        info.append(f"  Installed: {self.ollama_installed}")
        info.append(f"  Running: {self.ollama_running}")
        info.append(f"  Models: {len(self.models_list)}")
        
        text.insert('1.0', '\n'.join(info))
        text.config(state=tk.DISABLED)
    
    # ==================== НАСТРОЙКИ ====================
    
    def create_settings_tab(self):
        """Вкладка настроек"""
        general_frame = ttk.LabelFrame(self.settings_frame, text="General Settings", padding=10)
        general_frame.pack(fill=tk.X, pady=5)
        
        # Тема
        theme_frame = ttk.Frame(general_frame)
        theme_frame.pack(fill=tk.X, pady=5)
        
        ttk.Label(theme_frame, text="Theme:").pack(side=tk.LEFT)
        theme_var = tk.StringVar(value=self.config.get('theme', 'clam'))
        theme_combo = ttk.Combobox(theme_frame, textvariable=theme_var,
                                   values=['clam', 'alt', 'vista', 'xpnative'],
                                   width=15)
        theme_combo.pack(side=tk.LEFT, padx=10)
        
        def apply_theme():
            theme = theme_var.get()
            self.config.set('theme', theme)
            style = ttk.Style()
            style.theme_use(theme)
            self.status_var.set(f"Theme changed to {theme}")
        
        ttk.Button(theme_frame, text="Apply Theme", command=apply_theme).pack(side=tk.LEFT)
        
        # URL
        url_frame = ttk.Frame(general_frame)
        url_frame.pack(fill=tk.X, pady=5)
        
        ttk.Label(url_frame, text="Ollama URL:").pack(side=tk.LEFT)
        url_var = tk.StringVar(value=self.config.get('ollama_url', 'http://localhost:11434'))
        url_entry = ttk.Entry(url_frame, textvariable=url_var, width=40)
        url_entry.pack(side=tk.LEFT, padx=10, fill=tk.X, expand=True)
        
        def apply_url():
            url = url_var.get().strip()
            if url:
                self.config.set('ollama_url', url)
                self.ollama_service = OllamaService(url)
                self.status_var.set(f"Ollama URL updated to {url}")
                logger.info(f"Ollama URL updated: {url}")
        
        ttk.Button(url_frame, text="Apply URL", command=apply_url).pack(side=tk.LEFT)
        
        # Управление данными
        data_frame = ttk.LabelFrame(self.settings_frame, text="Data Management", padding=10)
        data_frame.pack(fill=tk.X, pady=5)
        
        def clear_cache():
            if messagebox.askyesno("Clear Cache", "Clear all cached data?"):
                try:
                    cache_dir = Path.home() / '.ollama-chat-history'
                    if cache_dir.exists():
                        shutil.rmtree(cache_dir)
                    messagebox.showinfo("Success", "Cache cleared")
                    self.status_var.set("Cache cleared")
                    logger.info("Cache cleared")
                except Exception as e:
                    messagebox.showerror("Error", f"Failed to clear cache: {e}")
        
        ttk.Button(data_frame, text="🗑️ Clear Chat History", command=clear_cache).pack(side=tk.LEFT, padx=5)
        
        def reset_config():
            if messagebox.askyesno("Reset Config", "Reset all settings to defaults?"):
                self.config._config = self.config.DEFAULTS.copy()
                self.config.save()
                messagebox.showinfo("Success", "Settings reset to defaults")
                self.status_var.set("Settings reset")
                logger.info("Settings reset")
        
        ttk.Button(data_frame, text="🔄 Reset Settings", command=reset_config).pack(side=tk.LEFT, padx=5)
        
        # О программе
        about_frame = ttk.LabelFrame(self.settings_frame, text="About", padding=10)
        about_frame.pack(fill=tk.X, pady=5)
        
        about_text = """
        Ollama Manager v2.0
        
        Full-featured GUI manager for Ollama.
        
        Features:
        • Model management (install, update, delete)
        • Chat interface with session history
        • GPU configuration
        • System monitoring
        • Export/Import models
        • Multi-session support
        
        Created with Python and Tkinter
        """
        
        ttk.Label(about_frame, text=about_text, justify=tk.LEFT).pack(anchor=tk.W)
    
    # ==================== ЛОГИ ====================
    
    def create_logs_tab(self):
        """Вкладка логов"""
        frame = ttk.Frame(self.logs_frame)
        frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        btn_frame = ttk.Frame(frame)
        btn_frame.pack(fill=tk.X, pady=(0, 10))
        
        ttk.Button(btn_frame, text="🔄 Refresh", command=self.refresh_logs).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="🗑️ Clear", command=self.clear_logs).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="📂 Open Log File", command=self.open_log_file).pack(side=tk.LEFT, padx=2)
        
        self.log_text = scrolledtext.ScrolledText(frame, wrap=tk.WORD)
        self.log_text.pack(fill=tk.BOTH, expand=True)
        self.refresh_logs()
    
    def refresh_logs(self):
        """Обновление логов"""
        if LOG_FILE.exists():
            try:
                with open(LOG_FILE, 'r', encoding='utf-8') as f:
                    content = f.read()
                    self.log_text.delete('1.0', tk.END)
                    self.log_text.insert('1.0', content)
                    self.log_text.see(tk.END)
                    self.status_var.set("Logs refreshed")
            except Exception as e:
                self.log_text.delete('1.0', tk.END)
                self.log_text.insert('1.0', f"Error reading log: {e}")
        else:
            self.log_text.delete('1.0', tk.END)
            self.log_text.insert('1.0', "No log file found")
    
    def clear_logs(self):
        """Очистка логов"""
        if messagebox.askyesno("Clear Logs", "Clear all logs?"):
            try:
                if LOG_FILE.exists():
                    with open(LOG_FILE, 'w', encoding='utf-8') as f:
                        f.write("")
                self.log_text.delete('1.0', tk.END)
                self.status_var.set("Logs cleared")
                logger.info("Logs cleared")
            except Exception as e:
                messagebox.showerror("Error", f"Failed to clear logs: {e}")
    
    def open_log_file(self):
        """Открыть файл логов"""
        if LOG_FILE.exists():
            try:
                if platform.system() == "Windows":
                    os.startfile(str(LOG_FILE))
                elif platform.system() == "Darwin":
                    subprocess.run(['open', str(LOG_FILE)])
                else:
                    subprocess.run(['xdg-open', str(LOG_FILE)])
            except Exception as e:
                messagebox.showerror("Error", f"Failed to open log file: {e}")
        else:
            messagebox.showinfo("Info", "Log file not found")

# ==================== ЗАПУСК ====================

def main():
    """Точка входа"""
    try:
        root = tk.Tk()
        app = OllamaManagerApp(root)
        root.mainloop()
    except KeyboardInterrupt:
        print("\nShutting down...")
        sys.exit(0)
    except Exception as e:
        logger.error(f"Fatal error: {e}", exc_info=True)
        print(f"Error: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()
