485 lines
18 KiB
Python
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()
|