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

323 lines
11 KiB
Python

"""
Superposition de suggestion pour afficher les actions suggérées
avec surlignage visuel des éléments UI
"""
from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QPushButton
from PyQt5.QtCore import Qt, QRect, QTimer, pyqtSignal
from PyQt5.QtGui import QPainter, QColor, QPen, QFont, QBrush
from typing import Optional, Dict, Any, Tuple
import logging
class SuggestionOverlay(QWidget):
"""
Superposition transparente pour afficher les suggestions d'actions
avec surlignage visuel des éléments UI suggérés
"""
# Signal émis quand l'utilisateur donne un retour
feedback_received = pyqtSignal(str) # "accept", "reject", "correct"
def __init__(self, decision: Dict[str, Any], parent=None):
"""
Initialiser la superposition de suggestion
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)
}
parent: Widget parent (optionnel)
"""
super().__init__(parent)
self.decision = decision
self.logger = logging.getLogger(__name__)
self.feedback_result = None
self.init_ui()
self.setup_shortcuts()
self.logger.info(f"SuggestionOverlay créée pour action: {decision.get('action_type')}")
def init_ui(self):
"""Initialiser l'interface de la superposition"""
# Fenêtre sans bordure, toujours au-dessus, transparente
self.setWindowFlags(
Qt.WindowStaysOnTopHint |
Qt.FramelessWindowHint |
Qt.Tool
)
self.setAttribute(Qt.WA_TranslucentBackground)
# Plein écran
from PyQt5.QtWidgets import QApplication
screen = QApplication.primaryScreen().geometry()
self.setGeometry(screen)
# Extraire les informations de la décision
action_type = self.decision.get("action_type", "action")
target = self.decision.get("target_element", "élément")
confidence = self.decision.get("confidence", 0.0)
description = self.decision.get("description", "")
# Créer le panneau d'information
self.info_panel = QWidget(self)
self.info_panel.setStyleSheet("""
QWidget {
background-color: rgba(33, 150, 243, 230);
border-radius: 10px;
padding: 15px;
}
""")
panel_layout = QVBoxLayout()
self.info_panel.setLayout(panel_layout)
# Titre
title_label = QLabel("🤝 Suggestion d'Action")
title_label.setFont(QFont("Arial", 14, QFont.Bold))
title_label.setStyleSheet("color: white;")
panel_layout.addWidget(title_label)
# Détails de l'action
action_text = f"Action: {action_type.upper()}"
action_label = QLabel(action_text)
action_label.setFont(QFont("Arial", 11))
action_label.setStyleSheet("color: white;")
panel_layout.addWidget(action_label)
target_text = f"Élément: {target}"
target_label = QLabel(target_text)
target_label.setFont(QFont("Arial", 11))
target_label.setStyleSheet("color: white;")
panel_layout.addWidget(target_label)
confidence_text = f"Confiance: {confidence * 100:.1f}%"
confidence_label = QLabel(confidence_text)
confidence_label.setFont(QFont("Arial", 11))
confidence_label.setStyleSheet("color: white;")
panel_layout.addWidget(confidence_label)
if description:
desc_label = QLabel(description)
desc_label.setFont(QFont("Arial", 10))
desc_label.setStyleSheet("color: rgba(255, 255, 255, 200);")
desc_label.setWordWrap(True)
panel_layout.addWidget(desc_label)
# Boutons de contrôle
button_layout = QHBoxLayout()
accept_button = QPushButton("✓ Accepter (Entrée)")
accept_button.setFont(QFont("Arial", 10))
accept_button.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
border: none;
padding: 8px 15px;
border-radius: 5px;
}
QPushButton:hover {
background-color: #45a049;
}
""")
accept_button.clicked.connect(lambda: self.on_feedback("accept"))
button_layout.addWidget(accept_button)
reject_button = QPushButton("✗ Refuser (Échap)")
reject_button.setFont(QFont("Arial", 10))
reject_button.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: white;
border: none;
padding: 8px 15px;
border-radius: 5px;
}
QPushButton:hover {
background-color: #da190b;
}
""")
reject_button.clicked.connect(lambda: self.on_feedback("reject"))
button_layout.addWidget(reject_button)
correct_button = QPushButton("✎ Corriger (Alt+C)")
correct_button.setFont(QFont("Arial", 10))
correct_button.setStyleSheet("""
QPushButton {
background-color: #FF9800;
color: white;
border: none;
padding: 8px 15px;
border-radius: 5px;
}
QPushButton:hover {
background-color: #e68900;
}
""")
correct_button.clicked.connect(lambda: self.on_feedback("correct"))
button_layout.addWidget(correct_button)
panel_layout.addLayout(button_layout)
# Positionner le panneau en haut au centre
panel_width = 400
panel_height = 250
screen_width = self.width()
panel_x = (screen_width - panel_width) // 2
panel_y = 50
self.info_panel.setGeometry(panel_x, panel_y, panel_width, panel_height)
def setup_shortcuts(self):
"""Configurer les raccourcis clavier"""
from PyQt5.QtWidgets import QShortcut
from PyQt5.QtGui import QKeySequence
# Entrée pour accepter
accept_shortcut = QShortcut(QKeySequence(Qt.Key_Return), self)
accept_shortcut.activated.connect(lambda: self.on_feedback("accept"))
enter_shortcut = QShortcut(QKeySequence(Qt.Key_Enter), self)
enter_shortcut.activated.connect(lambda: self.on_feedback("accept"))
# Échap pour refuser
reject_shortcut = QShortcut(QKeySequence(Qt.Key_Escape), self)
reject_shortcut.activated.connect(lambda: self.on_feedback("reject"))
# Alt+C pour corriger
correct_shortcut = QShortcut(QKeySequence("Alt+C"), self)
correct_shortcut.activated.connect(lambda: self.on_feedback("correct"))
def paintEvent(self, event):
"""Dessiner la superposition avec surlignage de l'élément"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# Fond semi-transparent
painter.fillRect(self.rect(), QColor(0, 0, 0, 100))
# Surligner l'élément UI suggéré
bbox = self.decision.get("bbox")
if bbox:
x, y, w, h = bbox
# Rectangle de surlignage avec bordure épaisse
pen = QPen(QColor(33, 150, 243), 4, Qt.SolidLine)
painter.setPen(pen)
# Fond légèrement transparent pour l'élément
brush = QBrush(QColor(33, 150, 243, 50))
painter.setBrush(brush)
# Dessiner le rectangle de surlignage
highlight_rect = QRect(x - 5, y - 5, w + 10, h + 10)
painter.drawRect(highlight_rect)
# Ajouter une animation de pulsation (optionnel)
# Pour l'instant, on garde un surlignage statique
def on_feedback(self, feedback: str):
"""
Gestionnaire de retour utilisateur
Args:
feedback: Type de retour ("accept", "reject", "correct")
"""
self.feedback_result = feedback
self.feedback_received.emit(feedback)
self.logger.info(f"Retour reçu: {feedback}")
self.close()
def wait_for_feedback(self) -> str:
"""
Attendre le retour utilisateur (bloquant)
Returns:
str: Type de retour ("accept", "reject", "correct")
"""
# Afficher la superposition
self.show()
# Boucle d'événements locale pour attendre le retour
from PyQt5.QtCore import QEventLoop
loop = QEventLoop()
self.feedback_received.connect(loop.quit)
loop.exec_()
return self.feedback_result or "reject"
def show_suggestion(self, decision: Dict[str, Any]) -> str:
"""
Méthode statique pour afficher une suggestion et attendre le retour
Args:
decision: Dictionnaire contenant les détails de la décision
Returns:
str: Type de retour ("accept", "reject", "correct")
"""
overlay = SuggestionOverlay(decision)
return overlay.wait_for_feedback()
class AnimatedSuggestionOverlay(SuggestionOverlay):
"""
Version animée de la superposition avec effet de pulsation
"""
def __init__(self, decision: Dict[str, Any], parent=None):
super().__init__(decision, parent)
# Timer pour l'animation
self.animation_timer = QTimer(self)
self.animation_timer.timeout.connect(self.animate)
self.animation_timer.start(500) # Pulse toutes les 500ms
self.pulse_state = 0
def animate(self):
"""Animer le surlignage avec effet de pulsation"""
self.pulse_state = (self.pulse_state + 1) % 2
self.update() # Redessiner
def paintEvent(self, event):
"""Dessiner avec animation de pulsation"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# Fond semi-transparent
painter.fillRect(self.rect(), QColor(0, 0, 0, 100))
# Surligner l'élément UI suggéré avec pulsation
bbox = self.decision.get("bbox")
if bbox:
x, y, w, h = bbox
# Varier l'épaisseur et l'opacité selon l'état de pulsation
pen_width = 4 if self.pulse_state == 0 else 6
alpha = 200 if self.pulse_state == 0 else 255
pen = QPen(QColor(33, 150, 243, alpha), pen_width, Qt.SolidLine)
painter.setPen(pen)
brush_alpha = 50 if self.pulse_state == 0 else 80
brush = QBrush(QColor(33, 150, 243, brush_alpha))
painter.setBrush(brush)
# Dessiner le rectangle de surlignage
padding = 5 if self.pulse_state == 0 else 8
highlight_rect = QRect(x - padding, y - padding, w + 2*padding, h + 2*padding)
painter.drawRect(highlight_rect)
def close(self):
"""Arrêter l'animation avant de fermer"""
self.animation_timer.stop()
super().close()