""" ProfileSelectorDialog - Dialog per selezione profilo filosofico all'avvio """ import os from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QButtonGroup, QScrollArea, QWidget ) from PyQt6.QtCore import Qt from PyQt6.QtGui import QFont from core.config_manager import ConfigManager class ProfileSelectorDialog(QDialog): """Dialog modale per selezione profilo filosofico""" def __init__(self, last_profile=None): super().__init__() self.selected_profile = last_profile or 'aurelio' # Default # Carica profili dinamicamente da config.yaml self.config_manager = None self.profiles = {} self.load_profiles() self.init_ui() def load_profiles(self): """Carica profili dinamicamente da config.yaml""" try: # Calcola config path config_path = self._get_config_path() # Inizializza ConfigManager self.config_manager = ConfigManager(config_path) # Carica profili self.profiles = self.config_manager.get_all_profiles() # Valida che selected_profile esista if self.selected_profile not in self.profiles: print(f"[ProfileSelector] Profilo '{self.selected_profile}' non trovato, fallback a primo disponibile") if self.profiles: self.selected_profile = sorted(self.profiles.keys())[0] else: self.selected_profile = 'aurelio' except Exception as e: print(f"[ProfileSelector] Errore caricamento profili: {e}") print("[ProfileSelector] Uso emergency defaults") self.profiles = self._get_emergency_defaults() self.selected_profile = 'aurelio' def _get_config_path(self): """Calcola path a config.yaml""" # ProfileSelectorDialog è in desktop/ui/ # 3x dirname porta a Jarvis-Cognitive/ project_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) return os.path.join(project_root, 'config', 'config.yaml') def _get_emergency_defaults(self): """Profilo minimo di emergenza se config non caricabile""" return { 'aurelio': { 'name': 'Marco Aurelio', 'subtitle': 'Imperatore Filosofo', 'icon': '👑', 'description': 'Profilo di sistema (emergency mode)', 'color': '#8B4513', 'period': '121-180 d.C.', 'services': [] } } def init_ui(self): """Inizializza interfaccia dialog""" self.setWindowTitle("Jarvis Cognitive - Scegli il tuo Filosofo") self.setModal(True) self.setMinimumWidth(700) self.setMinimumHeight(450) layout = QVBoxLayout() layout.setSpacing(20) layout.setContentsMargins(30, 30, 30, 30) # Header header = QLabel("Scegli il tuo Assistente") header_font = QFont() header_font.setPointSize(20) header_font.setBold(True) header.setFont(header_font) header.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(header) subtitle = QLabel( "Filosofo stoico o advisor finanziario: ogni profilo ha memoria separata e personalità unica.\n" "Per cambiare profilo dovrai chiudere e riaprire l'applicazione." ) subtitle.setAlignment(Qt.AlignmentFlag.AlignCenter) subtitle.setStyleSheet("color: #666; font-size: 12px; margin-bottom: 10px;") layout.addWidget(subtitle) # Profile cards con bottoni modifica (dinamico da config.yaml) # Usa scroll area se ci sono molti profili if len(self.profiles) > 3: scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) scroll_widget = QWidget() cards_layout = QHBoxLayout() cards_layout.setSpacing(15) scroll_widget.setLayout(cards_layout) scroll_area.setWidget(scroll_widget) else: cards_layout = QHBoxLayout() cards_layout.setSpacing(15) self.button_group = QButtonGroup(self) # Genera card dinamicamente per ogni profilo in config.yaml for profile_id in sorted(self.profiles.keys()): card_container = self._create_profile_card(profile_id) cards_layout.addWidget(card_container) if len(self.profiles) > 3: layout.addWidget(scroll_area) else: layout.addLayout(cards_layout) # Bottone Aggiungi Nuovo Profilo add_profile_btn = QPushButton("➕ Aggiungi Nuovo Profilo") add_profile_btn.setMinimumHeight(40) add_profile_btn.clicked.connect(self.add_new_profile) add_profile_btn.setStyleSheet(""" QPushButton { background-color: #2196F3; color: white; font-weight: bold; padding: 10px; border-radius: 5px; font-size: 13px; } QPushButton:hover { background-color: #1976D2; } """) layout.addWidget(add_profile_btn) # Action buttons actions_layout = QHBoxLayout() actions_layout.addStretch() cancel_btn = QPushButton("Annulla") cancel_btn.setMinimumWidth(120) cancel_btn.clicked.connect(self.reject) actions_layout.addWidget(cancel_btn) confirm_btn = QPushButton("Avvia") confirm_btn.setMinimumWidth(120) confirm_btn.setDefault(True) confirm_btn.clicked.connect(self.accept) confirm_btn.setStyleSheet(""" QPushButton { background-color: #4CAF50; color: white; font-weight: bold; padding: 10px; border-radius: 5px; } QPushButton:hover { background-color: #45a049; } """) actions_layout.addWidget(confirm_btn) layout.addLayout(actions_layout) self.setLayout(layout) def _create_profile_card(self, profile_id): """Crea card profilo con bottone selezione, modifica e elimina""" from PyQt6.QtWidgets import QWidget, QVBoxLayout SYSTEM_PROFILES = ['aurelio', 'warren'] container = QWidget() container_layout = QVBoxLayout() container_layout.setSpacing(8) container_layout.setContentsMargins(0, 0, 0, 0) # Bottone selezione profilo (card principale) profile_btn = self._create_profile_button(profile_id) container_layout.addWidget(profile_btn) # Layout per bottoni modifica/elimina buttons_layout = QHBoxLayout() buttons_layout.setSpacing(8) # Bottone modifica edit_btn = QPushButton("⚙️ Modifica") edit_btn.setMinimumHeight(35) edit_btn.clicked.connect(lambda: self.edit_profile(profile_id)) edit_btn.setStyleSheet(""" QPushButton { background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 5px; padding: 6px; font-size: 11px; color: #666; } QPushButton:hover { background-color: #e0e0e0; border-color: #999; color: #333; } """) buttons_layout.addWidget(edit_btn) # Bottone elimina (solo per profili custom, non di sistema) if profile_id not in SYSTEM_PROFILES: delete_btn = QPushButton("🗑️ Elimina") delete_btn.setMinimumHeight(35) delete_btn.clicked.connect(lambda: self.delete_profile_request(profile_id)) delete_btn.setToolTip("Elimina profilo") delete_btn.setStyleSheet(""" QPushButton { background-color: #ffebee; border: 1px solid #ef9a9a; border-radius: 5px; padding: 6px; font-size: 11px; color: #c62828; } QPushButton:hover { background-color: #ef5350; border-color: #c62828; color: white; } """) buttons_layout.addWidget(delete_btn) container_layout.addLayout(buttons_layout) container.setLayout(container_layout) return container def _create_profile_button(self, profile_id): """Crea bottone profilo con stile card""" profile = self.profiles[profile_id] # Container button (stile card) btn = QPushButton() btn.setCheckable(True) btn.setMinimumHeight(200) btn.setMinimumWidth(200) # Contenuto button btn.setText( f"{profile['icon']}\n\n" f"{profile['name']}\n" f"{profile['subtitle']}\n" f"{profile['period']}\n\n" f"{profile['description']}" ) # Styling btn.setStyleSheet(f""" QPushButton {{ background-color: white; border: 2px solid #ddd; border-radius: 10px; padding: 20px; text-align: center; font-size: 11px; }} QPushButton:hover {{ border-color: {profile['color']}; background-color: #f9f9f9; }} QPushButton:checked {{ border: 3px solid {profile['color']}; background-color: {profile['color']}22; font-weight: bold; }} """) # Select default if profile_id == self.selected_profile: btn.setChecked(True) # Connect signal btn.clicked.connect(lambda checked, pid=profile_id: self._on_profile_selected(pid)) return btn def _on_profile_selected(self, profile_id): """Handler selezione profilo""" self.selected_profile = profile_id def get_selected_profile(self): """Restituisce profilo selezionato""" return self.selected_profile def edit_profile(self, profile_id): """Apri editor per modificare profilo esistente""" from desktop.ui.profile_editor_dialog import ProfileEditorDialog from PyQt6.QtWidgets import QMessageBox # Crea un mock kernel temporaneo solo per passare il profile_id class MockKernel: def __init__(self, profile): self.active_profile = profile mock_kernel = MockKernel(profile_id) dialog = ProfileEditorDialog(mock_kernel, mode='edit', parent=self) if dialog.exec() == QDialog.DialogCode.Accepted: QMessageBox.information( self, "Profilo Aggiornato", f"Le modifiche al profilo '{profile_id}' sono state salvate.\n\n" "Le modifiche saranno applicate al prossimo avvio." ) def add_new_profile(self): """Apri editor per creare nuovo profilo""" from desktop.ui.profile_editor_dialog import ProfileEditorDialog from PyQt6.QtWidgets import QMessageBox # Crea un mock kernel per modalità creazione class MockKernel: def __init__(self): self.active_profile = None # Nessun profilo = modalità creazione mock_kernel = MockKernel() dialog = ProfileEditorDialog(mock_kernel, mode='create', parent=self) if dialog.exec() == QDialog.DialogCode.Accepted: # Ricarica profili e refresh UI self.load_profiles() self.refresh_ui() QMessageBox.information( self, "Profilo Creato", "Nuovo profilo creato con successo!\n\n" "Il profilo è ora visibile nella lista e pronto all'uso." ) def refresh_ui(self): """Ricostruisce UI dopo modifica profili (senza chiudere dialog)""" # Rimuovi layout corrente old_layout = self.layout() if old_layout: # Rimuovi tutti i widget while old_layout.count(): child = old_layout.takeAt(0) if child.widget(): child.widget().deleteLater() # Rimuovi layout QWidget().setLayout(old_layout) # Ricrea UI self.init_ui() def delete_profile_request(self, profile_id): """ Handler richiesta eliminazione profilo Mostra conferma con dettagli, elimina da config.yaml e directory, poi ricarica UI. """ from PyQt6.QtWidgets import QMessageBox import shutil SYSTEM_PROFILES = ['aurelio', 'warren'] # 1. Validazione: non può essere profilo di sistema if profile_id in SYSTEM_PROFILES: QMessageBox.warning( self, "Operazione Non Permessa", f"Il profilo '{profile_id}' è un profilo di sistema e non può essere eliminato." ) return # 2. Calcola cosa verrà eliminato profile_dir = self._get_profile_directory(profile_id) dir_exists = os.path.exists(profile_dir) # 3. Dialog conferma con dettagli msg = QMessageBox(self) msg.setIcon(QMessageBox.Icon.Warning) msg.setWindowTitle("Conferma Eliminazione Profilo") msg.setText(f"Sei sicuro di voler eliminare il profilo '{profile_id}'?") details = [ "Verranno eliminati:", "• Configurazione da config.yaml", ] if dir_exists: details.append(f"• Directory {profile_dir}/") details.append(" - Documenti RAG (ChromaDB, doc_store, source_docs)") details.append(" - Memoria chat (memoria_chat.sqlite)") details.append(" - Log") msg.setDetailedText("\n".join(details)) msg.setStandardButtons( QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) msg.setDefaultButton(QMessageBox.StandardButton.No) if msg.exec() != QMessageBox.StandardButton.Yes: return # 4. Elimina da config.yaml try: self.config_manager.delete_profile(profile_id) except Exception as e: QMessageBox.critical( self, "Errore", f"Impossibile eliminare profilo da config:\n{str(e)}" ) return # 5. Elimina directory se esiste if dir_exists: try: shutil.rmtree(profile_dir) print(f"[ProfileSelector] Directory eliminata: {profile_dir}") except Exception as e: QMessageBox.warning( self, "Eliminazione Parziale", f"Profilo rimosso da config ma errore eliminando directory:\n{str(e)}" ) # 6. Se profilo eliminato era selezionato, seleziona primo disponibile if self.selected_profile == profile_id: self.selected_profile = 'aurelio' # Fallback sicuro # 7. Reload profili e refresh UI self.load_profiles() self.refresh_ui() # 8. Messaggio successo QMessageBox.information( self, "Profilo Eliminato", f"Il profilo '{profile_id}' è stato eliminato con successo." ) def _get_profile_directory(self, profile_id): """ Calcola path directory profilo Args: profile_id: ID del profilo Returns: Path assoluto alla directory del profilo """ project_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) return os.path.join(project_root, 'data', 'agents', profile_id) def load_last_profile(): """ Carica ultimo profilo usato da file persistente Valida che il profilo esista in config.yaml prima di ritornarlo. Se non esiste, ritorna il primo profilo disponibile o 'aurelio' come fallback. """ config_dir = os.path.expanduser("~/.config/jarvis") profile_file = os.path.join(config_dir, "last_profile.txt") last_profile = None if os.path.exists(profile_file): try: with open(profile_file, 'r') as f: last_profile = f.read().strip() except Exception: pass # Valida che il profilo esista in config.yaml try: # Calcola config path import sys if hasattr(sys.modules[__name__], '__file__'): module_dir = os.path.dirname(__file__) else: module_dir = os.getcwd() project_root = os.path.dirname(os.path.dirname(module_dir)) config_path = os.path.join(project_root, 'config', 'config.yaml') # Carica config e controlla se profilo esiste config_manager = ConfigManager(config_path) available_profiles = config_manager.get_all_profiles() if last_profile and last_profile in available_profiles: return last_profile # Se last_profile non valido, ritorna primo disponibile if available_profiles: first_profile = sorted(available_profiles.keys())[0] print(f"[load_last_profile] Profilo '{last_profile}' non trovato, uso '{first_profile}'") return first_profile except Exception as e: print(f"[load_last_profile] Errore validazione: {e}") # Fallback finale return 'aurelio' def save_last_profile(profile_name): """Salva ultimo profilo usato""" config_dir = os.path.expanduser("~/.config/jarvis") os.makedirs(config_dir, exist_ok=True) profile_file = os.path.join(config_dir, "last_profile.txt") try: with open(profile_file, 'w') as f: f.write(profile_name) except Exception as e: print(f"[WARNING] Impossibile salvare last_profile: {e}")