Files
Geniusia_v2/geniusia2/gui/minimal_gui.py
2026-03-05 00:20:25 +01:00

485 lines
18 KiB
Python

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