""" Interface GUI minimale pour RPA Vision V2 Fournit indicateurs de mode, contrôles et notifications """ from PyQt5.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QSystemTrayIcon, QMenu ) from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QObject from PyQt5.QtGui import QIcon, QFont, QKeySequence from typing import Optional, Callable, Dict, Any import logging from .suggestion_overlay import SuggestionOverlay, AnimatedSuggestionOverlay class MinimalGUI(QMainWindow): """ Interface GUI minimale pour RPA Vision V2 Affiche les indicateurs de mode, contrôles et notifications """ # Signaux pour communication avec l'orchestrateur start_requested = pyqtSignal() stop_requested = pyqtSignal() pause_requested = pyqtSignal() emergency_stop_requested = pyqtSignal() def __init__(self, orchestrator=None): """ Initialiser l'interface GUI minimale Args: orchestrator: Instance de l'orchestrateur (optionnel) """ super().__init__() self.orchestrator = orchestrator self.logger = logging.getLogger(__name__) self.current_mode = "shadow" self.is_running = False # Icônes de mode self.mode_icons = { "shadow": "👀", "assist": "🤝", "auto": "🤖" } self.init_ui() self.setup_shortcuts() self.logger.info("MinimalGUI initialisée") def init_ui(self): """Initialiser l'interface utilisateur""" self.setWindowTitle("RPA Vision V2") self.setGeometry(100, 100, 400, 200) # Widget central central_widget = QWidget() self.setCentralWidget(central_widget) # Layout principal main_layout = QVBoxLayout() central_widget.setLayout(main_layout) # Indicateur de mode mode_layout = QHBoxLayout() mode_label_text = QLabel("Mode:") mode_label_text.setFont(QFont("Arial", 12)) mode_layout.addWidget(mode_label_text) self.mode_label = QLabel(f"{self.mode_icons['shadow']} Shadow") self.mode_label.setFont(QFont("Arial", 14, QFont.Bold)) self.mode_label.setStyleSheet("color: #2196F3; padding: 5px;") mode_layout.addWidget(self.mode_label) mode_layout.addStretch() main_layout.addLayout(mode_layout) # Indicateur d'état self.status_label = QLabel("État: Arrêté") self.status_label.setFont(QFont("Arial", 10)) self.status_label.setStyleSheet("color: #666; padding: 5px;") main_layout.addWidget(self.status_label) # Boutons de contrôle button_layout = QHBoxLayout() self.start_button = QPushButton("▶ Start") self.start_button.setFont(QFont("Arial", 11)) self.start_button.setStyleSheet(""" QPushButton { background-color: #4CAF50; color: white; border: none; padding: 10px 20px; border-radius: 5px; } QPushButton:hover { background-color: #45a049; } QPushButton:disabled { background-color: #cccccc; } """) self.start_button.clicked.connect(self.on_start_clicked) button_layout.addWidget(self.start_button) self.pause_button = QPushButton("⏸ Pause") self.pause_button.setFont(QFont("Arial", 11)) self.pause_button.setStyleSheet(""" QPushButton { background-color: #FF9800; color: white; border: none; padding: 10px 20px; border-radius: 5px; } QPushButton:hover { background-color: #e68900; } QPushButton:disabled { background-color: #cccccc; } """) self.pause_button.clicked.connect(self.on_pause_clicked) self.pause_button.setEnabled(False) button_layout.addWidget(self.pause_button) self.stop_button = QPushButton("⏹ Stop") self.stop_button.setFont(QFont("Arial", 11)) self.stop_button.setStyleSheet(""" QPushButton { background-color: #f44336; color: white; border: none; padding: 10px 20px; border-radius: 5px; } QPushButton:hover { background-color: #da190b; } QPushButton:disabled { background-color: #cccccc; } """) self.stop_button.clicked.connect(self.on_stop_clicked) self.stop_button.setEnabled(False) button_layout.addWidget(self.stop_button) main_layout.addLayout(button_layout) # Boutons de configuration config_layout = QHBoxLayout() # Bouton Whitelist self.whitelist_button = QPushButton("🛡️ Gérer la Liste Blanche") self.whitelist_button.setFont(QFont("Arial", 10)) self.whitelist_button.setStyleSheet(""" QPushButton { background-color: #2196F3; color: white; border: none; padding: 8px 15px; border-radius: 5px; } QPushButton:hover { background-color: #0b7dda; } """) self.whitelist_button.clicked.connect(self.on_whitelist_clicked) config_layout.addWidget(self.whitelist_button) # Bouton Mode Permissif self.permissive_button = QPushButton("🌍 Mode: Tout Autoriser") self.permissive_button.setFont(QFont("Arial", 10)) self.permissive_button.setCheckable(True) self.permissive_button.setChecked(True) # Activé par défaut self.permissive_button.setStyleSheet(""" QPushButton { background-color: #9C27B0; color: white; border: none; padding: 8px 15px; border-radius: 5px; } QPushButton:hover { background-color: #7B1FA2; } QPushButton:checked { background-color: #4CAF50; } """) self.permissive_button.clicked.connect(self.on_permissive_mode_clicked) config_layout.addWidget(self.permissive_button) main_layout.addLayout(config_layout) # Zone de notification self.notification_label = QLabel("") self.notification_label.setFont(QFont("Arial", 10)) self.notification_label.setWordWrap(True) self.notification_label.setStyleSheet("padding: 10px; border-radius: 5px;") main_layout.addWidget(self.notification_label) main_layout.addStretch() # Informations raccourcis shortcuts_label = QLabel( "Raccourcis: Ctrl+Pause = Arrêt d'urgence | " "Entrée = Accepter | Échap = Refuser | Alt+C = Corriger" ) shortcuts_label.setFont(QFont("Arial", 8)) shortcuts_label.setStyleSheet("color: #999; padding: 5px;") shortcuts_label.setWordWrap(True) main_layout.addWidget(shortcuts_label) def setup_shortcuts(self): """Configurer les raccourcis clavier""" # Note: Ctrl+Pause sera géré au niveau système via l'orchestrateur # car il doit fonctionner même quand la fenêtre n'a pas le focus pass def update_mode_indicator(self, mode: str): """ Mettre à jour l'indicateur de mode Args: mode: Mode actuel ("shadow", "assist", "auto") """ if mode not in self.mode_icons: self.logger.warning(f"Mode inconnu: {mode}") return self.current_mode = mode mode_names = { "shadow": "Shadow", "assist": "Assisté", "auto": "Autopilot" } mode_colors = { "shadow": "#2196F3", # Bleu "assist": "#FF9800", # Orange "auto": "#4CAF50" # Vert } icon = self.mode_icons[mode] name = mode_names.get(mode, mode.capitalize()) color = mode_colors.get(mode, "#666") self.mode_label.setText(f"{icon} {name}") self.mode_label.setStyleSheet(f"color: {color}; padding: 5px;") self.logger.info(f"Mode mis à jour: {mode}") def show_notification(self, message: str, notification_type: str = "info"): """ Afficher une notification dans l'interface Args: message: Message à afficher notification_type: Type de notification ("info", "success", "warning", "error") """ colors = { "info": "#2196F3", "success": "#4CAF50", "warning": "#FF9800", "error": "#f44336" } bg_colors = { "info": "#E3F2FD", "success": "#E8F5E9", "warning": "#FFF3E0", "error": "#FFEBEE" } color = colors.get(notification_type, colors["info"]) bg_color = bg_colors.get(notification_type, bg_colors["info"]) self.notification_label.setText(message) self.notification_label.setStyleSheet( f"padding: 10px; border-radius: 5px; " f"background-color: {bg_color}; color: {color}; " f"border-left: 4px solid {color};" ) # Auto-effacer après 5 secondes QTimer.singleShot(5000, lambda: self.notification_label.setText("")) self.logger.info(f"Notification [{notification_type}]: {message}") def on_start_clicked(self): """Gestionnaire du bouton Start""" self.is_running = True self.start_button.setEnabled(False) self.pause_button.setEnabled(True) self.stop_button.setEnabled(True) self.status_label.setText("État: En cours d'exécution") self.status_label.setStyleSheet("color: #4CAF50; padding: 5px;") self.start_requested.emit() self.show_notification( "Système démarré en mode Shadow 👀\n" "Effectuez des actions dans une fenêtre autorisée pour commencer l'apprentissage.", "success" ) self.logger.info("Démarrage demandé") def on_pause_clicked(self): """Gestionnaire du bouton Pause""" if self.is_running: self.is_running = False self.pause_button.setText("▶ Reprendre") self.status_label.setText("État: En pause") self.status_label.setStyleSheet("color: #FF9800; padding: 5px;") self.show_notification("Système en pause", "warning") self.logger.info("Pause demandée") else: self.is_running = True self.pause_button.setText("⏸ Pause") self.status_label.setText("État: En cours d'exécution") self.status_label.setStyleSheet("color: #4CAF50; padding: 5px;") self.show_notification("Système repris", "success") self.logger.info("Reprise demandée") self.pause_requested.emit() def on_stop_clicked(self): """Gestionnaire du bouton Stop""" self.is_running = False self.start_button.setEnabled(True) self.pause_button.setEnabled(False) self.pause_button.setText("⏸ Pause") self.stop_button.setEnabled(False) self.status_label.setText("État: Arrêté") self.status_label.setStyleSheet("color: #666; padding: 5px;") self.stop_requested.emit() self.show_notification("Système arrêté", "info") self.logger.info("Arrêt demandé") def on_whitelist_clicked(self): """Gestionnaire du bouton Whitelist""" from PyQt5.QtWidgets import QInputDialog, QMessageBox # Afficher un dialogue pour ajouter une fenêtre window_name, ok = QInputDialog.getText( self, "Ajouter à la Liste Blanche", "Nom de la fenêtre à autoriser:\n(ex: Firefox, Chrome, Terminal)" ) if ok and window_name: # Ajouter à la liste blanche via l'orchestrateur if self.orchestrator and self.orchestrator.whitelist_manager: try: self.orchestrator.whitelist_manager.add_window(window_name) self.show_notification(f"✅ '{window_name}' ajouté à la liste blanche", "success") self.logger.info(f"Fenêtre ajoutée à la liste blanche: {window_name}") except Exception as e: self.show_notification(f"❌ Erreur: {str(e)}", "error") self.logger.error(f"Erreur lors de l'ajout à la liste blanche: {e}") else: self.show_notification("❌ Gestionnaire de liste blanche non disponible", "error") def on_permissive_mode_clicked(self): """Gestionnaire du bouton Mode Permissif""" from PyQt5.QtWidgets import QMessageBox if self.permissive_button.isChecked(): # Activer le mode permissif (tout autoriser) reply = QMessageBox.question( self, "Mode Permissif", "⚠️ Activer le mode 'Tout Autoriser' ?\n\n" "L'application observera TOUTES les fenêtres,\n" "y compris les applications sensibles.\n\n" "Recommandé pour les workflows multi-applications.", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: if self.orchestrator: self.orchestrator.set_whitelist_enforcement(False) self.permissive_button.setText("🌍 Mode: Tout Autorisé ✓") self.show_notification( "Mode permissif activé\n" "Toutes les fenêtres seront observées", "warning" ) self.logger.info("Mode permissif activé") else: self.permissive_button.setChecked(False) else: # Désactiver le mode permissif (retour à la liste blanche) if self.orchestrator: self.orchestrator.set_whitelist_enforcement(True) self.permissive_button.setText("🛡️ Mode: Liste Blanche") self.show_notification( "Mode liste blanche activé\n" "Seules les fenêtres autorisées seront observées", "success" ) self.logger.info("Mode liste blanche activé") def on_emergency_stop(self): """Gestionnaire d'arrêt d'urgence (Ctrl+Pause)""" self.on_stop_clicked() self.show_notification("⚠️ ARRÊT D'URGENCE ACTIVÉ", "error") self.emergency_stop_requested.emit() self.logger.warning("Arrêt d'urgence activé") def show_suggestion(self, decision: Dict[str, Any], animated: bool = True) -> str: """ Afficher une suggestion d'action et attendre le retour utilisateur Args: decision: Dictionnaire contenant les détails de la décision { "action_type": str, "target_element": str, "bbox": (x, y, w, h), "confidence": float, "description": str (optionnel) } animated: Utiliser l'animation de pulsation (défaut: True) Returns: str: Type de retour ("accept", "reject", "correct") """ if animated: overlay = AnimatedSuggestionOverlay(decision) else: overlay = SuggestionOverlay(decision) feedback = overlay.wait_for_feedback() # Afficher une notification selon le retour if feedback == "accept": self.show_notification("✓ Action acceptée", "success") elif feedback == "reject": self.show_notification("✗ Action refusée", "warning") elif feedback == "correct": self.show_notification("✎ Correction demandée", "info") return feedback def hide_suggestion(self): """ Masque la suggestion actuelle """ # Pour l'instant, juste logger self.logger.info("Suggestion masquée") def show_execution_result(self, result: Dict[str, Any]): """ Affiche le résultat d'une exécution de suggestion Args: result: Résultat de l'exécution avec 'success', 'executed_actions', 'failed_actions' """ success = result.get("success", False) executed = result.get("executed_actions", 0) failed = result.get("failed_actions", 0) if success: message = f"✅ Suggestion exécutée avec succès ({executed} actions)" self.show_notification(message, "success") else: message = f"❌ Échec de l'exécution ({failed} actions échouées)" self.show_notification(message, "error") self.logger.info(f"Résultat d'exécution: {message}") def closeEvent(self, event): """Gestionnaire de fermeture de fenêtre""" if self.is_running: self.on_stop_clicked() event.accept()