import sys from PySide6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QTextEdit, QPushButton, QLabel, QSplitter ) from PySide6.QtCore import Qt from PySide6 import QtGui from NodeGraphQt import NodeGraph, BaseNode from core.workflow import Graph from nodes.image_reader import ImageReader from nodes.feature_extractor import FeatureExtractor from nodes.feature_writer import FeatureWriter # GUI node wrappers around core nodes class ImageReaderNode(BaseNode): __identifier__ = "radiuma.gui" NODE_NAME = "ImageReader" def __init__(self): super().__init__() self.add_output("image") self.core_node = ImageReader() class FeatureExtractorNode(BaseNode): __identifier__ = "radiuma.gui" NODE_NAME = "FeatureExtractor" def __init__(self): super().__init__() self.add_input("image") self.add_output("features") self.core_node = FeatureExtractor() class FeatureWriterNode(BaseNode): __identifier__ = "radiuma.gui" NODE_NAME = "FeatureWriter" def __init__(self): super().__init__() self.add_input("features") self.core_node = FeatureWriter() class VisualEditor(QMainWindow): # Visual workflow editor with dynamic semantic validation def __init__(self): super().__init__() self.setWindowTitle("Radiuma R&D – Semantic Workflow Editor") self.resize(1200, 800) self.graph_engine = Graph() self.graph = NodeGraph() self.viewer = self.graph.widget self.graph.register_node(ImageReaderNode) self.graph.register_node(FeatureExtractorNode) self.graph.register_node(FeatureWriterNode) self.log_panel = QTextEdit() self.log_panel.setReadOnly(True) btn_reader = QPushButton("Add ImageReader") btn_extractor = QPushButton("Add FeatureExtractor") btn_writer = QPushButton("Add FeatureWriter") btn_reader.clicked.connect( lambda: self.graph.create_node("radiuma.gui.ImageReaderNode") ) btn_extractor.clicked.connect( lambda: self.graph.create_node("radiuma.gui.FeatureExtractorNode") ) btn_writer.clicked.connect( lambda: self.graph.create_node("radiuma.gui.FeatureWriterNode") ) left_panel = QWidget() left_layout = QVBoxLayout(left_panel) left_layout.addWidget(QLabel("Nodes")) left_layout.addWidget(btn_reader) left_layout.addWidget(btn_extractor) left_layout.addWidget(btn_writer) left_layout.addStretch() top_splitter = QSplitter(Qt.Horizontal) top_splitter.addWidget(left_panel) top_splitter.addWidget(self.viewer) main_splitter = QSplitter(Qt.Vertical) main_splitter.addWidget(top_splitter) main_splitter.addWidget(self.log_panel) main_splitter.setSizes([600, 200]) self.setCentralWidget(main_splitter) self.graph.port_connected.connect(self._on_port_connected) self.graph.port_disconnected.connect(self._on_port_disconnected) def log(self, msg, level="INFO"): self.log_panel.append(f"[{level}] {msg}") def _on_port_connected(self, port_a, port_b): node_a = port_a.node() node_b = port_b.node() core_a = getattr(node_a, "core_node", None) core_b = getattr(node_b, "core_node", None) if not core_a or not core_b: self._color_connection(port_a, port_b, (200, 200, 200)) self.log("Missing core node mapping", "ERROR") return # Normalize out → in if port_a.type_() == "out": out_node, out_port = core_a, port_a.name() in_node, in_port = core_b, port_b.name() gui_out, gui_in = port_a, port_b else: out_node, out_port = core_b, port_b.name() in_node, in_port = core_a, port_a.name() gui_out, gui_in = port_b, port_a ok, msg = self.graph_engine.can_connect( out_node, out_port, in_node, in_port ) if ok: self._color_connection(gui_out, gui_in, (46, 204, 113)) self.log(msg, "INFO") else: self._color_connection(gui_out, gui_in, (231, 76, 60)) self.log(msg, "ERROR") def _on_port_disconnected(self, port_a, port_b): self._color_connection(port_a, port_b, (180, 180, 180)) self.log("Disconnected ports", "INFO") def _color_connection(self, port_a, port_b, color): # Patch connection paint dynamically scene = self.graph._viewer.scene() for item in scene.items(): if hasattr(item, "port1") and hasattr(item, "port2"): if {item.port1, item.port2} == {port_a, port_b}: setattr(item, "_custom_color", color) if not hasattr(item, "_patched"): original_paint = item.paint def patched_paint(painter, option, widget=None, _orig=original_paint, _item=item): pen = painter.pen() pen.setColor(QtGui.QColor(*_item._custom_color)) painter.setPen(pen) _orig(painter, option, widget) item.paint = patched_paint item._patched = True item.update() return