""" Tableau de bord résumé pour RPA Vision V2 Affiche les statistiques de tâches, niveaux de confiance et historique d'exécution """ from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QPushButton, QLabel, QLineEdit, QComboBox, QHeaderView, QFileDialog, QMessageBox, QWidget, QGroupBox ) from PyQt5.QtCore import Qt, QTimer, pyqtSignal from PyQt5.QtGui import QFont, QColor, QBrush from typing import Dict, Any, List, Optional import logging import json import csv from datetime import datetime class SummaryDashboard(QDialog): """ Tableau de bord affichant les métriques de tâches en temps réel avec filtrage, recherche et export """ # Signal émis quand une tâche est sélectionnée task_selected = pyqtSignal(str) # task_id def __init__(self, learning_manager=None, parent=None): """ Initialiser le tableau de bord Args: learning_manager: Instance du gestionnaire d'apprentissage parent: Widget parent (optionnel) """ super().__init__(parent) self.learning_manager = learning_manager self.logger = logging.getLogger(__name__) # Données des tâches self.tasks_data: Dict[str, Dict[str, Any]] = {} # Timer pour mise à jour automatique self.update_timer = QTimer() self.update_timer.timeout.connect(self.refresh_data) self.init_ui() # Charger les données initiales if self.learning_manager: self.refresh_data() # Démarrer les mises à jour automatiques (toutes les 2 secondes) self.update_timer.start(2000) self.logger.info("SummaryDashboard initialisé") def init_ui(self): """Initialiser l'interface utilisateur""" self.setWindowTitle("Tableau de Bord - RPA Vision V2") self.setMinimumWidth(1000) self.setMinimumHeight(600) # Layout principal main_layout = QVBoxLayout() self.setLayout(main_layout) # Titre title_label = QLabel("📊 Tableau de Bord des Tâches") title_label.setFont(QFont("Arial", 16, QFont.Bold)) title_label.setStyleSheet("color: #2196F3; padding: 10px;") main_layout.addWidget(title_label) # Barre d'outils (recherche et filtres) toolbar_layout = QHBoxLayout() # Recherche search_label = QLabel("🔍 Recherche:") search_label.setFont(QFont("Arial", 10)) toolbar_layout.addWidget(search_label) self.search_input = QLineEdit() self.search_input.setPlaceholderText("Rechercher par nom de tâche...") self.search_input.setFont(QFont("Arial", 10)) self.search_input.textChanged.connect(self.apply_filters) toolbar_layout.addWidget(self.search_input) # Filtre par mode mode_label = QLabel("Mode:") mode_label.setFont(QFont("Arial", 10)) toolbar_layout.addWidget(mode_label) self.mode_filter = QComboBox() self.mode_filter.setFont(QFont("Arial", 10)) self.mode_filter.addItems(["Tous", "Shadow", "Assisté", "Autopilot"]) self.mode_filter.currentTextChanged.connect(self.apply_filters) toolbar_layout.addWidget(self.mode_filter) # Bouton rafraîchir refresh_button = QPushButton("🔄 Rafraîchir") refresh_button.setFont(QFont("Arial", 10)) refresh_button.clicked.connect(self.refresh_data) toolbar_layout.addWidget(refresh_button) main_layout.addLayout(toolbar_layout) # Statistiques globales stats_group = QGroupBox("Statistiques Globales") stats_group.setFont(QFont("Arial", 11, QFont.Bold)) stats_layout = QHBoxLayout() stats_group.setLayout(stats_layout) self.total_tasks_label = QLabel("Total: 0") self.total_tasks_label.setFont(QFont("Arial", 10)) stats_layout.addWidget(self.total_tasks_label) self.shadow_tasks_label = QLabel("👀 Shadow: 0") self.shadow_tasks_label.setFont(QFont("Arial", 10)) stats_layout.addWidget(self.shadow_tasks_label) self.assist_tasks_label = QLabel("🤝 Assisté: 0") self.assist_tasks_label.setFont(QFont("Arial", 10)) stats_layout.addWidget(self.assist_tasks_label) self.auto_tasks_label = QLabel("🤖 Autopilot: 0") self.auto_tasks_label.setFont(QFont("Arial", 10)) stats_layout.addWidget(self.auto_tasks_label) stats_layout.addStretch() main_layout.addWidget(stats_group) # Tableau des tâches self.tasks_table = QTableWidget() self.tasks_table.setFont(QFont("Arial", 9)) self.tasks_table.setColumnCount(8) self.tasks_table.setHorizontalHeaderLabels([ "Tâche", "Mode", "Confiance", "Observations", "Concordance", "Corrections", "Taux Correction", "Dernière Exécution" ]) # Configuration du tableau header = self.tasks_table.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.Stretch) # Tâche header.setSectionResizeMode(1, QHeaderView.ResizeToContents) # Mode header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # Confiance header.setSectionResizeMode(3, QHeaderView.ResizeToContents) # Observations header.setSectionResizeMode(4, QHeaderView.ResizeToContents) # Concordance header.setSectionResizeMode(5, QHeaderView.ResizeToContents) # Corrections header.setSectionResizeMode(6, QHeaderView.ResizeToContents) # Taux Correction header.setSectionResizeMode(7, QHeaderView.Stretch) # Dernière Exécution self.tasks_table.setAlternatingRowColors(True) self.tasks_table.setSelectionBehavior(QTableWidget.SelectRows) self.tasks_table.setSelectionMode(QTableWidget.SingleSelection) self.tasks_table.itemDoubleClicked.connect(self.on_task_double_clicked) self.tasks_table.setStyleSheet(""" QTableWidget { border: 2px solid #E0E0E0; border-radius: 5px; gridline-color: #E0E0E0; } QTableWidget::item { padding: 5px; } QTableWidget::item:selected { background-color: #E3F2FD; color: #1976D2; } QHeaderView::section { background-color: #2196F3; color: white; padding: 8px; border: none; font-weight: bold; } """) main_layout.addWidget(self.tasks_table) # Boutons d'action button_layout = QHBoxLayout() export_csv_button = QPushButton("📄 Exporter CSV") export_csv_button.setFont(QFont("Arial", 10)) export_csv_button.setStyleSheet(""" QPushButton { background-color: #4CAF50; color: white; border: none; padding: 8px 16px; border-radius: 5px; } QPushButton:hover { background-color: #45a049; } """) export_csv_button.clicked.connect(self.export_to_csv) button_layout.addWidget(export_csv_button) export_json_button = QPushButton("📋 Exporter JSON") export_json_button.setFont(QFont("Arial", 10)) export_json_button.setStyleSheet(""" QPushButton { background-color: #2196F3; color: white; border: none; padding: 8px 16px; border-radius: 5px; } QPushButton:hover { background-color: #1976D2; } """) export_json_button.clicked.connect(self.export_to_json) button_layout.addWidget(export_json_button) button_layout.addStretch() close_button = QPushButton("✗ Fermer") close_button.setFont(QFont("Arial", 10)) close_button.setStyleSheet(""" QPushButton { background-color: #f44336; color: white; border: none; padding: 8px 16px; border-radius: 5px; } QPushButton:hover { background-color: #da190b; } """) close_button.clicked.connect(self.close) button_layout.addWidget(close_button) main_layout.addLayout(button_layout) # Note d'aide help_label = QLabel( "💡 Astuce: Double-cliquez sur une tâche pour voir les détails" ) help_label.setFont(QFont("Arial", 9)) help_label.setStyleSheet("color: #666; padding: 5px;") main_layout.addWidget(help_label) def refresh_data(self): """Rafraîchir les données depuis le gestionnaire d'apprentissage""" if not self.learning_manager: return try: # Obtenir toutes les tâches tasks = self.learning_manager.get_all_tasks() # Mettre à jour les données self.tasks_data = {task["task_id"]: task for task in tasks} # Mettre à jour les statistiques globales self.update_global_stats() # Mettre à jour le tableau self.update_table() self.logger.debug(f"Données rafraîchies: {len(self.tasks_data)} tâches") except Exception as e: self.logger.error(f"Erreur lors du rafraîchissement: {e}") def update_global_stats(self): """Mettre à jour les statistiques globales""" if not self.learning_manager: return try: stats = self.learning_manager.get_task_stats() self.total_tasks_label.setText(f"Total: {stats.get('total_tasks', 0)}") self.shadow_tasks_label.setText(f"👀 Shadow: {stats.get('shadow_tasks', 0)}") self.assist_tasks_label.setText(f"🤝 Assisté: {stats.get('assist_tasks', 0)}") self.auto_tasks_label.setText(f"🤖 Autopilot: {stats.get('auto_tasks', 0)}") except Exception as e: self.logger.error(f"Erreur lors de la mise à jour des stats: {e}") def update_metrics(self, task_id: str, metrics: Dict[str, Any]): """ Mettre à jour les métriques d'une tâche spécifique Args: task_id: ID de la tâche metrics: Dictionnaire de métriques """ if task_id in self.tasks_data: self.tasks_data[task_id].update(metrics) else: self.tasks_data[task_id] = metrics # Mettre à jour le tableau self.update_table() self.logger.debug(f"Métriques mises à jour pour tâche: {task_id}") def update_table(self): """Mettre à jour le tableau avec les données filtrées""" # Appliquer les filtres filtered_tasks = self.get_filtered_tasks() # Mettre à jour le nombre de lignes self.tasks_table.setRowCount(len(filtered_tasks)) # Remplir le tableau for row, task in enumerate(filtered_tasks): self._populate_row(row, task) self.logger.debug(f"Tableau mis à jour: {len(filtered_tasks)} tâches affichées") def _populate_row(self, row: int, task: Dict[str, Any]): """ Remplir une ligne du tableau avec les données d'une tâche Args: row: Numéro de ligne task: Dictionnaire de données de tâche """ # Tâche task_item = QTableWidgetItem(task.get("task_name", "")) task_item.setData(Qt.UserRole, task.get("task_id")) self.tasks_table.setItem(row, 0, task_item) # Mode mode = task.get("mode", "shadow") mode_icons = {"shadow": "👀", "assist": "🤝", "auto": "🤖"} mode_names = {"shadow": "Shadow", "assist": "Assisté", "auto": "Autopilot"} mode_text = f"{mode_icons.get(mode, '')} {mode_names.get(mode, mode)}" mode_item = QTableWidgetItem(mode_text) mode_item.setTextAlignment(Qt.AlignCenter) # Couleur selon le mode mode_colors = { "shadow": QColor(33, 150, 243), # Bleu "assist": QColor(255, 152, 0), # Orange "auto": QColor(76, 175, 80) # Vert } if mode in mode_colors: mode_item.setForeground(QBrush(mode_colors[mode])) self.tasks_table.setItem(row, 1, mode_item) # Confiance confidence = task.get("confidence_score", 0.0) confidence_item = QTableWidgetItem(f"{confidence * 100:.1f}%") confidence_item.setTextAlignment(Qt.AlignCenter) # Couleur selon la confiance if confidence >= 0.95: confidence_item.setForeground(QBrush(QColor(76, 175, 80))) # Vert elif confidence >= 0.85: confidence_item.setForeground(QBrush(QColor(255, 152, 0))) # Orange else: confidence_item.setForeground(QBrush(QColor(244, 67, 54))) # Rouge self.tasks_table.setItem(row, 2, confidence_item) # Observations obs_count = task.get("observation_count", 0) obs_item = QTableWidgetItem(str(obs_count)) obs_item.setTextAlignment(Qt.AlignCenter) self.tasks_table.setItem(row, 3, obs_item) # Concordance concordance = task.get("concordance_rate", 0.0) concordance_item = QTableWidgetItem(f"{concordance * 100:.1f}%") concordance_item.setTextAlignment(Qt.AlignCenter) # Couleur selon la concordance if concordance >= 0.95: concordance_item.setForeground(QBrush(QColor(76, 175, 80))) # Vert elif concordance >= 0.85: concordance_item.setForeground(QBrush(QColor(255, 152, 0))) # Orange else: concordance_item.setForeground(QBrush(QColor(244, 67, 54))) # Rouge self.tasks_table.setItem(row, 4, concordance_item) # Corrections corrections = task.get("correction_count", 0) corrections_item = QTableWidgetItem(str(corrections)) corrections_item.setTextAlignment(Qt.AlignCenter) self.tasks_table.setItem(row, 5, corrections_item) # Taux de correction correction_rate = task.get("correction_rate", 0.0) correction_rate_item = QTableWidgetItem(f"{correction_rate * 100:.1f}%") correction_rate_item.setTextAlignment(Qt.AlignCenter) # Couleur selon le taux de correction (inverse: moins c'est mieux) if correction_rate <= 0.03: correction_rate_item.setForeground(QBrush(QColor(76, 175, 80))) # Vert elif correction_rate <= 0.05: correction_rate_item.setForeground(QBrush(QColor(255, 152, 0))) # Orange else: correction_rate_item.setForeground(QBrush(QColor(244, 67, 54))) # Rouge self.tasks_table.setItem(row, 6, correction_rate_item) # Dernière exécution last_exec = task.get("last_execution", "") if last_exec: try: dt = datetime.fromisoformat(last_exec) last_exec_text = dt.strftime("%Y-%m-%d %H:%M:%S") except: last_exec_text = last_exec else: last_exec_text = "Jamais" last_exec_item = QTableWidgetItem(last_exec_text) self.tasks_table.setItem(row, 7, last_exec_item) def get_filtered_tasks(self) -> List[Dict[str, Any]]: """ Obtenir la liste des tâches filtrées selon les critères Returns: Liste de tâches filtrées """ filtered = list(self.tasks_data.values()) # Filtre de recherche search_text = self.search_input.text().lower() if search_text: filtered = [ task for task in filtered if search_text in task.get("task_name", "").lower() or search_text in task.get("task_id", "").lower() ] # Filtre de mode mode_filter = self.mode_filter.currentText() if mode_filter != "Tous": mode_map = { "Shadow": "shadow", "Assisté": "assist", "Autopilot": "auto" } target_mode = mode_map.get(mode_filter) if target_mode: filtered = [ task for task in filtered if task.get("mode") == target_mode ] # Trier par dernière exécution (plus récent en premier) filtered.sort( key=lambda t: t.get("last_execution", ""), reverse=True ) return filtered def apply_filters(self): """Appliquer les filtres et mettre à jour le tableau""" self.update_table() def on_task_double_clicked(self, item): """ Gestionnaire de double-clic sur une tâche Args: item: Élément de tableau cliqué """ row = item.row() task_id_item = self.tasks_table.item(row, 0) if task_id_item: task_id = task_id_item.data(Qt.UserRole) if task_id and task_id in self.tasks_data: self.show_task_details(task_id) self.task_selected.emit(task_id) def show_task_details(self, task_id: str): """ Afficher les détails d'une tâche dans une boîte de dialogue Args: task_id: ID de la tâche """ if task_id not in self.tasks_data: return task = self.tasks_data[task_id] details = f"""
ID: {task.get('task_id', '')}
Nom: {task.get('task_name', '')}
Mode: {task.get('mode', '')}
Observations: {task.get('observation_count', 0)}
Confiance: {task.get('confidence_score', 0) * 100:.1f}%
Concordance: {task.get('concordance_rate', 0) * 100:.1f}%
Corrections: {task.get('correction_count', 0)}
Taux de correction: {task.get('correction_rate', 0) * 100:.1f}%
Dernière exécution: {task.get('last_execution', 'Jamais')}
""" msg_box = QMessageBox(self) msg_box.setWindowTitle("Détails de la Tâche") msg_box.setTextFormat(Qt.RichText) msg_box.setText(details) msg_box.setIcon(QMessageBox.Information) msg_box.exec_() def export_to_csv(self): """Exporter les données vers un fichier CSV""" if not self.tasks_data: QMessageBox.warning( self, "Aucune Donnée", "Aucune donnée à exporter." ) return # Dialogue de sauvegarde file_path, _ = QFileDialog.getSaveFileName( self, "Exporter vers CSV", f"rpa_tasks_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv", "CSV Files (*.csv)" ) if not file_path: return try: with open(file_path, 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) # En-têtes writer.writerow([ "ID Tâche", "Nom Tâche", "Mode", "Confiance (%)", "Observations", "Concordance (%)", "Corrections", "Taux Correction (%)", "Dernière Exécution" ]) # Données for task in self.get_filtered_tasks(): writer.writerow([ task.get("task_id", ""), task.get("task_name", ""), task.get("mode", ""), f"{task.get('confidence_score', 0) * 100:.1f}", task.get("observation_count", 0), f"{task.get('concordance_rate', 0) * 100:.1f}", task.get("correction_count", 0), f"{task.get('correction_rate', 0) * 100:.1f}", task.get("last_execution", "") ]) QMessageBox.information( self, "Export Réussi", f"Données exportées vers:\n{file_path}" ) self.logger.info(f"Données exportées vers CSV: {file_path}") except Exception as e: QMessageBox.critical( self, "Erreur d'Export", f"Erreur lors de l'export CSV:\n{str(e)}" ) self.logger.error(f"Erreur d'export CSV: {e}") def export_to_json(self): """Exporter les données vers un fichier JSON""" if not self.tasks_data: QMessageBox.warning( self, "Aucune Donnée", "Aucune donnée à exporter." ) return # Dialogue de sauvegarde file_path, _ = QFileDialog.getSaveFileName( self, "Exporter vers JSON", f"rpa_tasks_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", "JSON Files (*.json)" ) if not file_path: return try: export_data = { "export_date": datetime.now().isoformat(), "total_tasks": len(self.tasks_data), "tasks": self.get_filtered_tasks() } with open(file_path, 'w', encoding='utf-8') as f: json.dump(export_data, f, indent=2, ensure_ascii=False) QMessageBox.information( self, "Export Réussi", f"Données exportées vers:\n{file_path}" ) self.logger.info(f"Données exportées vers JSON: {file_path}") except Exception as e: QMessageBox.critical( self, "Erreur d'Export", f"Erreur lors de l'export JSON:\n{str(e)}" ) self.logger.error(f"Erreur d'export JSON: {e}") def closeEvent(self, event): """Gestionnaire de fermeture""" # Arrêter le timer de mise à jour self.update_timer.stop() event.accept() @staticmethod def show_dashboard(learning_manager=None, parent=None) -> 'SummaryDashboard': """ Méthode statique pour afficher le tableau de bord Args: learning_manager: Instance du gestionnaire d'apprentissage parent: Widget parent (optionnel) Returns: Instance du tableau de bord """ dashboard = SummaryDashboard(learning_manager, parent) dashboard.show() return dashboard if __name__ == "__main__": """Test du tableau de bord""" from PyQt5.QtWidgets import QApplication import sys app = QApplication(sys.argv) # Créer un tableau de bord avec des données de test dashboard = SummaryDashboard() # Ajouter des données de test test_tasks = [ { "task_id": "task_001", "task_name": "Ouvrir Facture", "mode": "auto", "confidence_score": 0.97, "observation_count": 45, "concordance_rate": 0.98, "correction_count": 1, "correction_rate": 0.022, "last_execution": datetime.now().isoformat() }, { "task_id": "task_002", "task_name": "Valider Commande", "mode": "assist", "confidence_score": 0.89, "observation_count": 12, "concordance_rate": 0.92, "correction_count": 2, "correction_rate": 0.167, "last_execution": datetime.now().isoformat() }, { "task_id": "task_003", "task_name": "Saisie Données Client", "mode": "shadow", "confidence_score": 0.65, "observation_count": 3, "concordance_rate": 0.67, "correction_count": 0, "correction_rate": 0.0, "last_execution": datetime.now().isoformat() } ] for task in test_tasks: dashboard.update_metrics(task["task_id"], task) dashboard.show() sys.exit(app.exec_())