import sys import json import os # Ensure QApplication exists before any widget creation from PyQt5.QtWidgets import QApplication if not QApplication.instance(): app = QApplication(sys.argv) else: app = QApplication.instance() from PyQt5.QtWidgets import ( QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QLineEdit, QPushButton, QDialog, QTextEdit, QFormLayout, QDialogButtonBox, QMessageBox, QFileDialog, QLabel, QSpinBox, QDoubleSpinBox, QComboBox, QListWidget, QCheckBox, QSplitter, QAction, QScrollArea, QTabWidget, QTableWidget, QTableWidgetItem, QHeaderView, QProgressBar, QSlider, QGroupBox, QGridLayout, QFrame, QTreeWidget, QTreeWidgetItem, QTextBrowser, QSizePolicy ) from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont from NodeGraphQt import NodeGraph, BaseNode, PropertiesBinWidget # Harmonious theme with complementary color palette HARMONIOUS_THEME_STYLESHEET = """ QWidget { background-color: #1e1e2e; color: #cdd6f4; font-family: "Inter", "SF Pro Display", "Segoe UI", sans-serif; font-size: 13px; } QMainWindow { background-color: #181825; } QDialog { background-color: #1e1e2e; border: 1px solid #313244; } QLabel { color: #f9e2af; font-weight: 500; } QLineEdit, QTextEdit, QSpinBox, QDoubleSpinBox, QComboBox { background-color: #313244; border: 2px solid #45475a; padding: 8px 12px; border-radius: 8px; color: #cdd6f4; selection-background-color: #74c7ec; font-size: 13px; } QLineEdit:focus, QTextEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QComboBox:focus { border-color: #89b4fa; background-color: #383a59; outline: none; box-shadow: 0 0 0 3px rgba(137, 180, 250, 0.1); } QLineEdit:hover, QTextEdit:hover, QSpinBox:hover, QDoubleSpinBox:hover, QComboBox:hover { border-color: #585b70; } QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #89b4fa, stop:1 #74c7ec); color: #1e1e2e; border: none; padding: 10px 16px; border-radius: 8px; font-weight: 600; font-size: 13px; min-height: 16px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #a6c8ff, stop:1 #89dceb); transform: translateY(-1px); } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #7287fd, stop:1 #5fb3d3); } QPushButton:disabled { background-color: #45475a; color: #6c7086; } QDialogButtonBox QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #89b4fa, stop:1 #74c7ec); color: #1e1e2e; min-width: 90px; margin: 2px; } QDialogButtonBox QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #a6c8ff, stop:1 #89dceb); } QDialogButtonBox QPushButton[text="Cancel"] { background-color: #585b70; color: #cdd6f4; border: 1px solid #6c7086; } QDialogButtonBox QPushButton[text="Cancel"]:hover { background-color: #6c7086; } QListWidget { background-color: #313244; border: 2px solid #45475a; border-radius: 8px; outline: none; } QListWidget::item { padding: 12px; border-bottom: 1px solid #45475a; color: #cdd6f4; border-radius: 4px; margin: 2px; } QListWidget::item:selected { background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #89b4fa, stop:1 #74c7ec); color: #1e1e2e; border-radius: 6px; } QListWidget::item:hover { background-color: #383a59; border-radius: 6px; } QSplitter::handle { background-color: #45475a; width: 3px; height: 3px; } QSplitter::handle:hover { background-color: #89b4fa; } QCheckBox { color: #cdd6f4; spacing: 8px; } QCheckBox::indicator { width: 18px; height: 18px; border: 2px solid #45475a; border-radius: 4px; background-color: #313244; } QCheckBox::indicator:checked { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #89b4fa, stop:1 #74c7ec); border-color: #89b4fa; } QCheckBox::indicator:hover { border-color: #89b4fa; } QScrollArea { border: none; background-color: #1e1e2e; } QScrollBar:vertical { background-color: #313244; width: 14px; border-radius: 7px; margin: 0px; } QScrollBar::handle:vertical { background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #89b4fa, stop:1 #74c7ec); border-radius: 7px; min-height: 20px; margin: 2px; } QScrollBar::handle:vertical:hover { background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #a6c8ff, stop:1 #89dceb); } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { border: none; background: none; height: 0px; } QMenuBar { background-color: #181825; color: #cdd6f4; border-bottom: 1px solid #313244; padding: 4px; } QMenuBar::item { padding: 8px 12px; background-color: transparent; border-radius: 6px; } QMenuBar::item:selected { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #89b4fa, stop:1 #74c7ec); color: #1e1e2e; } QMenu { background-color: #313244; color: #cdd6f4; border: 1px solid #45475a; border-radius: 8px; padding: 4px; } QMenu::item { padding: 8px 16px; border-radius: 4px; } QMenu::item:selected { background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #89b4fa, stop:1 #74c7ec); color: #1e1e2e; } QComboBox::drop-down { border: none; width: 30px; border-radius: 4px; } QComboBox::down-arrow { image: none; border: 5px solid transparent; border-top: 6px solid #cdd6f4; margin-right: 8px; } QFormLayout QLabel { font-weight: 600; margin-bottom: 4px; color: #f9e2af; } QTextEdit { line-height: 1.4; } /* Custom accent colors for different UI states */ .success { color: #a6e3a1; } .warning { color: #f9e2af; } .error { color: #f38ba8; } .info { color: #89b4fa; } """ # 1. 修改节点类,只使用简单的create_property # 更新节点类,添加业务相关属性 class ModelNode(BaseNode): """Model node for ML inference""" __identifier__ = 'com.cluster.model_node' NODE_NAME = 'Model Node' def __init__(self): super(ModelNode, self).__init__() self.add_input('input', multi_input=False, color=(255, 140, 0)) self.add_output('output', color=(0, 255, 0)) self.set_color(65, 84, 102) # 业务属性 self.create_property('model_path', '') self.create_property('dongle_series', '520') self.create_property('num_dongles', 1) self.create_property('port_id', '') # 属性选项和验证规则 self._property_options = { 'dongle_series': ['520', '720', '1080', 'Custom'], 'num_dongles': {'min': 1, 'max': 16}, 'model_path': {'type': 'file_path', 'filter': 'Model files (*.onnx *.tflite *.pb)'}, 'port_id': {'placeholder': 'e.g., 8080 or auto'} } class PreprocessNode(BaseNode): """Preprocessing node""" __identifier__ = 'com.cluster.preprocess_node' NODE_NAME = 'Preprocess Node' def __init__(self): super(PreprocessNode, self).__init__() self.add_input('input', multi_input=False, color=(255, 140, 0)) self.add_output('output', color=(0, 255, 0)) self.set_color(45, 126, 72) # 预处理业务属性 self.create_property('resize_width', 640) self.create_property('resize_height', 480) self.create_property('normalize', True) self.create_property('crop_enabled', False) self.create_property('operations', 'resize,normalize') self._property_options = { 'resize_width': {'min': 64, 'max': 4096}, 'resize_height': {'min': 64, 'max': 4096}, 'operations': {'placeholder': 'comma-separated: resize,normalize,crop'} } class PostprocessNode(BaseNode): """Postprocessing node""" __identifier__ = 'com.cluster.postprocess_node' NODE_NAME = 'Postprocess Node' def __init__(self): super(PostprocessNode, self).__init__() self.add_input('input', multi_input=False, color=(255, 140, 0)) self.add_output('output', color=(0, 255, 0)) self.set_color(153, 51, 51) # 后处理业务属性 self.create_property('output_format', 'JSON') self.create_property('confidence_threshold', 0.5) self.create_property('nms_threshold', 0.4) self.create_property('max_detections', 100) self._property_options = { 'output_format': ['JSON', 'XML', 'CSV', 'Binary'], 'confidence_threshold': {'min': 0.0, 'max': 1.0, 'step': 0.1}, 'nms_threshold': {'min': 0.0, 'max': 1.0, 'step': 0.1}, 'max_detections': {'min': 1, 'max': 1000} } class InputNode(BaseNode): """Input data source node""" __identifier__ = 'com.cluster.input_node' NODE_NAME = 'Input Node' def __init__(self): super(InputNode, self).__init__() self.add_output('output', color=(0, 255, 0)) self.set_color(83, 133, 204) # 输入源业务属性 self.create_property('source_type', 'Camera') self.create_property('device_id', 0) self.create_property('source_path', '') self.create_property('resolution', '1920x1080') self.create_property('fps', 30) self._property_options = { 'source_type': ['Camera', 'Microphone', 'File', 'RTSP Stream', 'HTTP Stream'], 'device_id': {'min': 0, 'max': 10}, 'resolution': ['640x480', '1280x720', '1920x1080', '3840x2160', 'Custom'], 'fps': {'min': 1, 'max': 120}, 'source_path': {'type': 'file_path', 'filter': 'Media files (*.mp4 *.avi *.mov *.mkv *.wav *.mp3)'} } class OutputNode(BaseNode): """Output data sink node""" __identifier__ = 'com.cluster.output_node' NODE_NAME = 'Output Node' def __init__(self): super(OutputNode, self).__init__() self.add_input('input', multi_input=False, color=(255, 140, 0)) self.set_color(255, 140, 0) # 输出业务属性 self.create_property('output_type', 'File') self.create_property('destination', '') self.create_property('format', 'JSON') self.create_property('save_interval', 1.0) self._property_options = { 'output_type': ['File', 'API Endpoint', 'Database', 'Display', 'MQTT'], 'format': ['JSON', 'XML', 'CSV', 'Binary'], 'destination': {'type': 'file_path', 'filter': 'Output files (*.json *.xml *.csv *.txt)'}, 'save_interval': {'min': 0.1, 'max': 60.0, 'step': 0.1} } # 修复后的CustomPropertiesWidget class CustomPropertiesWidget(QWidget): def __init__(self, graph): super().__init__() self.graph = graph self.current_node = None self.property_widgets = {} self.original_values = {} # Store original values for reset functionality self.setup_ui() print("Connecting node selection changed signal...") self.graph.node_selection_changed.connect(self.on_selection_changed) print("Signal connected successfully") def setup_ui(self): layout = QVBoxLayout(self) # 标题 self.title = QLabel("Business Properties") self.title.setFont(QFont("Arial", 12, QFont.Bold)) self.title.setStyleSheet("color: #343a40; padding: 10px; background-color: #e9ecef; border-radius: 4px;") layout.addWidget(self.title) # 调试信息标签 self.debug_label = QLabel("No node selected") self.debug_label.setStyleSheet("color: #6c757d; font-size: 10px; padding: 5px;") layout.addWidget(self.debug_label) # 滚动区域 self.scroll_area = QScrollArea() self.properties_container = QWidget() self.properties_layout = QFormLayout(self.properties_container) self.properties_layout.setSpacing(8) self.scroll_area.setWidget(self.properties_container) self.scroll_area.setWidgetResizable(True) layout.addWidget(self.scroll_area) # 底部按钮 self.button_layout = QHBoxLayout() self.apply_btn = QPushButton("Apply Changes") self.apply_btn.clicked.connect(self.apply_changes) self.reset_btn = QPushButton("Reset") self.reset_btn.clicked.connect(self.reset_properties) self.button_layout.addWidget(self.apply_btn) self.button_layout.addWidget(self.reset_btn) layout.addLayout(self.button_layout) def on_selection_changed(self): print("Selection changed event triggered") selected = self.graph.selected_nodes() print(f"Selected nodes: {[node.name() for node in selected]}") if selected: self.load_node_properties(selected[0]) else: self.clear_properties() def clear_properties(self): print("Clearing properties...") for widget in self.property_widgets.values(): widget.deleteLater() self.property_widgets.clear() for i in reversed(range(self.properties_layout.count())): item = self.properties_layout.itemAt(i) if item and item.widget(): item.widget().deleteLater() self.title.setText("Business Properties") self.debug_label.setText("No node selected") def load_node_properties(self, node): print(f"Loading properties for node: {node.name()}") self.clear_properties() self.current_node = node self.title.setText(f"Business Properties - {node.name()}") # 从custom字典中获取业务属性 try: all_properties = node.properties() print(f"All properties: {all_properties}") # 检查是否有custom字典 if 'custom' in all_properties: custom_properties = all_properties['custom'] print(f"Custom properties found: {custom_properties}") # Store original values for reset functionality self.original_values = custom_properties.copy() self.debug_label.setText(f"Found {len(custom_properties)} business properties") if not custom_properties: no_props_label = QLabel("No business properties found for this node type.") no_props_label.setStyleSheet("color: #dc3545; font-style: italic; padding: 10px;") self.properties_layout.addRow(no_props_label) return for prop_name, value in custom_properties.items(): try: print(f"Property {prop_name} = {value}") widget = self.create_property_widget(prop_name, value, node) if widget: self.property_widgets[prop_name] = widget label = QLabel(prop_name.replace('_', ' ').title() + ":") label.setStyleSheet("font-weight: bold; color: #495057;") self.properties_layout.addRow(label, widget) print(f"Added widget for {prop_name}") except Exception as e: print(f"Error loading property {prop_name}: {e}") import traceback traceback.print_exc() # 添加错误显示但不停止加载其他属性 error_label = QLabel(f"Error loading {prop_name}") error_label.setStyleSheet("color: #dc3545; font-style: italic;") self.properties_layout.addRow(f"{prop_name}:", error_label) else: print("No custom properties found") self.debug_label.setText("No custom properties found") self.original_values = {} no_props_label = QLabel("This node type has no configurable properties.") no_props_label.setStyleSheet("color: #6c757d; font-style: italic; padding: 10px;") self.properties_layout.addRow(no_props_label) except Exception as e: print(f"Error accessing node properties: {e}") import traceback traceback.print_exc() self.debug_label.setText("Error loading properties") def create_property_widget(self, prop_name, value, node): """根据属性类型和选项创建对应的输入控件""" print(f"Creating widget for {prop_name}, value: {value}") options = None if hasattr(node, '_property_options') and prop_name in node._property_options: options = node._property_options[prop_name] print(f"Found options for {prop_name}: {options}") # 文件路径选择器 if isinstance(options, dict) and options.get('type') == 'file_path': container = QWidget() layout = QHBoxLayout(container) layout.setContentsMargins(0, 0, 0, 0) line_edit = QLineEdit() line_edit.setText(str(value)) line_edit.setPlaceholderText(options.get('placeholder', 'Select file...')) browse_btn = QPushButton("Browse") browse_btn.setMaximumWidth(70) browse_btn.clicked.connect(lambda: self.browse_file(line_edit, options.get('filter', 'All files (*.*)'))) layout.addWidget(line_edit) layout.addWidget(browse_btn) # 绑定变化事件 - 使用直接属性更新 line_edit.textChanged.connect(lambda text: self.update_node_property(node, prop_name, text)) return container # 下拉选择 elif isinstance(options, list): widget = QComboBox() widget.addItems(options) if str(value) in options: widget.setCurrentText(str(value)) widget.currentTextChanged.connect(lambda text: self.update_node_property(node, prop_name, text)) return widget # 数值范围控件 - 修复类型转换 elif isinstance(options, dict) and 'min' in options and 'max' in options: if isinstance(value, float) or options.get('step'): widget = QDoubleSpinBox() widget.setDecimals(1) widget.setSingleStep(options.get('step', 0.1)) widget.setValue(float(value) if isinstance(value, (int, float)) else float(options['min'])) else: widget = QSpinBox() # 修复: 确保传入int类型 widget.setValue(int(value) if isinstance(value, (int, float)) else int(options['min'])) widget.setRange(options['min'], options['max']) widget.valueChanged.connect(lambda val: self.update_node_property(node, prop_name, val)) return widget # 布尔值 elif isinstance(value, bool): widget = QCheckBox() widget.setChecked(value) widget.toggled.connect(lambda checked: self.update_node_property(node, prop_name, checked)) return widget # 普通文本输入 else: widget = QLineEdit() widget.setText(str(value)) if isinstance(options, dict) and 'placeholder' in options: widget.setPlaceholderText(options['placeholder']) widget.textChanged.connect(lambda text: self.update_node_property(node, prop_name, text)) return widget def update_node_property(self, node, prop_name, value): """更新节点属性 - 使用直接属性更新方式""" try: print(f"Updating {prop_name} = {value}") # 尝试直接设置属性(如果NodeGraphQt支持) try: node.set_property(prop_name, value) print(f"Successfully updated {prop_name} using set_property") return except Exception as e: print(f"set_property failed: {e}") # 如果直接设置失败,尝试通过节点内部更新 # 直接修改节点的_property_changed属性(如果存在) if hasattr(node, '_model') and hasattr(node._model, '_custom_properties'): if not hasattr(node._model, '_custom_properties'): node._model._custom_properties = {} node._model._custom_properties[prop_name] = value print(f"Updated via _custom_properties: {prop_name} = {value}") return # 最后的备用方案:直接修改properties字典(可能不持久化) properties = node.properties() if 'custom' in properties: properties['custom'][prop_name] = value print(f"Updated via properties dict: {prop_name} = {value}") except Exception as e: print(f"Error updating property {prop_name}: {e}") import traceback traceback.print_exc() # 显示错误但不崩溃 QMessageBox.warning(self, "Property Update Error", f"Failed to update {prop_name}: {str(e)}\n\n" f"Value will be reset when node is reselected.") def browse_file(self, line_edit, file_filter): """打开文件浏览对话框""" filename, _ = QFileDialog.getOpenFileName(self, "Select File", "", file_filter) if filename: line_edit.setText(filename) def apply_changes(self): """应用所有更改""" if self.current_node: # 尝试保存当前所有属性值 try: saved_count = 0 for prop_name, widget in self.property_widgets.items(): # 根据widget类型获取当前值 if isinstance(widget, QLineEdit): value = widget.text() elif isinstance(widget, QComboBox): value = widget.currentText() elif isinstance(widget, (QSpinBox, QDoubleSpinBox)): value = widget.value() elif isinstance(widget, QCheckBox): value = widget.isChecked() elif isinstance(widget, QWidget): # 文件路径容器 line_edit = widget.findChild(QLineEdit) value = line_edit.text() if line_edit else "" else: continue self.update_node_property(self.current_node, prop_name, value) saved_count += 1 QMessageBox.information(self, "Applied", f"Applied {saved_count} properties to {self.current_node.name()}") except Exception as e: QMessageBox.warning(self, "Apply Error", f"Error applying changes: {str(e)}") def reset_properties(self): """Reset properties to original values when node was first loaded""" if self.current_node: reply = QMessageBox.question(self, "Reset Properties", "Reset all properties to their original values?", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: try: # Use original values if available, otherwise fall back to defaults values_to_restore = self.original_values if self.original_values else self.get_default_values_for_node(self.current_node) if not values_to_restore: QMessageBox.information(self, "No Reset Available", "No original or default values available to reset.") return # Reset each property to its original/default value reset_count = 0 for prop_name, original_value in values_to_restore.items(): try: self.update_node_property(self.current_node, prop_name, original_value) reset_count += 1 print(f"Reset {prop_name} to {original_value}") except Exception as e: print(f"Error resetting property {prop_name}: {e}") # Update the UI widgets to show the reset values self.update_ui_widgets_with_values(values_to_restore) source_type = "original" if self.original_values else "default" QMessageBox.information(self, "Reset Complete", f"Reset {reset_count} properties to {source_type} values.") except Exception as e: QMessageBox.warning(self, "Reset Error", f"Error resetting properties: {str(e)}") def update_ui_widgets_with_values(self, values_dict): """Update the UI widgets to display the specified values""" for prop_name, value in values_dict.items(): if prop_name in self.property_widgets: widget = self.property_widgets[prop_name] try: # Update widget based on type if isinstance(widget, QLineEdit): widget.setText(str(value)) elif isinstance(widget, QComboBox): widget.setCurrentText(str(value)) elif isinstance(widget, (QSpinBox, QDoubleSpinBox)): widget.setValue(value) elif isinstance(widget, QCheckBox): widget.setChecked(bool(value)) elif isinstance(widget, QWidget): # File path container line_edit = widget.findChild(QLineEdit) if line_edit: line_edit.setText(str(value)) except Exception as e: print(f"Error updating widget for {prop_name}: {e}") def get_default_values_for_node(self, node): """Get the default property values for a specific node type""" # Define default values for each node type defaults = { 'ModelNode': { 'model_path': '', 'dongle_series': '520', 'num_dongles': 1, 'port_id': '' }, 'PreprocessNode': { 'resize_width': 640, 'resize_height': 480, 'normalize': True, 'crop_enabled': False, 'operations': 'resize,normalize' }, 'PostprocessNode': { 'output_format': 'JSON', 'confidence_threshold': 0.5, 'nms_threshold': 0.4, 'max_detections': 100 }, 'InputNode': { 'source_type': 'Camera', 'device_id': 0, 'source_path': '', 'resolution': '1920x1080', 'fps': 30 }, 'OutputNode': { 'output_type': 'File', 'destination': '', 'format': 'JSON', 'save_interval': 1.0 } } # Get the node class name node_class_name = node.__class__.__name__ # Return the defaults for this node type, or empty dict if not found return defaults.get(node_class_name, {}) class CreatePipelineDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Create New Pipeline") self.setMinimumWidth(450) self.setStyleSheet(HARMONIOUS_THEME_STYLESHEET) self.projectNameInput = QLineEdit(self) self.descriptionInput = QTextEdit(self) self.descriptionInput.setPlaceholderText("Optional (briefly describe the pipeline's purpose)") self.descriptionInput.setFixedHeight(80) formLayout = QFormLayout() formLayout.setSpacing(10) formLayout.addRow("Project Name:", self.projectNameInput) formLayout.addRow("Description:", self.descriptionInput) self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.buttonBox.accepted.connect(self.handle_ok) self.buttonBox.rejected.connect(self.reject) mainLayout = QVBoxLayout(self) mainLayout.setContentsMargins(15, 15, 15, 15) mainLayout.addLayout(formLayout) mainLayout.addSpacing(10) mainLayout.addWidget(self.buttonBox) def handle_ok(self): if not self.projectNameInput.text().strip(): QMessageBox.warning(self, "Input Error", "Project Name cannot be empty.") return self.accept() def get_data(self): return { "project_name": self.projectNameInput.text().strip(), "description": self.descriptionInput.toPlainText().strip() } class SimplePropertiesDialog(QDialog): # Corrected and placed before PipelineEditor """Simple properties dialog as fallback""" def __init__(self, node, parent=None): super().__init__(parent) self.node = node self.setWindowTitle(f"Properties - {node.name()}") self.setMinimumWidth(400) self.setStyleSheet(HARMONIOUS_THEME_STYLESHEET) layout = QVBoxLayout(self) # Node info info_label = QLabel(f"Node: {node.name()}\nType: {node.type_}") info_label.setFont(QFont("Arial", 12, QFont.Bold)) layout.addWidget(info_label) # Properties form_layout = QFormLayout() self.property_widgets = {} try: if hasattr(node, 'properties'): for prop_name in node.properties(): try: current_value = node.get_property(prop_name) # Create simple text input for all properties widget = QLineEdit() widget.setText(str(current_value)) self.property_widgets[prop_name] = widget form_layout.addRow(f"{prop_name}:", widget) except Exception as e: print(f"Error loading property {prop_name} for node {node.name()}: {e}") # Optionally add a label to the dialog indicating error for this property error_prop_label = QLabel(f"(Error loading {prop_name})") form_layout.addRow(f"{prop_name}:", error_prop_label) except Exception as e: print(f"Error loading properties for node {node.name()}: {e}") error_label = QLabel(f"Error loading properties: {e}") layout.addWidget(error_label) layout.addLayout(form_layout) # Buttons button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(self.save_properties) button_box.rejected.connect(self.reject) layout.addWidget(button_box) def save_properties(self): """Save property changes""" try: for prop_name, widget in self.property_widgets.items(): if not isinstance(widget, QLineEdit): # Skip if widget is not an input (e.g. error label) continue text_value = widget.text() # Try to convert to appropriate type try: # Get original value to determine type original_value = self.node.get_property(prop_name) # Assuming this doesn't error again if isinstance(original_value, bool): value = text_value.lower() in ('true', '1', 'yes', 'on') elif isinstance(original_value, int): value = int(text_value) elif isinstance(original_value, float): value = float(text_value) else: value = text_value except (ValueError, TypeError): # Fallback if conversion fails value = text_value except Exception as e_get_prop: # Handle case where get_property failed during save_properties' type check print(f"Could not get original property type for {prop_name} during save. Saving as string. Error: {e_get_prop}") value = text_value # Save as string if original type unknown # Set the property self.node.set_property(prop_name, value) self.accept() except Exception as e: QMessageBox.critical(self, "Error", f"Failed to save properties: {str(e)}") class NodePalette(QWidget): """Node palette for adding nodes""" def __init__(self, graph): super().__init__() self.graph = graph self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) title = QLabel("Node Palette") title.setFont(QFont("Arial", 12, QFont.Bold)) layout.addWidget(title) # Node creation buttons nodes = [ ("Input", InputNode), ("Model", ModelNode), ("Preprocess", PreprocessNode), ("Postprocess", PostprocessNode), ("Output", OutputNode) ] for name, node_class in nodes: btn = QPushButton(name) btn.clicked.connect(lambda checked, cls=node_class: self.create_node(cls)) layout.addWidget(btn) # Instructions instructions = QLabel() instructions.setText( "📍 Click buttons to add nodes\n" "⌨️ Or press TAB in graph area to search nodes\n" "🔗 Drag from 🟢GREEN output to 🟠ORANGE input\n" "⚙️ Double-click nodes to see properties\n" "🗑️ Select + Delete key to remove\n" "💾 Ctrl+S to save\n" "🎯 Right-click for context menu\n" "📋 View menu → Properties Panel\n" "🔍 Debug menu → Show Registered Nodes" ) instructions.setWordWrap(True) instructions.setStyleSheet("color: #6c757d; font-size: 10px; margin: 10px 0; padding: 10px; background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px;") layout.addWidget(instructions) layout.addStretch() def create_node(self, node_class): """Create a new node and add to graph""" try: # Use class name instead of NODE_NAME for identifier node_id = f"{node_class.__identifier__}.{node_class.__name__}" # print(f"Creating node: {node_id}") # Optional: for debugging node = self.graph.create_node(node_id) if node: # Check if node was created node.set_pos(100, 100) # Position new nodes at visible location # print(f"Successfully created node: {node_id}") # Optional: for debugging else: # This case might happen if registration was incomplete or ID is wrong. raise Exception(f"Graph returned None for create_node with ID: {node_id}") except Exception as e: print(f"Failed to create node {node_class.__name__}: {e}") QMessageBox.warning(self, "Node Creation Error", f"Failed to create {node_class.__name__}: {str(e)}\n\n" f"Try using Tab key to search for nodes, or check console for registration errors.") class IntegratedPipelineDashboard(QMainWindow): """Integrated dashboard combining pipeline editor, stage configuration, and performance estimation""" def __init__(self, project_name="", description="", filename=None): super().__init__() self.project_name = project_name self.description = description self.current_file = filename self.is_modified = False # Initialize attributes that will be used by methods self.allocation_layout = None self.stage_configs_layout = None self.stages_spinbox = None self.dongles_list = None self.fps_label = None self.latency_label = None self.memory_label = None self.suggestions_text = None self.props_instructions = None self.node_props_container = None self.node_props_layout = None # Initialize node graph self.graph = NodeGraph(properties_bin_class=None) # Register custom nodes nodes_to_register = [InputNode, ModelNode, PreprocessNode, PostprocessNode, OutputNode] for node_class in nodes_to_register: try: self.graph.register_node(node_class) # Uncomment for debugging: print(f"✅ Registered {node_class.__name__}") except Exception as e: print(f"❌ Failed to register {node_class.__name__}: {e}") # Connect signals self.graph.node_created.connect(self.mark_modified) self.graph.nodes_deleted.connect(self.mark_modified) self.graph.property_changed.connect(self.mark_modified) if hasattr(self.graph, 'port_connected'): self.graph.port_connected.connect(self.on_port_connected) if hasattr(self.graph, 'port_disconnected'): self.graph.port_disconnected.connect(self.on_port_disconnected) self.setup_integrated_ui() self.setup_menu() # Add keyboard shortcut for delete self.delete_shortcut = QAction("Delete", self) self.delete_shortcut.setShortcut('Delete') self.delete_shortcut.triggered.connect(self.delete_selected_nodes) self.addAction(self.delete_shortcut) self.update_window_title() self.setGeometry(50, 50, 2000, 1200) # Wider window for 3-panel layout self.setStyleSheet(HARMONIOUS_THEME_STYLESHEET) def setup_integrated_ui(self): """Setup the integrated UI with node templates, pipeline editor and configuration panels""" central_widget = QWidget() self.setCentralWidget(central_widget) # Main horizontal splitter with 3 panels main_splitter = QSplitter(Qt.Horizontal) main_splitter.setStyleSheet(""" QSplitter::handle { background-color: #45475a; width: 3px; } QSplitter::handle:hover { background-color: #89b4fa; } """) # Left side: Node Template Panel (20% width) left_panel = self.create_node_template_panel() left_panel.setMinimumWidth(250) left_panel.setMaximumWidth(350) # Middle: Pipeline Editor (50% width) editor_widget = QWidget() editor_layout = QVBoxLayout(editor_widget) editor_layout.setContentsMargins(5, 5, 5, 5) # Add pipeline editor title editor_title = QLabel("Pipeline Editor") editor_title.setStyleSheet("color: #f9e2af; font-size: 16px; font-weight: bold; padding: 10px;") editor_layout.addWidget(editor_title) # Add the node graph widget graph_widget = self.graph.widget graph_widget.setMinimumHeight(400) editor_layout.addWidget(graph_widget) # Right side: Configuration panels (30% width) right_panel = QWidget() right_panel.setMinimumWidth(350) right_panel.setMaximumWidth(450) right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(5, 5, 5, 5) right_layout.setSpacing(10) # Create tabs for different configuration sections config_tabs = QTabWidget() config_tabs.setStyleSheet(""" QTabWidget::pane { border: 2px solid #45475a; border-radius: 8px; background-color: #313244; } QTabWidget::tab-bar { alignment: center; } QTabBar::tab { background-color: #45475a; color: #cdd6f4; padding: 6px 12px; margin: 1px; border-radius: 4px; font-size: 11px; } QTabBar::tab:selected { background-color: #89b4fa; color: #1e1e2e; font-weight: bold; } QTabBar::tab:hover { background-color: #585b70; } """) # Node Properties Tab (most important for editing) node_props_panel = self.create_node_properties_panel() config_tabs.addTab(node_props_panel, "📝 Properties") # Stage Configuration Tab stage_config = self.create_stage_config_panel() config_tabs.addTab(stage_config, "⚙️ Stages") # Performance Estimation Tab performance_panel = self.create_performance_panel() config_tabs.addTab(performance_panel, "📊 Performance") # Dongle Management Tab dongle_panel = self.create_dongle_panel() config_tabs.addTab(dongle_panel, "🔌 Dongles") right_layout.addWidget(config_tabs) # Add widgets to splitter main_splitter.addWidget(left_panel) main_splitter.addWidget(editor_widget) main_splitter.addWidget(right_panel) main_splitter.setSizes([300, 800, 400]) # 20-50-30 split # Set main layout main_layout = QVBoxLayout(central_widget) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.addWidget(main_splitter) def create_node_template_panel(self): """Create left panel with node templates""" panel = QWidget() layout = QVBoxLayout(panel) layout.setContentsMargins(10, 10, 10, 10) layout.setSpacing(10) # Header header = QLabel("Node Templates") header.setStyleSheet("color: #f9e2af; font-size: 16px; font-weight: bold; padding: 10px;") layout.addWidget(header) # Node template buttons nodes_info = [ ("🎯 Input Node", "Data input source", InputNode), ("🧠 Model Node", "AI inference model", ModelNode), ("⚙️ Preprocess Node", "Data preprocessing", PreprocessNode), ("🔧 Postprocess Node", "Output processing", PostprocessNode), ("📤 Output Node", "Final output", OutputNode) ] for name, description, node_class in nodes_info: # Create container for each node type node_container = QFrame() node_container.setStyleSheet(""" QFrame { background-color: #313244; border: 2px solid #45475a; border-radius: 8px; padding: 5px; } QFrame:hover { border-color: #89b4fa; background-color: #383a59; } """) container_layout = QVBoxLayout(node_container) container_layout.setContentsMargins(8, 8, 8, 8) container_layout.setSpacing(4) # Node name name_label = QLabel(name) name_label.setStyleSheet("color: #cdd6f4; font-weight: bold; font-size: 12px;") container_layout.addWidget(name_label) # Description desc_label = QLabel(description) desc_label.setStyleSheet("color: #a6adc8; font-size: 10px;") desc_label.setWordWrap(True) container_layout.addWidget(desc_label) # Add button add_btn = QPushButton("+ Add") add_btn.setStyleSheet(""" QPushButton { background-color: #89b4fa; color: #1e1e2e; border: none; padding: 4px 8px; border-radius: 4px; font-size: 10px; font-weight: bold; } QPushButton:hover { background-color: #a6c8ff; } QPushButton:pressed { background-color: #7287fd; } """) add_btn.clicked.connect(lambda checked, nc=node_class: self.add_node_to_graph(nc)) container_layout.addWidget(add_btn) layout.addWidget(node_container) # Add stretch to push everything to top layout.addStretch() # Instructions instructions = QLabel("💡 Click 'Add' to insert nodes into the pipeline editor") instructions.setStyleSheet(""" color: #f9e2af; font-size: 10px; padding: 10px; background-color: #313244; border-radius: 6px; border-left: 3px solid #89b4fa; """) instructions.setWordWrap(True) layout.addWidget(instructions) return panel def create_node_properties_panel(self): """Create node properties editing panel""" widget = QScrollArea() content = QWidget() layout = QVBoxLayout(content) # Header header = QLabel("Node Properties") header.setStyleSheet("color: #f9e2af; font-size: 14px; font-weight: bold; padding: 5px;") layout.addWidget(header) # Instructions when no node selected self.props_instructions = QLabel("Select a node in the pipeline editor to view and edit its properties") self.props_instructions.setStyleSheet(""" color: #a6adc8; font-size: 12px; padding: 20px; background-color: #313244; border-radius: 8px; border: 2px dashed #45475a; """) self.props_instructions.setWordWrap(True) self.props_instructions.setAlignment(Qt.AlignCenter) layout.addWidget(self.props_instructions) # Container for dynamic properties (will be populated when node is selected) self.node_props_container = QWidget() self.node_props_layout = QVBoxLayout(self.node_props_container) layout.addWidget(self.node_props_container) # Initially hide the container self.node_props_container.setVisible(False) layout.addStretch() widget.setWidget(content) widget.setWidgetResizable(True) # Connect to node selection changes self.graph.node_selection_changed.connect(self.update_node_properties_panel) return widget def add_node_to_graph(self, node_class): """Add a new node to the graph""" try: # Create node instance directly and add to graph node = node_class() self.graph.add_node(node) # Position it in a reasonable location (center with some randomness) import random x = random.randint(-100, 100) y = random.randint(-100, 100) node.set_pos(x, y) print(f"✅ Added {node_class.__name__} to graph at position ({x}, {y})") self.mark_modified() # Auto-update performance estimation self.update_performance_estimation() except Exception as e: print(f"❌ Failed to add {node_class.__name__}: {e}") import traceback traceback.print_exc() QMessageBox.warning(self, "Error", f"Failed to add {node_class.__name__}:\n{str(e)}") def update_node_properties_panel(self): """Update the node properties panel based on selected node""" try: # Clear existing properties for i in reversed(range(self.node_props_layout.count())): child = self.node_props_layout.itemAt(i).widget() if child: child.setParent(None) selected_nodes = self.graph.selected_nodes() if not selected_nodes: # Show instructions when no node selected self.props_instructions.setVisible(True) self.node_props_container.setVisible(False) return # Hide instructions and show properties self.props_instructions.setVisible(False) self.node_props_container.setVisible(True) # Get first selected node node = selected_nodes[0] # Create properties form for the node self.create_node_properties_form(node) except Exception as e: print(f"Error updating node properties panel: {e}") def create_node_properties_form(self, node): """Create properties form for a specific node""" try: # Get node name safely try: node_name = node.name() if callable(node.name) else str(node.name) except: node_name = "Unknown Node" # Get node type safely try: node_type = node.type_() if callable(node.type_) else str(getattr(node, 'type_', 'Unknown')) except: node_type = "Unknown Type" # Node info header info_group = QGroupBox(f"{node_name} Properties") info_layout = QFormLayout(info_group) # Node name (editable) name_edit = QLineEdit(node_name) def update_node_name(text): try: if hasattr(node, 'set_name') and callable(node.set_name): node.set_name(text) self.mark_modified() except Exception as e: print(f"Error updating node name: {e}") name_edit.textChanged.connect(update_node_name) info_layout.addRow("Name:", name_edit) # Node type (read-only) type_label = QLabel(node_type) type_label.setStyleSheet("color: #a6adc8;") info_layout.addRow("Type:", type_label) self.node_props_layout.addWidget(info_group) # Get node properties - NodeGraphQt uses different property access methods custom_props = {} # Method 1: Try to get properties from NodeGraphQt node try: if hasattr(node, 'properties'): # Get all properties from the node all_props = node.properties() print(f"All node properties: {list(all_props.keys())}") # Check if there's a 'custom' property that contains our properties if 'custom' in all_props: custom_value = all_props['custom'] print(f"Custom property value type: {type(custom_value)}, value: {custom_value}") # If custom property contains a dict, use it if isinstance(custom_value, dict): custom_props = custom_value # If custom property is accessible via get_property, try that elif hasattr(node, 'get_property'): try: custom_from_get = node.get_property('custom') if isinstance(custom_from_get, dict): custom_props = custom_from_get except: pass # Also include other potentially useful properties useful_props = {} for k, v in all_props.items(): if k not in {'name', 'id', 'selected', 'disabled', 'visible', 'pos', 'color', 'type_', 'icon', 'border_color', 'text_color', 'width', 'height', 'layout_direction', 'port_deletion_allowed', 'subgraph_session'}: useful_props[k] = v # Merge custom_props with other useful properties if useful_props: custom_props.update(useful_props) print(f"Found properties via node.properties(): {list(custom_props.keys())}") except Exception as e: print(f"Method 1 - node.properties() failed: {e}") # Method 2: Try to access properties via get_property (for NodeGraphQt created properties) # This should work for properties created with create_property() try: # Get all properties defined for this node type if hasattr(node, 'get_property'): # Define properties for different node types node_type_properties = { 'ModelNode': ['model_path', 'dongle_series', 'num_dongles', 'port_id'], 'InputNode': ['input_path', 'source_type', 'fps', 'source_path'], 'OutputNode': ['output_path', 'output_format', 'save_results'], 'PreprocessNode': ['resize_width', 'resize_height', 'operations'], 'PostprocessNode': ['confidence_threshold', 'nms_threshold', 'max_detections'] } # Try to determine node type node_class_name = node.__class__.__name__ properties_to_check = [] if node_class_name in node_type_properties: properties_to_check = node_type_properties[node_class_name] else: # Try all known properties if we can't determine the type properties_to_check = [] for props_list in node_type_properties.values(): properties_to_check.extend(props_list) print(f"Checking properties for {node_class_name}: {properties_to_check}") for prop in properties_to_check: try: value = node.get_property(prop) # Only add if the property actually exists (not None and not raising exception) custom_props[prop] = value print(f" Found {prop}: {value}") except Exception as prop_error: # Property doesn't exist for this node, which is expected pass print(f"Found properties via get_property(): {list(custom_props.keys())}") except Exception as e: print(f"Method 2 - get_property() failed: {e}") # Method 3: Try to access custom attribute if it exists if not custom_props: try: if hasattr(node, 'custom'): custom_attr = node.custom if not callable(node.custom) else node.custom() if isinstance(custom_attr, dict): custom_props = custom_attr print(f"Found properties via custom attribute: {list(custom_props.keys())}") except Exception as e: print(f"Method 3 - custom attribute failed: {e}") if custom_props: custom_group = QGroupBox("Custom Properties") custom_layout = QFormLayout(custom_group) for prop_name, prop_value in custom_props.items(): if prop_name in ['model_path', 'input_path', 'output_path']: # File path property path_layout = QHBoxLayout() path_edit = QLineEdit(str(prop_value)) browse_btn = QPushButton("Browse...") browse_btn.clicked.connect(lambda checked, pe=path_edit, pn=prop_name: self.browse_file_for_property(pe, pn)) path_layout.addWidget(path_edit) path_layout.addWidget(browse_btn) path_widget = QWidget() path_widget.setLayout(path_layout) custom_layout.addRow(prop_name.replace("_", " ").title() + ":", path_widget) # Connect to update node property path_edit.textChanged.connect(lambda text, pn=prop_name: self.update_node_property(node, pn, text)) elif isinstance(prop_value, (int, float)): # Numeric property if isinstance(prop_value, int): spin_box = QSpinBox() spin_box.setRange(-999999, 999999) spin_box.setValue(prop_value) spin_box.valueChanged.connect(lambda val, pn=prop_name: self.update_node_property(node, pn, val)) else: spin_box = QDoubleSpinBox() spin_box.setRange(-999999.0, 999999.0) spin_box.setValue(prop_value) spin_box.valueChanged.connect(lambda val, pn=prop_name: self.update_node_property(node, pn, val)) custom_layout.addRow(prop_name.replace("_", " ").title() + ":", spin_box) elif isinstance(prop_value, bool): # Boolean property check_box = QCheckBox() check_box.setChecked(prop_value) check_box.toggled.connect(lambda checked, pn=prop_name: self.update_node_property(node, pn, checked)) custom_layout.addRow(prop_name.replace("_", " ").title() + ":", check_box) else: # String property text_edit = QLineEdit(str(prop_value)) text_edit.textChanged.connect(lambda text, pn=prop_name: self.update_node_property(node, pn, text)) custom_layout.addRow(prop_name.replace("_", " ").title() + ":", text_edit) self.node_props_layout.addWidget(custom_group) # Position info (read-only) pos_group = QGroupBox("Position") pos_layout = QFormLayout(pos_group) # Get position safely try: pos = node.pos() if callable(node.pos) else node.pos if isinstance(pos, (list, tuple)) and len(pos) >= 2: x, y = pos[0], pos[1] else: x, y = 0.0, 0.0 except Exception as e: print(f"Error getting node position: {e}") x, y = 0.0, 0.0 x_label = QLabel(f"{x:.1f}") y_label = QLabel(f"{y:.1f}") x_label.setStyleSheet("color: #a6adc8;") y_label.setStyleSheet("color: #a6adc8;") pos_layout.addRow("X:", x_label) pos_layout.addRow("Y:", y_label) self.node_props_layout.addWidget(pos_group) except Exception as e: print(f"Error creating properties form: {e}") error_label = QLabel(f"Error loading properties: {str(e)}") error_label.setStyleSheet("color: #f38ba8;") self.node_props_layout.addWidget(error_label) def update_node_property(self, node, property_name, value): """Update a node's custom property""" try: success = False # Method 1: Try NodeGraphQt set_property if hasattr(node, 'set_property') and callable(node.set_property): try: node.set_property(property_name, value) success = True print(f"✅ Updated via set_property: {property_name} = {value}") except Exception as e: print(f"set_property failed: {e}") # Method 2: Try custom attribute if not success and hasattr(node, 'custom'): try: if not callable(node.custom): node.custom[property_name] = value success = True print(f"✅ Updated via custom attribute: {property_name} = {value}") else: print(f"Warning: Cannot update property {property_name} - custom is a method") except Exception as e: print(f"custom attribute update failed: {e}") # Method 3: Try direct attribute setting if not success: try: setattr(node, property_name, value) success = True print(f"✅ Updated via setattr: {property_name} = {value}") except Exception as e: print(f"setattr failed: {e}") if success: self.mark_modified() # Get node name safely for logging try: node_name = node.name() if callable(node.name) else str(node.name) except: node_name = "Unknown Node" print(f"✅ Successfully updated {node_name}.{property_name} = {value}") else: print(f"❌ Failed to update property {property_name} with all methods") except Exception as e: print(f"❌ Error updating node property {property_name}: {e}") import traceback traceback.print_exc() def browse_file_for_property(self, line_edit, property_name): """Open file browser for file path properties""" try: if 'model' in property_name.lower(): file_filter = "Model files (*.nef *.onnx *.tflite);;All files (*.*)" title = "Select Model File" elif 'input' in property_name.lower(): file_filter = "Media files (*.mp4 *.avi *.jpg *.png *.bmp);;All files (*.*)" title = "Select Input File" else: file_filter = "All files (*.*)" title = "Select File" filename, _ = QFileDialog.getOpenFileName(self, title, "", file_filter) if filename: line_edit.setText(filename) except Exception as e: print(f"Error browsing file: {e}") def create_stage_config_panel(self): """Create stage configuration panel""" widget = QScrollArea() content = QWidget() layout = QVBoxLayout(content) # Header header = QLabel("Stage Configuration") header.setStyleSheet("color: #f9e2af; font-size: 14px; font-weight: bold; padding: 5px;") layout.addWidget(header) # Number of stages stages_layout = QHBoxLayout() stages_layout.addWidget(QLabel("Number of Stages:")) self.stages_spinbox = QSpinBox() self.stages_spinbox.setRange(1, 10) self.stages_spinbox.setValue(2) self.stages_spinbox.valueChanged.connect(self.update_stage_configs) stages_layout.addWidget(self.stages_spinbox) layout.addLayout(stages_layout) # Container for dynamic stage configurations self.stage_configs_container = QWidget() self.stage_configs_layout = QVBoxLayout(self.stage_configs_container) layout.addWidget(self.stage_configs_container) # Initialize with default stages self.update_stage_configs() layout.addStretch() widget.setWidget(content) widget.setWidgetResizable(True) return widget def create_performance_panel(self): """Create performance estimation panel""" widget = QScrollArea() content = QWidget() layout = QVBoxLayout(content) # Header header = QLabel("Performance Estimation") header.setStyleSheet("color: #f9e2af; font-size: 14px; font-weight: bold; padding: 5px;") layout.addWidget(header) # Performance metrics metrics_group = QGroupBox("Pipeline Metrics") metrics_layout = QGridLayout(metrics_group) # FPS estimation metrics_layout.addWidget(QLabel("Estimated FPS:"), 0, 0) self.fps_label = QLabel("0.0") self.fps_label.setStyleSheet("color: #a6e3a1; font-weight: bold;") metrics_layout.addWidget(self.fps_label, 0, 1) # Latency estimation metrics_layout.addWidget(QLabel("Pipeline Latency:"), 1, 0) self.latency_label = QLabel("0.0 ms") self.latency_label.setStyleSheet("color: #fab387; font-weight: bold;") metrics_layout.addWidget(self.latency_label, 1, 1) # Memory usage metrics_layout.addWidget(QLabel("Memory Usage:"), 2, 0) self.memory_label = QLabel("0 MB") self.memory_label.setStyleSheet("color: #f38ba8; font-weight: bold;") metrics_layout.addWidget(self.memory_label, 2, 1) layout.addWidget(metrics_group) # Performance optimization suggestions suggestions_group = QGroupBox("Optimization Suggestions") suggestions_layout = QVBoxLayout(suggestions_group) self.suggestions_text = QTextBrowser() self.suggestions_text.setMaximumHeight(150) self.suggestions_text.setText("• Connect nodes to see performance analysis\n• Add stages to see optimization suggestions") suggestions_layout.addWidget(self.suggestions_text) layout.addWidget(suggestions_group) # Update button update_btn = QPushButton("🔄 Update Performance") update_btn.clicked.connect(self.update_performance_estimation) layout.addWidget(update_btn) layout.addStretch() widget.setWidget(content) widget.setWidgetResizable(True) return widget def create_dongle_panel(self): """Create dongle management panel""" widget = QScrollArea() content = QWidget() layout = QVBoxLayout(content) # Header header = QLabel("Dongle Management") header.setStyleSheet("color: #f9e2af; font-size: 14px; font-weight: bold; padding: 5px;") layout.addWidget(header) # Available dongles available_group = QGroupBox("Available Dongles") available_layout = QVBoxLayout(available_group) # Auto-detect button detect_btn = QPushButton("🔍 Auto-Detect Dongles") detect_btn.clicked.connect(self.detect_dongles) available_layout.addWidget(detect_btn) # Dongles list self.dongles_list = QListWidget() self.dongles_list.setMaximumHeight(120) available_layout.addWidget(self.dongles_list) layout.addWidget(available_group) # Dongle allocation allocation_group = QGroupBox("Stage Allocation") allocation_layout = QVBoxLayout(allocation_group) # Container for allocation widgets self.allocation_container = QWidget() self.allocation_layout = QVBoxLayout(self.allocation_container) allocation_layout.addWidget(self.allocation_container) layout.addWidget(allocation_group) # Initialize dongle detection self.detect_dongles() layout.addStretch() widget.setWidget(content) widget.setWidgetResizable(True) return widget def update_stage_configs(self): """Update stage configuration widgets based on number of stages""" if not self.stage_configs_layout or not self.stages_spinbox: return # Clear existing configs for i in reversed(range(self.stage_configs_layout.count())): self.stage_configs_layout.itemAt(i).widget().setParent(None) # Add new stage configs num_stages = self.stages_spinbox.value() for i in range(num_stages): stage_group = QGroupBox(f"Stage {i+1}") stage_layout = QFormLayout(stage_group) # Model path model_edit = QLineEdit() model_edit.setPlaceholderText("model.nef") stage_layout.addRow("Model Path:", model_edit) # Port IDs ports_edit = QLineEdit() ports_edit.setPlaceholderText("28,32") stage_layout.addRow("Port IDs:", ports_edit) # Queue size queue_spin = QSpinBox() queue_spin.setRange(1, 100) queue_spin.setValue(10) stage_layout.addRow("Queue Size:", queue_spin) self.stage_configs_layout.addWidget(stage_group) # Update allocation panel self.update_allocation_panel() def update_allocation_panel(self): """Update dongle allocation panel""" if not self.allocation_layout or not self.stages_spinbox or not self.dongles_list: return # Clear existing allocations for i in reversed(range(self.allocation_layout.count())): self.allocation_layout.itemAt(i).widget().setParent(None) # Add allocation widgets for each stage num_stages = self.stages_spinbox.value() for i in range(num_stages): alloc_layout = QHBoxLayout() alloc_layout.addWidget(QLabel(f"Stage {i+1}:")) dongle_combo = QComboBox() dongle_combo.addItem("Auto-assign") # Add detected dongles for j in range(self.dongles_list.count()): dongle_combo.addItem(self.dongles_list.item(j).text()) alloc_layout.addWidget(dongle_combo) alloc_widget = QWidget() alloc_widget.setLayout(alloc_layout) self.allocation_layout.addWidget(alloc_widget) def detect_dongles(self): """Simulate dongle detection""" if not self.dongles_list: return self.dongles_list.clear() # Simulate detected dongles dongles = ["KL520 Dongle (Port 28)", "KL520 Dongle (Port 32)", "KL720 Dongle (Port 36)"] for dongle in dongles: self.dongles_list.addItem(f"🔌 {dongle}") def update_performance_estimation(self): """Update performance metrics based on current pipeline""" if not all([self.fps_label, self.latency_label, self.memory_label, self.suggestions_text, self.stages_spinbox]): return # Simulate performance calculation num_nodes = len(self.graph.all_nodes()) num_stages = self.stages_spinbox.value() # Simple estimation logic base_fps = 30.0 fps = base_fps / max(1, num_stages * 0.5) latency = num_stages * 15 + num_nodes * 5 memory = num_stages * 50 + num_nodes * 20 self.fps_label.setText(f"{fps:.1f}") self.latency_label.setText(f"{latency:.1f} ms") self.memory_label.setText(f"{memory} MB") # Generate suggestions suggestions = [] if num_stages > 3: suggestions.append("• Consider reducing stages for better performance") if num_nodes > 5: suggestions.append("• Complex pipelines may benefit from optimization") if fps < 15: suggestions.append("• Low FPS detected - check dongle allocation") if not suggestions: suggestions = ["• Pipeline looks optimized!", "• Consider adding more stages for complex workflows"] self.suggestions_text.setText("\n".join(suggestions)) def update_window_title(self): title = f"Pipeline Dashboard - {self.project_name or 'Untitled'}" if self.current_file: title += f" [{os.path.basename(self.current_file)}]" if self.is_modified: title += "*" self.setWindowTitle(title) def mark_modified(self, *args, **kwargs): if not self.is_modified: self.is_modified = True self.update_window_title() def mark_saved(self): if self.is_modified: self.is_modified = False self.update_window_title() def on_port_connected(self, *args, **kwargs): """Handle port connection - update performance automatically""" self.update_performance_estimation() def on_port_disconnected(self, *args, **kwargs): """Handle port disconnection - update performance automatically""" self.update_performance_estimation() def delete_selected_nodes(self): """Delete selected nodes""" selected_nodes = self.graph.selected_nodes() for node in selected_nodes: self.graph.delete_node(node) def setup_menu(self): """Setup menu bar""" menubar = self.menuBar() # File menu file_menu = menubar.addMenu('File') save_action = QAction('Save', self) save_action.setShortcut('Ctrl+S') save_action.triggered.connect(self.save_pipeline) file_menu.addAction(save_action) save_as_action = QAction('Save As...', self) save_as_action.setShortcut('Ctrl+Shift+S') save_as_action.triggered.connect(self.save_pipeline_as) file_menu.addAction(save_as_action) file_menu.addSeparator() export_action = QAction('Export Configuration', self) export_action.triggered.connect(self.export_configuration) file_menu.addAction(export_action) def save_pipeline(self): """Save pipeline to current file""" if self.current_file: self.save_to_file(self.current_file) else: self.save_pipeline_as() def save_pipeline_as(self): """Save pipeline with file dialog""" filename, _ = QFileDialog.getSaveFileName( self, "Save Pipeline As", f"{self.project_name}.mflow", "MFlow files (*.mflow);;All files (*.*)") if filename: self.current_file = filename self.save_to_file(filename) def save_to_file(self, filename): """Save pipeline data to file""" try: performance_metrics = {} if all([self.fps_label, self.latency_label, self.memory_label]): performance_metrics = { "fps": self.fps_label.text(), "latency": self.latency_label.text(), "memory": self.memory_label.text() } pipeline_data = { "project_name": self.project_name, "description": self.description, "graph_data": self.graph.serialize_session(), "stage_configs": self.get_stage_configs(), "performance_metrics": performance_metrics, "metadata": {"version": "1.0", "editor": "IntegratedDashboard"} } with open(filename, 'w') as f: json.dump(pipeline_data, f, indent=2) self.mark_saved() QMessageBox.information(self, "Success", f"Pipeline saved to {os.path.basename(filename)}") except Exception as e: QMessageBox.critical(self, "Error", f"Failed to save pipeline: {str(e)}") def get_stage_configs(self): """Extract stage configurations from UI""" if not self.stage_configs_layout: return [] configs = [] for i in range(self.stage_configs_layout.count()): widget = self.stage_configs_layout.itemAt(i).widget() if isinstance(widget, QGroupBox): layout = widget.layout() config = {} for j in range(0, layout.rowCount()): label_item = layout.itemAt(j, QFormLayout.LabelRole) field_item = layout.itemAt(j, QFormLayout.FieldRole) if label_item and field_item: label = label_item.widget().text().replace(":", "") field = field_item.widget() if isinstance(field, QLineEdit): config[label.lower().replace(" ", "_")] = field.text() elif isinstance(field, QSpinBox): config[label.lower().replace(" ", "_")] = field.value() configs.append(config) return configs def load_pipeline_file(self, filename): """Load pipeline from file""" try: with open(filename, 'r') as f: data = json.load(f) # Load graph data if "graph_data" in data: self.graph.deserialize_session(data["graph_data"]) # Load stage configs if available if "stage_configs" in data: self.load_stage_configs(data["stage_configs"]) # Load performance metrics if available if "performance_metrics" in data: metrics = data["performance_metrics"] self.fps_label.setText(metrics.get("fps", "0.0")) self.latency_label.setText(metrics.get("latency", "0.0 ms")) self.memory_label.setText(metrics.get("memory", "0 MB")) self.mark_saved() self.update_performance_estimation() except Exception as e: QMessageBox.critical(self, "Error", f"Failed to load pipeline: {str(e)}") def load_stage_configs(self, configs): """Load stage configurations into UI""" if not self.stages_spinbox or not self.stage_configs_layout or not configs: return self.stages_spinbox.setValue(len(configs)) # The update_stage_configs will be called automatically # Then we can populate the fields for i, config in enumerate(configs): if i < self.stage_configs_layout.count(): widget = self.stage_configs_layout.itemAt(i).widget() if isinstance(widget, QGroupBox): layout = widget.layout() for j in range(0, layout.rowCount()): label_item = layout.itemAt(j, QFormLayout.LabelRole) field_item = layout.itemAt(j, QFormLayout.FieldRole) if label_item and field_item: label = label_item.widget().text().replace(":", "") field = field_item.widget() key = label.lower().replace(" ", "_") if key in config: if isinstance(field, QLineEdit): field.setText(str(config[key])) elif isinstance(field, QSpinBox): field.setValue(int(config[key])) def export_configuration(self): """Export pipeline configuration for deployment""" try: dongles = [] if self.dongles_list: dongles = [self.dongles_list.item(i).text() for i in range(self.dongles_list.count())] performance_estimate = {} if all([self.fps_label, self.latency_label, self.memory_label]): performance_estimate = { "fps": self.fps_label.text(), "latency": self.latency_label.text(), "memory": self.memory_label.text() } config_data = { "pipeline_name": self.project_name, "stages": self.get_stage_configs(), "performance_estimate": performance_estimate, "dongles": dongles, "export_timestamp": json.dumps({"timestamp": "2024-01-01T00:00:00Z"}) } filename, _ = QFileDialog.getSaveFileName( self, "Export Configuration", f"{self.project_name}_config.json", "JSON files (*.json);;All files (*.*)") if filename: with open(filename, 'w') as f: json.dump(config_data, f, indent=2) QMessageBox.information(self, "Success", f"Configuration exported to {os.path.basename(filename)}") except Exception as e: QMessageBox.critical(self, "Error", f"Failed to export configuration: {str(e)}") class PipelineEditor(QMainWindow): """Main pipeline editor using NodeGraphQt""" def __init__(self, project_name="", description="", filename=None): super().__init__() self.project_name = project_name self.description = description self.current_file = filename self.is_modified = False # Initialize node graph self.graph = NodeGraph(properties_bin_class=None) # Register custom nodes nodes_to_register = [InputNode, ModelNode, PreprocessNode, PostprocessNode, OutputNode] for node_class in nodes_to_register: try: self.graph.register_node(node_class) except Exception as e: print(f"Failed to register {node_class.__name__}: {e}") # --- MODIFICATION START --- # Create properties bin widget to be docked in the main window. try: # 完全禁用原来的PropertiesBinWidget,使用我们的自定义面板 print("Creating CustomPropertiesWidget...") # 调试信息 self.properties_bin = CustomPropertiesWidget(self.graph) self.properties_bin.setMinimumWidth(300) print("CustomPropertiesWidget created successfully") # 调试信息 except Exception as e: print(f"Failed to create CustomPropertiesWidget: {e}") import traceback traceback.print_exc() self.properties_bin = None # --- MODIFICATION END --- # Connect signals # We no longer need to connect node_double_clicked to show properties. # The PropertiesBinWidget handles selection changes automatically. self.graph.node_created.connect(self.mark_modified) # type: ignore self.graph.nodes_deleted.connect(self.mark_modified) # type: ignore self.graph.property_changed.connect(self.mark_modified) # type: ignore if hasattr(self.graph, 'port_connected'): self.graph.port_connected.connect(self.on_port_connected) if hasattr(self.graph, 'port_disconnected'): self.graph.port_disconnected.connect(self.on_port_disconnected) self.setup_ui() self.setup_menu() # Add keyboard shortcut for delete self.delete_shortcut = QAction("Delete", self) self.delete_shortcut.setShortcut('Delete') self.delete_shortcut.triggered.connect(self.delete_selected_nodes) self.addAction(self.delete_shortcut) self.update_window_title() self.setGeometry(100, 100, 1600, 900) # Increased width for the new panel self.setStyleSheet(HARMONIOUS_THEME_STYLESHEET) def update_window_title(self): title = f"Pipeline Editor - {self.project_name or 'Untitled'}" if self.current_file: title += f" [{os.path.basename(self.current_file)}]" if self.is_modified: title += "*" self.setWindowTitle(title) def mark_modified(self, *args, **kwargs): if not self.is_modified: self.is_modified = True self.update_window_title() def mark_saved(self): if self.is_modified: self.is_modified = False self.update_window_title() # This method is no longer needed as the primary way to show properties. # Kept for the fallback dialog. def display_properties_bin(self, node): if not self.properties_bin: self.show_simple_properties_dialog(node) # If the properties bin exists, it updates automatically. # Double-clicking can still be used to ensure it's visible. elif self.properties_bin and not self.properties_bin.isVisible(): self.properties_bin.show() def show_simple_properties_dialog(self, node): try: dialog = SimplePropertiesDialog(node, self) if dialog.exec_() == QDialog.Accepted: self.mark_modified() except Exception as e: print(f"Error with interactive SimplePropertiesDialog for {node.name()}: {e}") QMessageBox.warning(self, "Properties Error", f"Could not display interactive properties for {node.name()}: {str(e)}") def on_port_connected(self, input_port, output_port): self.mark_modified() def on_port_disconnected(self, input_port, output_port): self.mark_modified() def show_connections(self): all_nodes = self.graph.all_nodes() connections = [] for node in all_nodes: for output_port in node.output_ports(): for connected_input_port in output_port.connected_ports(): connections.append(f"{node.name()}.{output_port.name()} → {connected_input_port.node().name()}.{connected_input_port.name()}") QMessageBox.information(self, "Graph Connections", "Current connections:\n" + "\n".join(connections) if connections else "No connections found.") def show_registered_nodes(self): try: registered_info = [] if hasattr(self.graph, 'all_registered_nodes'): nodes_dict = self.graph.all_registered_nodes() registered_info.append(f"all_registered_nodes(): {list(nodes_dict.keys())}") msg = "Registered node information:\n\n" + "\n\n".join(registered_info) QMessageBox.information(self, "Registered Nodes Debug", msg) except Exception as e: QMessageBox.critical(self, "Debug Error", f"Error getting registered nodes: {e}") def setup_ui(self): """Setup main UI with a docked properties panel.""" central_widget = QWidget() self.setCentralWidget(central_widget) layout = QHBoxLayout(central_widget) # --- MODIFICATION START --- # The main splitter will now hold three widgets: Palette, Graph, Properties. main_splitter = QSplitter(Qt.Horizontal) # 1. Left Panel: Node Palette self.palette = NodePalette(self.graph) self.palette.setMaximumWidth(250) main_splitter.addWidget(self.palette) # 2. Center Panel: Node Graph graph_widget = self.graph.widget graph_widget.setFocusPolicy(Qt.StrongFocus) main_splitter.addWidget(graph_widget) # 3. Right Panel: Properties Bin if self.properties_bin: main_splitter.addWidget(self.properties_bin) # Set initial sizes for the three panels main_splitter.setSizes([220, 1080, 300]) else: # Fallback if properties bin failed to create main_splitter.setSizes([250, 1350]) # --- MODIFICATION END --- layout.addWidget(main_splitter) graph_widget.setFocus() def setup_menu(self): menubar = self.menuBar() file_menu = menubar.addMenu('File') # ... (File menu remains the same) save_action = file_menu.addAction('Save') save_action.setShortcut('Ctrl+S') save_action.triggered.connect(self.save_pipeline) save_as_action = file_menu.addAction('Save As...') save_as_action.triggered.connect(self.save_pipeline_as) open_action = file_menu.addAction('Open...') open_action.setShortcut('Ctrl+O') open_action.triggered.connect(self.open_pipeline) file_menu.addSeparator() close_action = file_menu.addAction('Close') close_action.triggered.connect(self.close) edit_menu = menubar.addMenu('Edit') # ... (Edit menu remains the same) undo_action = edit_menu.addAction('Undo') undo_action.setShortcut('Ctrl+Z') undo_action.triggered.connect(self.graph.undo_stack().undo) redo_action = edit_menu.addAction('Redo') redo_action.setShortcut('Ctrl+Y') redo_action.triggered.connect(self.graph.undo_stack().redo) edit_menu.addSeparator() delete_action = edit_menu.addAction('Delete Selected') delete_action.setShortcut('Delete') delete_action.triggered.connect(self.delete_selected_nodes) self.addAction(delete_action) select_all_action = edit_menu.addAction('Select All') select_all_action.setShortcut('Ctrl+A') select_all_action.triggered.connect(self.graph.select_all) clear_selection_action = edit_menu.addAction('Clear Selection') clear_selection_action.setShortcut('Ctrl+D') clear_selection_action.triggered.connect(self.graph.clear_selection) view_menu = menubar.addMenu('View') # --- MODIFICATION START --- # Change the action to be a checkable toggle for the properties panel. if self.properties_bin: properties_action = view_menu.addAction('Toggle Properties Panel') properties_action.setCheckable(True) properties_action.setChecked(True) # Start with the panel visible. properties_action.triggered.connect(self.toggle_properties_panel) # --- MODIFICATION END --- # Pipeline menu pipeline_menu = menubar.addMenu('Pipeline') configure_stages_action = pipeline_menu.addAction('Configure Stages') configure_stages_action.triggered.connect(self.show_stage_configuration_from_editor) performance_action = pipeline_menu.addAction('Performance Analysis') performance_action.triggered.connect(self.show_performance_analysis) deploy_action = pipeline_menu.addAction('Deploy Pipeline') deploy_action.triggered.connect(self.show_deploy_dialog) view_menu.addSeparator() fit_action = view_menu.addAction('Fit to Selection') fit_action.setShortcut('F') fit_action.triggered.connect(self.graph.fit_to_selection) auto_layout_action = view_menu.addAction('Auto Layout All') auto_layout_action.triggered.connect(self.graph.auto_layout_nodes) # type: ignore debug_menu = menubar.addMenu('Debug') # ... (Debug menu remains the same) show_connections_action = debug_menu.addAction('Show All Connections') show_connections_action.triggered.connect(self.show_connections) show_registered_action = debug_menu.addAction('Show Registered Nodes') show_registered_action.triggered.connect(self.show_registered_nodes) # --- NEW METHOD START --- def toggle_properties_panel(self, checked): """Show or hide the properties panel.""" if self.properties_bin: self.properties_bin.setVisible(checked) # --- NEW METHOD END --- # This method is now replaced by toggle_properties_panel def show_properties_panel(self): """Show properties panel safely (Legacy, now used for fallback).""" if self.properties_bin: if not self.properties_bin.isVisible(): self.properties_bin.show() self.properties_bin.raise_() self.properties_bin.activateWindow() else: selected_nodes = self.graph.selected_nodes() if selected_nodes: self.show_simple_properties_dialog(selected_nodes[0]) else: QMessageBox.information(self, "Properties Panel", "Properties panel is not available.") def delete_selected_nodes(self): selected_nodes = self.graph.selected_nodes() if selected_nodes: try: self.graph.delete_nodes(selected_nodes) except Exception as e: print(f"Error deleting nodes: {e}") QMessageBox.warning(self, "Delete Error", f"Failed to delete nodes: {e}") def save_pipeline(self): if self.current_file: return self._save_to_file(self.current_file) else: return self.save_pipeline_as() def save_pipeline_as(self): default_name = f"{self.project_name or 'untitled'}.mflow" filename, _ = QFileDialog.getSaveFileName( self, "Save Pipeline", default_name, "MFlow files (*.mflow);;All files (*.*)") if filename: if self._save_to_file(filename): self.current_file = filename self.project_name = os.path.splitext(os.path.basename(filename))[0] self.mark_saved() return True return False def _save_to_file(self, filename): try: graph_data = self.graph.serialize_session() pipeline_data = { "project_name": self.project_name, "description": self.description, "graph_data": graph_data, "metadata": { "version": "1.0", "editor": "NodeGraphQt" } } with open(filename, 'w') as f: json.dump(pipeline_data, f, indent=2) self.mark_saved() QMessageBox.information(self, "Saved", f"Pipeline saved to {os.path.basename(filename)}") return True except Exception as e: QMessageBox.critical(self, "Error", f"Failed to save to {filename}: {str(e)}") return False def open_pipeline(self): if self.is_modified: reply = QMessageBox.question(self, 'Save Changes', "Current pipeline has unsaved changes. Save before opening a new one?", QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, QMessageBox.Save) if reply == QMessageBox.Save: if not self.save_pipeline(): return elif reply == QMessageBox.Cancel: return filename, _ = QFileDialog.getOpenFileName( self, "Open Pipeline", "", "MFlow files (*.mflow);;All files (*.*)") if filename: self.load_pipeline_file(filename) def load_pipeline_file(self, filename): try: with open(filename, 'r') as f: data = json.load(f) self.project_name = data.get("project_name", os.path.splitext(os.path.basename(filename))[0]) self.description = data.get("description", "") self.graph.clear_session() if "graph_data" in data: self.graph.deserialize_session(data["graph_data"]) self.current_file = filename self.mark_saved() except Exception as e: QMessageBox.critical(self, "Error", f"Failed to open {filename}: {str(e)}") self.project_name = "Untitled" self.description = "" self.current_file = None self.graph.clear_session() self.mark_saved() def show_stage_configuration_from_editor(self): """Show stage configuration from editor""" # Get current pipeline data current_data = { "graph_data": self.graph.serialize_session(), "project_name": self.project_name, "description": self.description } config_dialog = StageConfigurationDialog(current_data, self) config_dialog.exec_() def show_performance_analysis(self): """Show performance analysis for current pipeline""" # Generate sample stage configs from current pipeline stage_configs = [ {"name": "Input Processing", "dongles": 2, "port_ids": "28,30", "model_path": ""}, {"name": "Main Inference", "dongles": 4, "port_ids": "32,34,36,38", "model_path": ""}, {"name": "Post Processing", "dongles": 2, "port_ids": "40,42", "model_path": ""} ] perf_panel = PerformanceEstimationPanel(stage_configs, self) perf_panel.exec_() def show_deploy_dialog(self): """Show deployment dialog for current pipeline""" # Generate sample stage configs from current pipeline stage_configs = [ {"name": "Input Processing", "dongles": 2, "port_ids": "28,30", "model_path": ""}, {"name": "Main Inference", "dongles": 4, "port_ids": "32,34,36,38", "model_path": ""}, {"name": "Post Processing", "dongles": 2, "port_ids": "40,42", "model_path": ""} ] deploy_dialog = SaveDeployDialog(stage_configs, self) deploy_dialog.exec_() def closeEvent(self, event): if self.is_modified: reply = QMessageBox.question(self, 'Save Changes', "The pipeline has unsaved changes. Do you want to save them?", QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, QMessageBox.Save) if reply == QMessageBox.Save: if self.save_pipeline(): event.accept() else: event.ignore() elif reply == QMessageBox.Discard: event.accept() else: event.ignore() else: event.accept() class DashboardLogin(QWidget): def __init__(self): super().__init__() self.setStyleSheet(HARMONIOUS_THEME_STYLESHEET) self.pipeline_editor = None # Keep track of the editor window self.recent_files = self.load_recent_files() # Store current project settings self.current_project_name = "" self.current_description = "" self.current_pipeline_data = {} self.initUI() def initUI(self): self.setWindowTitle("Cluster Dashboard - ML Pipeline Builder") self.setGeometry(300, 300, 700, 500) # Create main scroll area to ensure all content is viewable scrollArea = QScrollArea(self) scrollContent = QWidget() scrollArea.setWidget(scrollContent) scrollArea.setWidgetResizable(True) scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) mainLayout = QVBoxLayout(scrollContent) mainLayout.setContentsMargins(40, 40, 40, 40) mainLayout.setSpacing(25) # Header section with title and subtitle headerLayout = QVBoxLayout() headerLayout.setSpacing(8) titleLabel = QLabel("Cluster") titleFont = QFont("Segoe UI", 32, QFont.Bold) titleLabel.setFont(titleFont) titleLabel.setAlignment(Qt.AlignCenter) titleLabel.setStyleSheet("color: #89b4fa; margin-bottom: 5px;") subtitleLabel = QLabel("AI Pipeline Builder & Execution Platform") subtitleFont = QFont("Inter", 14) subtitleLabel.setFont(subtitleFont) subtitleLabel.setAlignment(Qt.AlignCenter) subtitleLabel.setStyleSheet("color: #a6adc8; margin-bottom: 20px;") headerLayout.addWidget(titleLabel) headerLayout.addWidget(subtitleLabel) # Action buttons section buttonLayout = QVBoxLayout() buttonLayout.setSpacing(15) # Create new pipeline button (primary action) self.createNewPipelineButton = QPushButton("🚀 Create New Pipeline") self.createNewPipelineButton.setMinimumHeight(55) self.createNewPipelineButton.clicked.connect(self.create_new_pipeline_dialog) self.createNewPipelineButton.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #89b4fa, stop:1 #74c7ec); color: #1e1e2e; border: none; padding: 15px 25px; border-radius: 12px; font-weight: 700; font-size: 16px; text-align: left; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #a6c8ff, stop:1 #89dceb); transform: translateY(-2px); } """) # Edit existing pipeline button self.editPipelineButton = QPushButton("📁 Edit Previous Pipeline") self.editPipelineButton.setMinimumHeight(55) self.editPipelineButton.clicked.connect(self.edit_previous_pipeline) self.editPipelineButton.setStyleSheet(""" QPushButton { background-color: #313244; color: #cdd6f4; border: 2px solid #89b4fa; padding: 15px 25px; border-radius: 12px; font-weight: 600; font-size: 16px; text-align: left; } QPushButton:hover { background-color: #383a59; border-color: #a6c8ff; color: #ffffff; } """) # Recent files section recentLabel = QLabel("Recent Pipelines") recentLabel.setFont(QFont("Segoe UI", 12, QFont.Bold)) recentLabel.setStyleSheet("color: #f9e2af; margin-top: 10px;") self.recentFilesList = QListWidget() self.recentFilesList.setMaximumHeight(120) self.recentFilesList.setStyleSheet(""" QListWidget { background-color: #313244; border: 2px solid #45475a; border-radius: 12px; padding: 8px; } QListWidget::item { padding: 10px 14px; border-radius: 6px; margin: 2px; color: #cdd6f4; } QListWidget::item:hover { background-color: #383a59; } QListWidget::item:selected { background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #89b4fa, stop:1 #74c7ec); color: #1e1e2e; } """) self.populate_recent_files() self.recentFilesList.itemDoubleClicked.connect(self.open_recent_file) # Help/Info section infoLabel = QLabel("💡 Quick Start Guide") infoLabel.setFont(QFont("Segoe UI", 12, QFont.Bold)) infoLabel.setStyleSheet("color: #f9e2af; margin-top: 15px;") helpText = QLabel( "• Create a new pipeline to build ML workflows visually\n" "• Edit existing .mflow files to continue work\n" "• Drag and connect nodes to create processing chains\n" "• Configure dongle properties in the integrated panel\n" "• Monitor performance and deploy your pipelines" ) helpText.setStyleSheet(""" color: #a6adc8; font-size: 12px; line-height: 1.6; padding: 16px; background-color: #313244; border-radius: 12px; border-left: 4px solid #89b4fa; """) helpText.setWordWrap(True) # Add all sections to main layout buttonLayout.addWidget(self.createNewPipelineButton) buttonLayout.addWidget(self.editPipelineButton) mainLayout.addLayout(headerLayout) mainLayout.addSpacing(20) mainLayout.addLayout(buttonLayout) mainLayout.addSpacing(15) mainLayout.addWidget(recentLabel) mainLayout.addWidget(self.recentFilesList) mainLayout.addSpacing(10) mainLayout.addWidget(infoLabel) mainLayout.addWidget(helpText) mainLayout.addStretch(1) # Set the scroll area as the main widget containerLayout = QVBoxLayout(self) containerLayout.setContentsMargins(0, 0, 0, 0) containerLayout.addWidget(scrollArea) def load_recent_files(self): """Load recent files from settings or return empty list""" try: recent_files_path = os.path.expanduser("~/.cluster_recent_files.json") if os.path.exists(recent_files_path): with open(recent_files_path, 'r') as f: return json.load(f) except Exception as e: print(f"Error loading recent files: {e}") return [] def save_recent_files(self): """Save recent files to settings""" try: recent_files_path = os.path.expanduser("~/.cluster_recent_files.json") with open(recent_files_path, 'w') as f: json.dump(self.recent_files, f, indent=2) except Exception as e: print(f"Error saving recent files: {e}") def add_recent_file(self, filepath): """Add file to recent files list""" if filepath in self.recent_files: self.recent_files.remove(filepath) self.recent_files.insert(0, filepath) # Keep only last 5 files self.recent_files = self.recent_files[:5] self.save_recent_files() self.populate_recent_files() def populate_recent_files(self): """Populate the recent files list widget""" self.recentFilesList.clear() for filepath in self.recent_files: if os.path.exists(filepath): filename = os.path.basename(filepath) self.recentFilesList.addItem(f"📄 {filename}") else: # Remove non-existent files self.recent_files.remove(filepath) if not self.recentFilesList.count(): self.recentFilesList.addItem("No recent files") def open_recent_file(self, item): """Open a recent file when double-clicked""" if item.text() == "No recent files": return filename = item.text().replace("📄 ", "") for filepath in self.recent_files: if os.path.basename(filepath) == filename: try: with open(filepath, 'r') as f: data = json.load(f) project_name = data.get("project_name", os.path.splitext(filename)[0]) description = data.get("description", "") # Store loaded pipeline data in dashboard self.current_project_name = project_name self.current_description = description self.current_pipeline_data = data self._open_editor_window(project_name, description, filepath) break except Exception as e: QMessageBox.critical(self, "Error", f"Failed to open {filename}: {str(e)}") def _open_editor_window(self, project_name, description, filename): """Helper to ensure only one editor is managed by dashboard, or handle existing.""" if self.pipeline_editor and self.pipeline_editor.isVisible(): # Ask if user wants to close current editor or if it's okay # For simplicity here, we just bring it to front. # A more robust solution might involve managing multiple editors or prompting user. self.pipeline_editor.raise_() self.pipeline_editor.activateWindow() QMessageBox.information(self, "Editor Open", "An editor window is already open.") # Optionally, load the new data into the existing editor if desired, after prompting. return # Create the integrated dashboard instead of separate pipeline editor self.pipeline_editor = IntegratedPipelineDashboard(project_name, description, filename) if filename and os.path.exists(filename): # If filename is provided and exists, load it self.pipeline_editor.load_pipeline_file(filename) self.pipeline_editor.show() def edit_previous_pipeline(self): """Open existing pipeline""" filename, _ = QFileDialog.getOpenFileName( self, "Open Pipeline", "", "MFlow files (*.mflow);;All files (*.*)") if filename: try: # Basic check, more validation could be in PipelineEditor.load_pipeline_file with open(filename, 'r') as f: data = json.load(f) project_name = data.get("project_name", os.path.splitext(os.path.basename(filename))[0]) description = data.get("description", "") # Store loaded pipeline data in dashboard self.current_project_name = project_name self.current_description = description self.current_pipeline_data = data self._open_editor_window(project_name, description, filename) self.add_recent_file(filename) except Exception as e: QMessageBox.critical(self, "Error", f"Failed to open pipeline {filename}: {str(e)}") def show_stage_configuration(self): """Show stage configuration dialog for quick pipeline setup""" # Use current project data if available, otherwise create minimal pipeline if self.current_pipeline_data: pipeline_data = self.current_pipeline_data.copy() else: # Create a minimal pipeline with current project settings pipeline_data = { "project_name": self.current_project_name or "Untitled Pipeline", "description": self.current_description or "Pipeline configuration", "graph_data": { "nodes": { "node1": {"type_": "ModelNode", "custom": {"model_path": ""}}, "node2": {"type_": "ModelNode", "custom": {"model_path": ""}} } }, "metadata": {"version": "1.0", "editor": "Dashboard"} } config_dialog = StageConfigurationDialog(pipeline_data, self) if config_dialog.exec_(): # Update stored pipeline data with stage configurations self.current_pipeline_data = pipeline_data self.current_pipeline_data["stage_configs"] = config_dialog.get_stage_configs() def create_new_pipeline_dialog(self): """Create new pipeline""" dialog = CreatePipelineDialog(self) if dialog.exec_(): data = dialog.get_data() # Store project settings in dashboard self.current_project_name = data['project_name'] self.current_description = data['description'] # Prompt for initial save location save_filename, _ = QFileDialog.getSaveFileName( self, "Save New Pipeline As", f"{data['project_name']}.mflow", "MFlow files (*.mflow);;All files (*.*)") if save_filename: try: # Create an empty pipeline structure to save empty_graph_data = NodeGraph().serialize_session() # Get empty session data pipeline_data = { "project_name": data['project_name'], "description": data['description'], "graph_data": empty_graph_data, # Start with an empty graph "metadata": { "version": "1.0", "editor": "NodeGraphQt" } } # Store current pipeline data in dashboard self.current_pipeline_data = pipeline_data with open(save_filename, 'w') as f: json.dump(pipeline_data, f, indent=2) QMessageBox.information( self, "Pipeline Created", f"New pipeline '{data['project_name']}' has been created and saved as {os.path.basename(save_filename)}." ) # Open editor with this new, saved pipeline self._open_editor_window(data['project_name'], data['description'], save_filename) self.add_recent_file(save_filename) except Exception as e: QMessageBox.critical(self, "Error", f"Failed to create and save new pipeline: {str(e)}") class StageConfigurationDialog(QDialog): """Dialog for configuring pipeline stages and dongle allocation""" def __init__(self, pipeline_data=None, parent=None): super().__init__(parent) self.pipeline_data = pipeline_data or {} self.stage_configs = [] self.total_dongles_available = 16 # Default max dongles self.setWindowTitle("Stage Configuration & Dongle Allocation") self.setMinimumSize(800, 600) self.setStyleSheet(HARMONIOUS_THEME_STYLESHEET) self.setup_ui() self.load_stages_from_pipeline() def setup_ui(self): layout = QVBoxLayout(self) # Header with project information project_name = self.pipeline_data.get("project_name", "Untitled Pipeline") header = QLabel(f"Configure Pipeline Stages - {project_name}") header.setFont(QFont("Arial", 16, QFont.Bold)) header.setStyleSheet("color: #89b4fa; margin-bottom: 5px;") layout.addWidget(header) # Project description if available description = self.pipeline_data.get("description", "") if description: desc_label = QLabel(f"Description: {description}") desc_label.setStyleSheet("color: #a6adc8; margin-bottom: 10px; font-style: italic;") desc_label.setWordWrap(True) layout.addWidget(desc_label) # Instructions instructions = QLabel( "Break your pipeline into sections (A → B → C...) and assign dongles for optimal performance" ) instructions.setStyleSheet("color: #a6adc8; margin-bottom: 15px;") instructions.setWordWrap(True) layout.addWidget(instructions) # Main content area content_splitter = QSplitter(Qt.Horizontal) # Left side: Stage list and controls left_widget = QWidget() left_layout = QVBoxLayout(left_widget) # Stage list stages_group = QGroupBox("Pipeline Stages") stages_layout = QVBoxLayout(stages_group) self.stages_list = QListWidget() self.stages_list.currentItemChanged.connect(self.on_stage_selected) stages_layout.addWidget(self.stages_list) # Stage controls stage_controls = QHBoxLayout() self.add_stage_btn = QPushButton("➕ Add Stage") self.add_stage_btn.clicked.connect(self.add_stage) self.remove_stage_btn = QPushButton("➖ Remove Stage") self.remove_stage_btn.clicked.connect(self.remove_stage) self.auto_balance_btn = QPushButton("⚖️ Auto-Balance") self.auto_balance_btn.clicked.connect(self.auto_balance_dongles) stage_controls.addWidget(self.add_stage_btn) stage_controls.addWidget(self.remove_stage_btn) stage_controls.addWidget(self.auto_balance_btn) stages_layout.addLayout(stage_controls) left_layout.addWidget(stages_group) # Right side: Stage configuration right_widget = QWidget() right_layout = QVBoxLayout(right_widget) # Stage details details_group = QGroupBox("Stage Configuration") details_layout = QFormLayout(details_group) self.stage_name_input = QLineEdit() self.stage_name_input.textChanged.connect(self.update_current_stage) details_layout.addRow("Stage Name:", self.stage_name_input) # Dongle allocation dongle_layout = QHBoxLayout() self.dongle_count_slider = QSlider(Qt.Horizontal) self.dongle_count_slider.setRange(1, 8) self.dongle_count_slider.setValue(2) self.dongle_count_slider.valueChanged.connect(self.update_dongle_count) self.dongle_count_label = QLabel("2 dongles") dongle_layout.addWidget(self.dongle_count_slider) dongle_layout.addWidget(self.dongle_count_label) details_layout.addRow("Dongles Assigned:", dongle_layout) # Port IDs self.port_ids_input = QLineEdit() self.port_ids_input.setPlaceholderText("e.g., 28,30,32 or auto") self.port_ids_input.textChanged.connect(self.update_current_stage) details_layout.addRow("Port IDs:", self.port_ids_input) # Model selection self.model_path_input = QLineEdit() model_browse_layout = QHBoxLayout() model_browse_btn = QPushButton("Browse") model_browse_btn.clicked.connect(self.browse_model_file) model_browse_layout.addWidget(self.model_path_input) model_browse_layout.addWidget(model_browse_btn) details_layout.addRow("Model File:", model_browse_layout) # Performance estimates perf_group = QGroupBox("Performance Estimation") perf_layout = QFormLayout(perf_group) self.estimated_fps_label = QLabel("--") self.estimated_latency_label = QLabel("--") self.throughput_label = QLabel("--") perf_layout.addRow("Estimated FPS:", self.estimated_fps_label) perf_layout.addRow("Latency:", self.estimated_latency_label) perf_layout.addRow("Throughput:", self.throughput_label) right_layout.addWidget(details_group) right_layout.addWidget(perf_group) # Resource summary resource_group = QGroupBox("Resource Summary") resource_layout = QFormLayout(resource_group) self.total_dongles_label = QLabel(f"0 / {self.total_dongles_available}") self.pipeline_fps_label = QLabel("--") resource_layout.addRow("Total Dongles Used:", self.total_dongles_label) resource_layout.addRow("Pipeline FPS:", self.pipeline_fps_label) right_layout.addWidget(resource_group) right_layout.addStretch() content_splitter.addWidget(left_widget) content_splitter.addWidget(right_widget) content_splitter.setSizes([400, 400]) layout.addWidget(content_splitter) # Buttons button_layout = QHBoxLayout() self.next_btn = QPushButton("Next: Performance Estimation") self.next_btn.clicked.connect(self.show_performance_panel) self.cancel_btn = QPushButton("Cancel") self.cancel_btn.clicked.connect(self.reject) button_layout.addStretch() button_layout.addWidget(self.cancel_btn) button_layout.addWidget(self.next_btn) layout.addLayout(button_layout) def load_stages_from_pipeline(self): """Extract stages from pipeline data""" if not self.pipeline_data.get('graph_data'): # Add default stage if no pipeline data self.add_stage() return # Parse graph data to identify stages # This is a simplified implementation nodes = self.pipeline_data.get('graph_data', {}).get('nodes', {}) model_nodes = [node for node in nodes.values() if 'Model' in node.get('type_', '')] if not model_nodes: self.add_stage() return for i, node in enumerate(model_nodes): stage_config = { 'name': f"Stage {i+1}", 'dongles': 2, 'port_ids': 'auto', 'model_path': node.get('custom', {}).get('model_path', ''), 'node_id': node.get('id', '') } self.stage_configs.append(stage_config) self.update_stages_list() def add_stage(self): """Add a new stage""" stage_num = len(self.stage_configs) + 1 stage_config = { 'name': f"Stage {stage_num}", 'dongles': 2, 'port_ids': 'auto', 'model_path': '', 'node_id': '' } self.stage_configs.append(stage_config) self.update_stages_list() # Select the new stage self.stages_list.setCurrentRow(len(self.stage_configs) - 1) def remove_stage(self): """Remove selected stage""" current_row = self.stages_list.currentRow() if current_row >= 0 and self.stage_configs: self.stage_configs.pop(current_row) self.update_stages_list() # Select previous stage or first stage if self.stage_configs: new_row = min(current_row, len(self.stage_configs) - 1) self.stages_list.setCurrentRow(new_row) def update_stages_list(self): """Update the stages list widget""" self.stages_list.clear() for i, config in enumerate(self.stage_configs): dongles = config['dongles'] item_text = f"{config['name']} ({dongles} dongles)" self.stages_list.addItem(item_text) self.update_resource_summary() def on_stage_selected(self, current, previous): """Handle stage selection""" row = self.stages_list.currentRow() if row >= 0 and row < len(self.stage_configs): config = self.stage_configs[row] # Update UI with selected stage data self.stage_name_input.setText(config['name']) self.dongle_count_slider.setValue(config['dongles']) self.port_ids_input.setText(config['port_ids']) self.model_path_input.setText(config['model_path']) # Update performance estimates self.update_performance_estimates(config) def update_current_stage(self): """Update current stage configuration""" row = self.stages_list.currentRow() if row >= 0 and row < len(self.stage_configs): config = self.stage_configs[row] config['name'] = self.stage_name_input.text() config['port_ids'] = self.port_ids_input.text() config['model_path'] = self.model_path_input.text() self.update_stages_list() self.stages_list.setCurrentRow(row) # Maintain selection def update_dongle_count(self, value): """Update dongle count for current stage""" self.dongle_count_label.setText(f"{value} dongles") row = self.stages_list.currentRow() if row >= 0 and row < len(self.stage_configs): self.stage_configs[row]['dongles'] = value self.update_stages_list() self.stages_list.setCurrentRow(row) # Update performance estimates self.update_performance_estimates(self.stage_configs[row]) def auto_balance_dongles(self): """Automatically balance dongles across stages""" if not self.stage_configs: return num_stages = len(self.stage_configs) dongles_per_stage = max(1, self.total_dongles_available // num_stages) remaining = self.total_dongles_available % num_stages for i, config in enumerate(self.stage_configs): config['dongles'] = dongles_per_stage + (1 if i < remaining else 0) self.update_stages_list() # Refresh current stage display current_row = self.stages_list.currentRow() if current_row >= 0: self.on_stage_selected(None, None) def browse_model_file(self): """Browse for model file""" filename, _ = QFileDialog.getOpenFileName( self, "Select Model File", "", "Model files (*.nef *.onnx *.tflite);;All files (*.*)" ) if filename: self.model_path_input.setText(filename) self.update_current_stage() def update_performance_estimates(self, config): """Calculate and display performance estimates""" dongles = config['dongles'] # Simple performance estimation based on dongle count base_fps = 30 # Base FPS per dongle estimated_fps = dongles * base_fps latency = 1000 / max(estimated_fps, 1) # ms throughput = estimated_fps * 1 # frames per second self.estimated_fps_label.setText(f"{estimated_fps:.1f} FPS") self.estimated_latency_label.setText(f"{latency:.1f} ms") self.throughput_label.setText(f"{throughput:.1f} frames/sec") def update_resource_summary(self): """Update resource usage summary""" total_dongles = sum(config['dongles'] for config in self.stage_configs) self.total_dongles_label.setText(f"{total_dongles} / {self.total_dongles_available}") # Calculate overall pipeline FPS (limited by slowest stage) if self.stage_configs: min_fps = min(config['dongles'] * 30 for config in self.stage_configs) self.pipeline_fps_label.setText(f"{min_fps:.1f} FPS") else: self.pipeline_fps_label.setText("--") def show_performance_panel(self): """Show performance estimation panel""" if not self.stage_configs: QMessageBox.warning(self, "No Stages", "Please add at least one stage before proceeding.") return # Validate stage configurations for config in self.stage_configs: if not config['name'].strip(): QMessageBox.warning(self, "Invalid Configuration", "All stages must have names.") return self.accept() # Show performance panel perf_panel = PerformanceEstimationPanel(self.stage_configs, self.parent()) perf_panel.exec_() def get_stage_configs(self): """Get configured stages""" return self.stage_configs # Simplified versions of the other dialogs for now class PerformanceEstimationPanel(QDialog): """Performance estimation and tweaking panel""" def __init__(self, stage_configs, parent=None): super().__init__(parent) self.stage_configs = stage_configs self.setWindowTitle("Performance Estimation & Optimization") self.setMinimumSize(600, 400) self.setStyleSheet(HARMONIOUS_THEME_STYLESHEET) self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Header header = QLabel("Pipeline Performance Analysis") header.setFont(QFont("Arial", 16, QFont.Bold)) header.setStyleSheet("color: #89b4fa; margin-bottom: 10px;") layout.addWidget(header) # Performance summary summary_text = f"Analyzing {len(self.stage_configs)} stages:\n" total_dongles = sum(config['dongles'] for config in self.stage_configs) min_fps = min(config['dongles'] * 30 for config in self.stage_configs) if self.stage_configs else 0 summary_text += f"• Total dongles: {total_dongles}\n" summary_text += f"• Pipeline FPS: {min_fps:.1f}\n" summary_text += f"• Estimated latency: {sum(1000/(config['dongles']*30) for config in self.stage_configs):.1f} ms" summary_label = QLabel(summary_text) summary_label.setStyleSheet("padding: 20px; background-color: #313244; border-radius: 8px;") layout.addWidget(summary_label) # Buttons button_layout = QHBoxLayout() self.deploy_btn = QPushButton("Next: Save & Deploy") self.deploy_btn.clicked.connect(self.show_deploy_dialog) self.back_btn = QPushButton("← Back") self.back_btn.clicked.connect(self.reject) button_layout.addWidget(self.back_btn) button_layout.addStretch() button_layout.addWidget(self.deploy_btn) layout.addLayout(button_layout) def show_deploy_dialog(self): """Show deployment dialog""" self.accept() deploy_dialog = SaveDeployDialog(self.stage_configs, self.parent()) deploy_dialog.exec_() class SaveDeployDialog(QDialog): """Save and Deploy dialog with cost estimation""" def __init__(self, stage_configs, parent=None): super().__init__(parent) self.stage_configs = stage_configs self.setWindowTitle("Save & Deploy Pipeline") self.setMinimumSize(600, 400) self.setStyleSheet(HARMONIOUS_THEME_STYLESHEET) self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Header header = QLabel("Save & Deploy Configuration") header.setFont(QFont("Arial", 16, QFont.Bold)) header.setStyleSheet("color: #89b4fa; margin-bottom: 10px;") layout.addWidget(header) # Export options export_group = QGroupBox("Export Configuration") export_layout = QFormLayout(export_group) self.config_name_input = QLineEdit() self.config_name_input.setText(f"pipeline_config_{time.strftime('%Y%m%d_%H%M%S')}") export_layout.addRow("Configuration Name:", self.config_name_input) self.export_format_combo = QComboBox() self.export_format_combo.addItems(["Python Script", "JSON Config", "YAML Pipeline"]) export_layout.addRow("Export Format:", self.export_format_combo) self.export_btn = QPushButton("💾 Export Configuration") self.export_btn.clicked.connect(self.export_configuration) export_layout.addRow("", self.export_btn) layout.addWidget(export_group) # Cost estimation total_dongles = sum(config['dongles'] for config in self.stage_configs) cost_text = f"Cost Estimation:\n" cost_text += f"• Hardware: {total_dongles} dongles (~${total_dongles * 299:,})\n" cost_text += f"• Power: {total_dongles * 5}W continuous\n" cost_text += f"• Monthly operating: ~${total_dongles * 15:.2f}" cost_label = QLabel(cost_text) cost_label.setStyleSheet("padding: 20px; background-color: #313244; border-radius: 8px;") layout.addWidget(cost_label) # Buttons button_layout = QHBoxLayout() self.deploy_btn = QPushButton("🚀 Deploy Pipeline") self.deploy_btn.clicked.connect(self.deploy_pipeline) self.back_btn = QPushButton("← Back") self.back_btn.clicked.connect(self.reject) button_layout.addWidget(self.back_btn) button_layout.addStretch() button_layout.addWidget(self.deploy_btn) layout.addLayout(button_layout) def export_configuration(self): """Export configuration to file""" format_type = self.export_format_combo.currentText() config_name = self.config_name_input.text().strip() if not config_name: QMessageBox.warning(self, "Invalid Name", "Please enter a configuration name.") return # Generate content based on format if format_type == "Python Script": content = self.generate_python_script() ext = ".py" elif format_type == "JSON Config": content = json.dumps({ "stages": [{"name": c['name'], "dongles": c['dongles'], "port_ids": c['port_ids']} for c in self.stage_configs] }, indent=2) ext = ".json" else: # YAML content = "stages:\n" for config in self.stage_configs: content += f" - name: {config['name']}\n dongles: {config['dongles']}\n port_ids: {config['port_ids']}\n" ext = ".yaml" # Save file filename, _ = QFileDialog.getSaveFileName( self, f"Save {format_type}", f"{config_name}{ext}", f"{format_type} files (*{ext});;All files (*.*)" ) if filename: try: with open(filename, 'w') as f: f.write(content) QMessageBox.information(self, "Exported", f"Configuration exported to {os.path.basename(filename)}") except Exception as e: QMessageBox.critical(self, "Export Error", f"Failed to export: {str(e)}") def generate_python_script(self): """Generate Python script""" script = '''#!/usr/bin/env python3 """ Generated Pipeline Configuration """ from src.cluster4npu.InferencePipeline import InferencePipeline, StageConfig def main(): stage_configs = [ ''' for config in self.stage_configs: port_ids = config['port_ids'].split(',') if ',' in config['port_ids'] else [28, 30] script += f''' StageConfig( stage_id="{config['name'].lower().replace(' ', '_')}", port_ids={port_ids}, scpu_fw_path="fw_scpu.bin", ncpu_fw_path="fw_ncpu.bin", model_path="{config.get('model_path', 'model.nef')}", upload_fw=True ), ''' script += ''' ] pipeline = InferencePipeline(stage_configs) pipeline.initialize() pipeline.start() try: import time while True: time.sleep(1) except KeyboardInterrupt: pipeline.stop() if __name__ == "__main__": main() ''' return script def deploy_pipeline(self): """Deploy the pipeline""" QMessageBox.information( self, "Deployment Started", "Pipeline deployment initiated!\n\nThis would start the actual hardware deployment process." ) self.accept() if __name__ == '__main__': app.setFont(QFont("Arial", 9)) dashboard = DashboardLogin() dashboard.show() sys.exit(app.exec_())