Initial commit

This commit is contained in:
Dom
2026-03-05 00:20:25 +01:00
commit dcd4de9945
1954 changed files with 669380 additions and 0 deletions

View File

@@ -0,0 +1,612 @@
"""
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_())