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

613 lines
22 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Notifications de transition de mode et alertes système
pour informer l'utilisateur des changements d'état et problèmes détectés
"""
from PyQt5.QtWidgets import (
QWidget, QLabel, QVBoxLayout, QHBoxLayout, QPushButton, QDialog
)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QPropertyAnimation, QEasingCurve, QPoint
from PyQt5.QtGui import QFont, QPainter, QColor
from typing import Optional, Dict, Any, Callable
import logging
class TransitionNotification(QWidget):
"""
Notification pour les transitions de mode et alertes système
Supporte différents types: transition autopilot, baisse confiance,
violation liste blanche, changement UI
"""
# Types de notification
TYPE_AUTOPILOT_PROPOSAL = "autopilot_proposal"
TYPE_CONFIDENCE_DROP = "confidence_drop"
TYPE_WHITELIST_VIOLATION = "whitelist_violation"
TYPE_UI_CHANGE = "ui_change"
TYPE_MODE_TRANSITION = "mode_transition"
# Signaux
action_taken = pyqtSignal(str) # "accept", "reject", "dismiss"
def __init__(self,
notification_type: str,
data: Dict[str, Any],
requires_action: bool = False,
timeout_ms: int = 10000,
parent=None):
"""
Initialiser la notification de transition
Args:
notification_type: Type de notification (voir constantes TYPE_*)
data: Données spécifiques au type de notification
requires_action: Nécessite une action utilisateur (défaut: False)
timeout_ms: Timeout en millisecondes (défaut: 10000)
parent: Widget parent (optionnel)
"""
super().__init__(parent)
self.notification_type = notification_type
self.data = data
self.requires_action = requires_action
self.timeout_ms = timeout_ms
self.logger = logging.getLogger(__name__)
self.is_closed = False
self.init_ui()
self.setup_animation()
if not requires_action:
self.setup_timer()
self.logger.info(f"TransitionNotification créée: {notification_type}")
def init_ui(self):
"""Initialiser l'interface de la notification"""
# Fenêtre sans bordure, toujours au-dessus
self.setWindowFlags(
Qt.WindowStaysOnTopHint |
Qt.FramelessWindowHint |
Qt.Tool
)
self.setAttribute(Qt.WA_TranslucentBackground)
# Taille variable selon le type
if self.requires_action:
self.setFixedSize(400, 180)
else:
self.setFixedSize(380, 140)
# Positionner en haut à droite de l'écran
from PyQt5.QtWidgets import QApplication
screen = QApplication.primaryScreen().geometry()
x = screen.width() - self.width() - 20
y = 20
self.move(x, y)
# Widget conteneur avec fond
self.container = QWidget(self)
self.container.setGeometry(0, 0, self.width(), self.height())
# Déterminer le style selon le type
style_config = self._get_style_config()
self.container.setStyleSheet(f"""
QWidget {{
background-color: {style_config['bg_color']};
border-radius: 10px;
border: 2px solid {style_config['border_color']};
}}
""")
# Layout principal
main_layout = QVBoxLayout()
self.container.setLayout(main_layout)
main_layout.setContentsMargins(15, 12, 15, 12)
main_layout.setSpacing(10)
# En-tête avec icône et titre
header_layout = QHBoxLayout()
icon_label = QLabel(style_config['icon'])
icon_label.setFont(QFont("Arial", 18))
header_layout.addWidget(icon_label)
title_label = QLabel(style_config['title'])
title_label.setFont(QFont("Arial", 12, QFont.Bold))
title_label.setStyleSheet(f"color: {style_config['text_color']};")
header_layout.addWidget(title_label)
header_layout.addStretch()
# Bouton de fermeture (si pas d'action requise)
if not self.requires_action:
close_button = QPushButton("×")
close_button.setFont(QFont("Arial", 16, QFont.Bold))
close_button.setFixedSize(25, 25)
close_button.setStyleSheet(f"""
QPushButton {{
background-color: transparent;
color: {style_config['text_color']};
border: none;
}}
QPushButton:hover {{
background-color: rgba(0, 0, 0, 30);
border-radius: 12px;
}}
""")
close_button.clicked.connect(self.close_notification)
header_layout.addWidget(close_button)
main_layout.addLayout(header_layout)
# Message principal
message = self._get_message()
message_label = QLabel(message)
message_label.setFont(QFont("Arial", 10))
message_label.setStyleSheet(f"color: {style_config['text_color']};")
message_label.setWordWrap(True)
main_layout.addWidget(message_label)
# Détails additionnels
details = self._get_details()
if details:
details_label = QLabel(details)
details_label.setFont(QFont("Arial", 9))
details_label.setStyleSheet(f"color: {style_config['details_color']};")
details_label.setWordWrap(True)
main_layout.addWidget(details_label)
# Boutons d'action (si requis)
if self.requires_action:
button_layout = QHBoxLayout()
button_layout.setSpacing(10)
accept_button = QPushButton(style_config['accept_text'])
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_action("accept"))
button_layout.addWidget(accept_button)
reject_button = QPushButton(style_config['reject_text'])
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_action("reject"))
button_layout.addWidget(reject_button)
main_layout.addLayout(button_layout)
def _get_style_config(self) -> Dict[str, str]:
"""Obtenir la configuration de style selon le type"""
configs = {
self.TYPE_AUTOPILOT_PROPOSAL: {
'bg_color': '#E8F5E9',
'border_color': '#4CAF50',
'text_color': '#2E7D32',
'details_color': '#558B2F',
'icon': '🤖',
'title': 'Passage en Autopilot Proposé',
'accept_text': '✓ Activer Autopilot',
'reject_text': '✗ Rester en Assisté'
},
self.TYPE_CONFIDENCE_DROP: {
'bg_color': '#FFF3E0',
'border_color': '#FF9800',
'text_color': '#E65100',
'details_color': '#F57C00',
'icon': '⚠️',
'title': 'Baisse de Confiance Détectée',
'accept_text': '✓ Continuer',
'reject_text': '✗ Arrêter'
},
self.TYPE_WHITELIST_VIOLATION: {
'bg_color': '#FFEBEE',
'border_color': '#f44336',
'text_color': '#C62828',
'details_color': '#D32F2F',
'icon': '🚫',
'title': 'Violation de Liste Blanche',
'accept_text': '✓ Ajouter à la Liste',
'reject_text': '✗ Bloquer'
},
self.TYPE_UI_CHANGE: {
'bg_color': '#E3F2FD',
'border_color': '#2196F3',
'text_color': '#1565C0',
'details_color': '#1976D2',
'icon': '🔄',
'title': 'Changement d\'Interface Détecté',
'accept_text': '✓ Ré-observer',
'reject_text': '✗ Ignorer'
},
self.TYPE_MODE_TRANSITION: {
'bg_color': '#F3E5F5',
'border_color': '#9C27B0',
'text_color': '#6A1B9A',
'details_color': '#7B1FA2',
'icon': '🔀',
'title': 'Transition de Mode',
'accept_text': '✓ OK',
'reject_text': '✗ Annuler'
}
}
return configs.get(self.notification_type, configs[self.TYPE_MODE_TRANSITION])
def _get_message(self) -> str:
"""Obtenir le message principal selon le type"""
if self.notification_type == self.TYPE_AUTOPILOT_PROPOSAL:
task_name = self.data.get('task_name', 'Tâche')
observations = self.data.get('observation_count', 0)
concordance = self.data.get('concordance_rate', 0.0)
return (f"La tâche '{task_name}' a atteint les critères pour le mode Autopilot:\n"
f"{observations} observations\n"
f"{concordance * 100:.1f}% de concordance")
elif self.notification_type == self.TYPE_CONFIDENCE_DROP:
task_name = self.data.get('task_name', 'Tâche')
confidence = self.data.get('confidence_score', 0.0)
threshold = self.data.get('threshold', 0.90)
return (f"La confiance pour '{task_name}' est tombée à {confidence * 100:.1f}% "
f"(seuil: {threshold * 100:.0f}%). Retour au mode Assisté recommandé.")
elif self.notification_type == self.TYPE_WHITELIST_VIOLATION:
window = self.data.get('window_title', 'Fenêtre inconnue')
action = self.data.get('action_type', 'action')
return (f"Tentative d'{action} dans une fenêtre non autorisée:\n"
f"'{window}'\n"
f"Cette action a été bloquée pour des raisons de sécurité.")
elif self.notification_type == self.TYPE_UI_CHANGE:
task_name = self.data.get('task_name', 'Tâche')
similarity = self.data.get('similarity', 0.0)
return (f"Changement d'interface détecté pour '{task_name}'.\n"
f"Similarité visuelle: {similarity * 100:.1f}% (seuil: 70%)\n"
f"Une ré-observation est recommandée.")
elif self.notification_type == self.TYPE_MODE_TRANSITION:
from_mode = self.data.get('from_mode', 'inconnu')
to_mode = self.data.get('to_mode', 'inconnu')
task_name = self.data.get('task_name', 'Tâche')
mode_names = {
'shadow': 'Shadow 👀',
'assist': 'Assisté 🤝',
'auto': 'Autopilot 🤖'
}
return (f"Transition de mode pour '{task_name}':\n"
f"{mode_names.get(from_mode, from_mode)}"
f"{mode_names.get(to_mode, to_mode)}")
return "Notification système"
def _get_details(self) -> Optional[str]:
"""Obtenir les détails additionnels selon le type"""
if self.notification_type == self.TYPE_CONFIDENCE_DROP:
reason = self.data.get('reason', '')
if reason:
return f"Raison: {reason}"
elif self.notification_type == self.TYPE_MODE_TRANSITION:
reason = self.data.get('reason', '')
if reason:
reasons_fr = {
'low_confidence': 'Confiance insuffisante',
'high_concordance': 'Concordance élevée atteinte',
'user_request': 'Demande utilisateur',
'ui_change': 'Changement d\'interface',
'error': 'Erreur détectée'
}
return f"Raison: {reasons_fr.get(reason, reason)}"
return None
def setup_animation(self):
"""Configurer l'animation d'entrée"""
from PyQt5.QtWidgets import QApplication
screen = QApplication.primaryScreen().geometry()
start_x = screen.width()
end_x = screen.width() - self.width() - 20
self.animation = QPropertyAnimation(self, b"pos")
self.animation.setDuration(400)
self.animation.setStartValue(QPoint(start_x, 20))
self.animation.setEndValue(QPoint(end_x, 20))
self.animation.setEasingCurve(QEasingCurve.OutCubic)
def setup_timer(self):
"""Configurer le timer de fermeture automatique"""
self.close_timer = QTimer(self)
self.close_timer.timeout.connect(self.close_notification)
self.close_timer.setSingleShot(True)
def show_notification(self):
"""Afficher la notification avec animation"""
self.show()
self.animation.start()
if not self.requires_action:
self.close_timer.start(self.timeout_ms)
self.logger.info(f"Notification affichée: {self.notification_type}")
def close_notification(self):
"""Fermer la notification"""
if self.is_closed:
return
self.is_closed = True
if hasattr(self, 'close_timer'):
self.close_timer.stop()
self.action_taken.emit("dismiss")
self.logger.info("Notification fermée")
self.close()
def on_action(self, action: str):
"""
Gestionnaire d'action utilisateur
Args:
action: Type d'action ("accept" ou "reject")
"""
self.is_closed = True
self.action_taken.emit(action)
self.logger.info(f"Action prise: {action}")
self.close()
@staticmethod
def show_autopilot_proposal(task_name: str,
observation_count: int,
concordance_rate: float,
parent=None) -> 'TransitionNotification':
"""
Afficher une proposition de passage en Autopilot
Args:
task_name: Nom de la tâche
observation_count: Nombre d'observations
concordance_rate: Taux de concordance
parent: Widget parent
Returns:
Instance de TransitionNotification
"""
data = {
'task_name': task_name,
'observation_count': observation_count,
'concordance_rate': concordance_rate
}
notification = TransitionNotification(
TransitionNotification.TYPE_AUTOPILOT_PROPOSAL,
data,
requires_action=True,
parent=parent
)
notification.show_notification()
return notification
@staticmethod
def show_confidence_drop(task_name: str,
confidence_score: float,
threshold: float = 0.90,
reason: str = "",
parent=None) -> 'TransitionNotification':
"""
Afficher une alerte de baisse de confiance
Args:
task_name: Nom de la tâche
confidence_score: Score de confiance actuel
threshold: Seuil de confiance
reason: Raison de la baisse (optionnel)
parent: Widget parent
Returns:
Instance de TransitionNotification
"""
data = {
'task_name': task_name,
'confidence_score': confidence_score,
'threshold': threshold,
'reason': reason
}
notification = TransitionNotification(
TransitionNotification.TYPE_CONFIDENCE_DROP,
data,
requires_action=False,
timeout_ms=8000,
parent=parent
)
notification.show_notification()
return notification
@staticmethod
def show_whitelist_violation(window_title: str,
action_type: str,
parent=None) -> 'TransitionNotification':
"""
Afficher une alerte de violation de liste blanche
Args:
window_title: Titre de la fenêtre
action_type: Type d'action tentée
parent: Widget parent
Returns:
Instance de TransitionNotification
"""
data = {
'window_title': window_title,
'action_type': action_type
}
notification = TransitionNotification(
TransitionNotification.TYPE_WHITELIST_VIOLATION,
data,
requires_action=True,
parent=parent
)
notification.show_notification()
return notification
@staticmethod
def show_ui_change(task_name: str,
similarity: float,
parent=None) -> 'TransitionNotification':
"""
Afficher une alerte de changement d'interface
Args:
task_name: Nom de la tâche
similarity: Score de similarité visuelle
parent: Widget parent
Returns:
Instance de TransitionNotification
"""
data = {
'task_name': task_name,
'similarity': similarity
}
notification = TransitionNotification(
TransitionNotification.TYPE_UI_CHANGE,
data,
requires_action=True,
parent=parent
)
notification.show_notification()
return notification
@staticmethod
def show_mode_transition(task_name: str,
from_mode: str,
to_mode: str,
reason: str = "",
parent=None) -> 'TransitionNotification':
"""
Afficher une notification de transition de mode
Args:
task_name: Nom de la tâche
from_mode: Mode d'origine
to_mode: Mode de destination
reason: Raison de la transition
parent: Widget parent
Returns:
Instance de TransitionNotification
"""
data = {
'task_name': task_name,
'from_mode': from_mode,
'to_mode': to_mode,
'reason': reason
}
notification = TransitionNotification(
TransitionNotification.TYPE_MODE_TRANSITION,
data,
requires_action=False,
timeout_ms=6000,
parent=parent
)
notification.show_notification()
return notification
if __name__ == "__main__":
"""Test des notifications de transition"""
from PyQt5.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
def test_autopilot_proposal():
"""Test proposition autopilot"""
notification = TransitionNotification.show_autopilot_proposal(
task_name="Ouvrir Facture",
observation_count=25,
concordance_rate=0.97
)
notification.action_taken.connect(
lambda action: print(f"Autopilot proposal: {action}")
)
def test_confidence_drop():
"""Test baisse de confiance"""
notification = TransitionNotification.show_confidence_drop(
task_name="Saisie Données",
confidence_score=0.85,
threshold=0.90,
reason="Changements UI détectés"
)
notification.action_taken.connect(
lambda action: print(f"Confidence drop: {action}")
)
def test_whitelist_violation():
"""Test violation liste blanche"""
notification = TransitionNotification.show_whitelist_violation(
window_title="Application Non Autorisée",
action_type="click"
)
notification.action_taken.connect(
lambda action: print(f"Whitelist violation: {action}")
)
def test_ui_change():
"""Test changement UI"""
notification = TransitionNotification.show_ui_change(
task_name="Navigation Menu",
similarity=0.65
)
notification.action_taken.connect(
lambda action: print(f"UI change: {action}")
)
def test_mode_transition():
"""Test transition de mode"""
notification = TransitionNotification.show_mode_transition(
task_name="Export Données",
from_mode="auto",
to_mode="assist",
reason="low_confidence"
)
notification.action_taken.connect(
lambda action: print(f"Mode transition: {action}")
)
# Tester séquentiellement
test_autopilot_proposal()
QTimer.singleShot(2000, test_confidence_drop)
QTimer.singleShot(4000, test_whitelist_violation)
QTimer.singleShot(6000, test_ui_change)
QTimer.singleShot(8000, test_mode_transition)
sys.exit(app.exec_())