Initial commit
This commit is contained in:
484
geniusia2/gui/minimal_gui.py
Normal file
484
geniusia2/gui/minimal_gui.py
Normal file
@@ -0,0 +1,484 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user